Skip to content

Commit 13ebede

Browse files
committed
refactor: make listener manually managed
1 parent 7b57e0f commit 13ebede

File tree

4 files changed

+111
-41
lines changed

4 files changed

+111
-41
lines changed

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Return Value Descriptions:
3434
* 'Denied' - The user explicitly denied access to contact data for the application.
3535
* 'Authorized' - The application is authorized to access contact data.
3636

37+
Example Usage:
38+
3739
```js
3840
const authStatus = contacts.getAuthStatus()
3941

@@ -73,7 +75,7 @@ The returned objects will take the following format:
7375

7476
This method will return an empty array (`[]`) if access to Contacts has not been granted.
7577

76-
Example:
78+
Example Usage:
7779

7880
```js
7981
const allContacts = contacts.getAllContacts()
@@ -123,7 +125,7 @@ The returned object will take the following format:
123125

124126
This method will return an empty array (`[]`) if access to Contacts has not been granted.
125127

126-
Example:
128+
Example Usage:
127129

128130
```js
129131
const contacts = contacts.getContactsByName('Appleseed')
@@ -160,7 +162,7 @@ Creates and save a new contact to the user's contacts database.
160162

161163
This method will return `false` if access to Contacts has not been granted.
162164

163-
Example:
165+
Example Usage:
164166

165167
```js
166168
const success = contacts.addNewContact({
@@ -188,7 +190,7 @@ However, you should take care to specify `name` to such a degree that you can be
188190

189191
This method will return `false` if access to Contacts has not been granted.
190192

191-
Example:
193+
Example Usage:
192194

193195
```js
194196
const name = 'Jonathan Appleseed'
@@ -215,7 +217,7 @@ You should take care to specify parameters to the `contact` object to such a deg
215217

216218
This method will return `false` if access to Contacts has not been granted.
217219

218-
Example:
220+
Example Usage:
219221

220222
```js
221223
// Change contact's nickname from Billy -> Will
@@ -232,11 +234,15 @@ console.log(`Contact was ${updated ? 'updated' : 'not updated'}.`)
232234

233235
This module exposes an `EventEmitter`, which can be used to listen to potential changes to the `CNContactStore`. When a contact is changed either with methods contained in this module, or manually by a user, the `contact-changed` event will be emitted.
234236

235-
Example:
237+
Owing to the underlying architecture of this module, the listener must be manually managed; before use you must initialize it with `listener.setup()` and when you are finished listening for events you must remove it with `listener.remove()`
238+
239+
Example Usage:
236240

237241
```js
238242
const { listener, addNewContact } = require('node-mac-contacts')
239243

244+
listener.setup()
245+
240246
addNewContact({
241247
firstName: 'William',
242248
lastName: 'Grapeseed',
@@ -246,7 +252,8 @@ addNewContact({
246252
emailAddresses: ['billy@grapeseed.com'],
247253
})
248254

249-
listener.on('contact-changed', () => {
255+
listener.once('contact-changed', () => {
250256
console.log('A contact was changed!')
257+
listener.remove()
251258
})
252259
```

contacts.mm

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <napi.h>
33

44
Napi::ThreadSafeFunction ts_fn;
5+
id observer;
56

67
/***** HELPERS *****/
78

@@ -508,12 +509,12 @@ CNAuthorizationStatus AuthStatus() {
508509
return Napi::Boolean::New(env, success);
509510
}
510511

511-
Napi::Boolean HandleEmit(const Napi::CallbackInfo &info) {
512+
Napi::Boolean SetupListener(const Napi::CallbackInfo &info) {
512513
Napi::Env env = info.Env();
513514
ts_fn = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(),
514515
"emitCallback", 0, 1);
515516

516-
[[NSNotificationCenter defaultCenter]
517+
observer = [[NSNotificationCenter defaultCenter]
517518
addObserverForName:CNContactStoreDidChangeNotification
518519
object:nil
519520
queue:[NSOperationQueue mainQueue]
@@ -523,16 +524,32 @@ CNAuthorizationStatus AuthStatus() {
523524
js_cb.Call({Napi::String::New(env, value)});
524525
};
525526
ts_fn.BlockingCall("contact-changed", callback);
526-
ts_fn.Release();
527527
}];
528528

529529
return Napi::Boolean::New(env, true);
530530
}
531531

532+
Napi::Boolean RemoveListener(const Napi::CallbackInfo &info) {
533+
Napi::Env env = info.Env();
534+
535+
if (!observer) {
536+
Napi::Error::New(env, "No observers are currently listening")
537+
.ThrowAsJavaScriptException();
538+
return Napi::Boolean::New(env, false);
539+
}
540+
541+
ts_fn.Release();
542+
[[NSNotificationCenter defaultCenter] removeObserver:observer];
543+
544+
return Napi::Boolean::New(env, true);
545+
}
546+
532547
// Initializes all functions exposed to JS.
533548
Napi::Object Init(Napi::Env env, Napi::Object exports) {
534-
exports.Set(Napi::String::New(env, "handleEmit"),
535-
Napi::Function::New(env, HandleEmit));
549+
exports.Set(Napi::String::New(env, "setupListener"),
550+
Napi::Function::New(env, SetupListener));
551+
exports.Set(Napi::String::New(env, "removeListener"),
552+
Napi::Function::New(env, RemoveListener));
536553
exports.Set(Napi::String::New(env, "getAuthStatus"),
537554
Napi::Function::New(env, GetAuthStatus));
538555
exports.Set(Napi::String::New(env, "getAllContacts"),

index.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ const contacts = require('bindings')('contacts.node')
33
const { EventEmitter } = require('events')
44

55
const listener = new EventEmitter()
6-
contacts.handleEmit(listener.emit.bind(listener))
76

8-
const validProperties = [
7+
listener.setup = () => {
8+
contacts.setupListener(listener.emit.bind(listener))
9+
}
10+
11+
listener.remove = () => {
12+
contacts.removeListener()
13+
}
14+
15+
const optionalProperties = [
916
'jobTitle',
1017
'departmentName',
1118
'organizationName',
@@ -22,9 +29,9 @@ function getAllContacts(extraProperties = []) {
2229
throw new TypeError('extraProperties must be an array')
2330
}
2431

25-
if (!extraProperties.every((p) => validProperties.includes(p))) {
32+
if (!extraProperties.every((p) => optionalProperties.includes(p))) {
2633
throw new TypeError(
27-
`properties in extraProperties must be one of ${validProperties.join(
34+
`properties in extraProperties must be one of ${optionalProperties.join(
2835
', ',
2936
)}`,
3037
)
@@ -42,9 +49,9 @@ function getContactsByName(name, extraProperties = []) {
4249
throw new TypeError('extraProperties must be an array')
4350
}
4451

45-
if (!extraProperties.every((p) => validProperties.includes(p))) {
52+
if (!extraProperties.every((p) => optionalProperties.includes(p))) {
4653
throw new TypeError(
47-
`properties in extraProperties must be one of ${validProperties.join(
54+
`properties in extraProperties must be one of ${optionalProperties.join(
4855
', ',
4956
)}`,
5057
)

test/module.spec.js

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,6 @@ describe('node-mac-contacts', () => {
2323
})
2424
})
2525

26-
describe('getContactsByName(name[, extraProperties])', () => {
27-
it('should throw if name is not a string', () => {
28-
expect(() => {
29-
getContactsByName(12345)
30-
}).to.throw(/name must be a string/)
31-
})
32-
33-
it('should throw if extraProperties is not an array', () => {
34-
expect(() => {
35-
getContactsByName('jim-bob', 12345)
36-
}).to.throw(/extraProperties must be an array/)
37-
})
38-
39-
it('should throw if extraProperties contains invalid properties', () => {
40-
const errorMessage =
41-
'properties in extraProperties must be one of jobTitle, departmentName, organizationName, middleName, note, contactImage, contactThumbnailImage, instantMessageAddresses, socialProfiles'
42-
43-
expect(() => {
44-
getContactsByName('jim-bob', ['bad-property'])
45-
}).to.throw(errorMessage)
46-
})
47-
})
48-
4926
describe('getAllContacts([extraProperties])', () => {
5027
it('should return an array', () => {
5128
const contacts = getAllContacts()
@@ -114,6 +91,59 @@ describe('node-mac-contacts', () => {
11491
addNewContact({ emailAddresses: 1 })
11592
}).to.throw(/emailAddresses must be an array/)
11693
})
94+
95+
it('should successfully add a contact', () => {
96+
const success = addNewContact({
97+
firstName: 'William',
98+
lastName: 'Grapeseed',
99+
nickname: 'Billy',
100+
birthday: '1990-09-09',
101+
phoneNumbers: ['+1234567890'],
102+
emailAddresses: ['billy@grapeseed.com'],
103+
})
104+
105+
expect(success).to.be.true
106+
})
107+
})
108+
109+
describe('getContactsByName(name[, extraProperties])', () => {
110+
it('should throw if name is not a string', () => {
111+
expect(() => {
112+
getContactsByName(12345)
113+
}).to.throw(/name must be a string/)
114+
})
115+
116+
it('should throw if extraProperties is not an array', () => {
117+
expect(() => {
118+
getContactsByName('jim-bob', 12345)
119+
}).to.throw(/extraProperties must be an array/)
120+
})
121+
122+
it('should throw if extraProperties contains invalid properties', () => {
123+
const errorMessage =
124+
'properties in extraProperties must be one of jobTitle, departmentName, organizationName, middleName, note, contactImage, contactThumbnailImage, instantMessageAddresses, socialProfiles'
125+
126+
expect(() => {
127+
getContactsByName('jim-bob', ['bad-property'])
128+
}).to.throw(errorMessage)
129+
})
130+
131+
it('should retrieve a contact by name predicates', () => {
132+
addNewContact({
133+
firstName: 'Sherlock',
134+
lastName: 'Holmes',
135+
nickname: 'Sherllock',
136+
birthday: '1854-01-06',
137+
phoneNumbers: ['+1234567890'],
138+
emailAddresses: ['sherlock@holmes.com'],
139+
})
140+
141+
const contacts = getContactsByName('Sherlock Holmes')
142+
expect(contacts).to.be.an('array').of.length.gte(1)
143+
144+
const contact = contacts[0]
145+
expect(contact.firstName).to.eql('Sherlock')
146+
})
117147
})
118148

119149
describe('deleteContact(name)', () => {
@@ -173,7 +203,15 @@ describe('node-mac-contacts', () => {
173203
})
174204

175205
describe('listener', () => {
206+
it('throws when trying to remove a nonexistent listener', () => {
207+
expect(() => {
208+
listener.remove()
209+
}).to.throw(/No observers are currently listening/)
210+
})
211+
176212
it('emits an event when the contact is changed', (done) => {
213+
listener.setup()
214+
177215
addNewContact({
178216
firstName: 'William',
179217
lastName: 'Grapeseed',
@@ -184,6 +222,7 @@ describe('node-mac-contacts', () => {
184222
})
185223

186224
listener.once('contact-changed', () => {
225+
listener.remove()
187226
done()
188227
})
189228
})

0 commit comments

Comments
 (0)