chronograph-npm 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cg.d.ts +23 -0
- package/dist/cg.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cg.js +19 -31
- package/test/cg.test.js +117 -0
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ class MyApp extends Homey.App {
|
|
|
36
36
|
module.exports = MyApp;
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`CG.init` accepts one option, `homey`, which must be the Homey instance. It
|
|
39
|
+
`CG.init` accepts one option, `homey`, which must be the Homey instance. It checks that Chronograph is installed and responding to its `/chronographs` API before registering a realtime listener. This checks availability during initialization, not continuously afterward. The response is discarded; call `CG.getChronographs()` or `CG.getAllChronographs()` when you need that data. If a check fails, the error is logged, `isReady` remains false, and `ready` remains pending; call `instance.init()` to retry. When Homey emits `unload` as the consuming app stops, `CG.destroy()` is called automatically to remove the listeners, without replacing your app's lifecycle methods. Cleanup cannot be guaranteed if the process is forcibly terminated.
|
|
40
40
|
|
|
41
41
|
## Available API
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ The package exports the `CG` class:
|
|
|
49
49
|
| `CG.getAllChronographs()` | Returns configured chronograph names grouped by type, such as `timer`, `stopwatch`, and `transition`. |
|
|
50
50
|
| `CG.onChronographUpdate({ chronograph })` | Update hook. Override it to handle realtime updates; the default implementation does nothing. |
|
|
51
51
|
| `CG.destroy()` | Removes the realtime and unload listeners. Called automatically on Homey's `unload` event; can also be called manually. |
|
|
52
|
-
| `instance.ready` / `CG.ready` | Promise that resolves after
|
|
52
|
+
| `instance.ready` / `CG.ready` | Promise that resolves after the installation and API checks and realtime-listener registration succeed. |
|
|
53
53
|
| `instance.isReady` / `CG.isReady` | Indicates whether the integration has become ready. |
|
|
54
54
|
|
|
55
55
|
For example, retrieve active chronographs after initialization:
|
|
@@ -93,4 +93,4 @@ module.exports = MyApp;
|
|
|
93
93
|
- The Chronograph app must be installed on the same Homey.
|
|
94
94
|
- Your app must declare the Chronograph permission in `app.json`.
|
|
95
95
|
- Initialize the package with your app's Homey instance before calling the retrieval methods.
|
|
96
|
-
- Initialization
|
|
96
|
+
- Initialization checks the Chronograph API and registers for realtime updates; fetch chronograph data explicitly using the retrieval methods.
|
package/dist/cg.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export class CG {
|
|
|
54
54
|
});
|
|
55
55
|
homey: any;
|
|
56
56
|
isReady: boolean;
|
|
57
|
+
_destroyed: boolean;
|
|
58
|
+
_defer: Defer;
|
|
57
59
|
ready: any;
|
|
58
60
|
init({}?: {}): Promise<CG>;
|
|
59
61
|
api: any;
|
|
@@ -61,4 +63,25 @@ export class CG {
|
|
|
61
63
|
/** @private */
|
|
62
64
|
private getAppApi;
|
|
63
65
|
}
|
|
66
|
+
declare class Defer {
|
|
67
|
+
/**
|
|
68
|
+
* @param {Number} timeout
|
|
69
|
+
*/
|
|
70
|
+
constructor(timeout: number, originalPromise: any, onTimeout: any);
|
|
71
|
+
set isDefered(arg: boolean);
|
|
72
|
+
get isDefered(): boolean;
|
|
73
|
+
_isDefered: any;
|
|
74
|
+
set promise(arg: any);
|
|
75
|
+
get promise(): any;
|
|
76
|
+
_promise: any;
|
|
77
|
+
_resolve: any;
|
|
78
|
+
_reject: any;
|
|
79
|
+
timeout: any;
|
|
80
|
+
then(fun: any): Defer;
|
|
81
|
+
catch(fun: any): Defer;
|
|
82
|
+
finally(fun: any): Defer;
|
|
83
|
+
resolve(val1: any, val2: any, val3: any, val4: any): any;
|
|
84
|
+
reject(val1: any, val2: any, val3: any, val4: any): any;
|
|
85
|
+
}
|
|
86
|
+
export {};
|
|
64
87
|
//# sourceMappingURL=cg.d.ts.map
|
package/dist/cg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cg.d.ts","sourceRoot":"","sources":["../src/cg.js"],"names":[],"mappings":"AAAA;IAGI;;
|
|
1
|
+
{"version":3,"file":"cg.d.ts","sourceRoot":"","sources":["../src/cg.js"],"names":[],"mappings":"AAAA;IAGI;;oBAOC;IAkCD;;OAEG;IACH,uBAUC;IA2BD,0CAEC;IAED,uCAEC;IAKD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH;QAlB0B,WAAW;YACE,EAAE,GAA9B,MAAM;YACqB,IAAI,EAA/B,MAAM;YACqB,QAAQ,EAAnC,MAAM;YACqB,OAAO,EAAlC,MAAM;YAC0B,cAAc,EAA9C,MAAM,GAAC,IAAI;YAEqB,SAAS,EAAzC,MAAM,GAAC,IAAI;YAEiB,OAAO,EAAnC,OAAO;YACqB,OAAO,EAAnC,OAAO;YACkB,GAAG,EAA5B,IAAI;YACuB,IAAI;gBAC4B,IAAI,GAA/D,OAAO,GAAC,WAAW,GAAC,YAAY;gBACC,IAAI,GAArC,MAAM;gBAC2B,EAAE,GAAnC,MAAM;gBAC2B,KAAK,GAAtC,MAAM;;;sBAKhB;IA7GD;;OAaC;IAXG,WAA6B;IAChB,iBAAoB;IACjC,oBAAuB;IAEvB,cAAqC;IAErC,WAA2C;IAO/C,2BAcC;IARW,SAAc;IA4B1B,uDAYC;IAGD,eAAe;IACf,kBAKC;CA2CJ;AAID;IAMI;;OAEG;IACH,mEAeC;IAtBD,4BAAyC;IADzC,yBAA6C;IAC1B,gBAAmB;IAGtC,sBAAqC;IADrC,mBAAuC;IACtB,cAAiB;IAM1B,cAAuB;IACvB,aAAqB;IAGrB,aAMsB;IAI9B,sBAAmD;IACnD,uBAAqD;IACrD,yBAAyD;IACzD,yDAAoK;IACpK,wDAAkK;CACrK"}
|
package/package.json
CHANGED
package/src/cg.js
CHANGED
|
@@ -6,33 +6,32 @@ class CG {
|
|
|
6
6
|
let cg = new CG({ homey });
|
|
7
7
|
|
|
8
8
|
homey.once('unload', CG.destroy);
|
|
9
|
-
|
|
10
|
-
await cg.init();
|
|
11
|
-
} catch (error) {
|
|
12
|
-
|
|
13
|
-
}
|
|
9
|
+
await cg.init();
|
|
14
10
|
return cg;
|
|
15
11
|
}
|
|
16
12
|
constructor({ homey }) {
|
|
17
13
|
CG.Current = this;
|
|
18
14
|
this.homey = CG.homey = homey;
|
|
19
|
-
this.isReady = false;
|
|
15
|
+
CG.isReady = this.isReady = false;
|
|
16
|
+
this._destroyed = false;
|
|
20
17
|
|
|
21
|
-
CG._defer = new Defer();
|
|
18
|
+
this._defer = CG._defer = new Defer();
|
|
22
19
|
|
|
23
|
-
this.ready = CG.ready =
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
this.ready = CG.ready = this._defer.promise;
|
|
21
|
+
this.ready.then(() => {
|
|
22
|
+
this.isReady = true;
|
|
23
|
+
if (CG.Current === this) CG.isReady = true;
|
|
26
24
|
});
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
async init({ } = {}) {
|
|
30
28
|
if (!this.api) {
|
|
31
29
|
try {
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const api = await this.getAppApi();
|
|
31
|
+
if (this._destroyed) return this;
|
|
32
|
+
api.on('realtime', this.realtimeFunction);
|
|
33
|
+
this.api = api;
|
|
34
|
+
this._defer.resolve();
|
|
36
35
|
} catch (error) {
|
|
37
36
|
this.homey.error('CG.init', error);
|
|
38
37
|
return this;
|
|
@@ -49,6 +48,7 @@ class CG {
|
|
|
49
48
|
static destroy() {
|
|
50
49
|
try {
|
|
51
50
|
if (!CG.Current) return;
|
|
51
|
+
CG.Current._destroyed = true;
|
|
52
52
|
CG.Current.homey.removeListener('unload', CG.destroy);
|
|
53
53
|
if (CG.Current.api) {
|
|
54
54
|
CG.Current.api.off('realtime', CG.Current.realtimeFunction);
|
|
@@ -59,8 +59,6 @@ class CG {
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
async realtimeFunction(event, data) {
|
|
62
|
-
if (!CG.isReady && CG._defer) CG._defer.resolve();
|
|
63
|
-
|
|
64
62
|
if (event === 'chronograph_event') {
|
|
65
63
|
if (data && data.now) data.now = new Date(data.now);
|
|
66
64
|
CG.onChronographUpdate({ chronograph: data });
|
|
@@ -76,21 +74,11 @@ class CG {
|
|
|
76
74
|
|
|
77
75
|
|
|
78
76
|
/** @private */
|
|
79
|
-
async getAppApi(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (onlyApi === true) return this.api;
|
|
86
|
-
if (!this.api) throw new Error('Chronograph is not installed.');
|
|
87
|
-
else {
|
|
88
|
-
try {
|
|
89
|
-
BL._isLoaded = await this.api.getInstalled();
|
|
90
|
-
} catch (error) { }
|
|
91
|
-
if (!BL._isLoaded) throw new Error('Chronograph is not installed.');
|
|
92
|
-
}
|
|
93
|
-
return this.api;
|
|
77
|
+
async getAppApi() {
|
|
78
|
+
const api = this.homey.api.getApiApp('nl.fellownet.chronograph');
|
|
79
|
+
if (!api || !await api.getInstalled()) throw new Error('Chronograph is not installed.');
|
|
80
|
+
await api.get('/chronographs');
|
|
81
|
+
return api;
|
|
94
82
|
}
|
|
95
83
|
|
|
96
84
|
|
package/test/cg.test.js
CHANGED
|
@@ -6,12 +6,129 @@ const { CG } = require('../src/cg');
|
|
|
6
6
|
function createHomey() {
|
|
7
7
|
const homey = new EventEmitter();
|
|
8
8
|
const api = new EventEmitter();
|
|
9
|
+
api.getInstalled = async () => true;
|
|
10
|
+
api.get = async () => [];
|
|
9
11
|
homey.api = { getApiApp: () => api };
|
|
10
12
|
homey.app = { onUninit() {} };
|
|
11
13
|
homey.error = (...args) => assert.fail(args.join(' '));
|
|
12
14
|
return { homey, api };
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
test('readiness requires Chronograph to be installed and supports retry', async (t) => {
|
|
18
|
+
const { homey, api } = createHomey();
|
|
19
|
+
const errors = [];
|
|
20
|
+
homey.error = (...args) => errors.push(args);
|
|
21
|
+
let installed = false;
|
|
22
|
+
api.getInstalled = async () => installed;
|
|
23
|
+
t.after(() => CG.destroy());
|
|
24
|
+
|
|
25
|
+
const cg = await CG.init({ homey });
|
|
26
|
+
let resolved = false;
|
|
27
|
+
cg.ready.then(() => { resolved = true; });
|
|
28
|
+
await cg.realtimeFunction('other');
|
|
29
|
+
|
|
30
|
+
assert.equal(cg.isReady, false);
|
|
31
|
+
assert.equal(CG.isReady, false);
|
|
32
|
+
assert.equal(resolved, false);
|
|
33
|
+
assert.equal(api.listenerCount('realtime'), 0);
|
|
34
|
+
assert.match(errors[0][1].message, /not installed/);
|
|
35
|
+
|
|
36
|
+
installed = true;
|
|
37
|
+
await cg.init();
|
|
38
|
+
await cg.ready;
|
|
39
|
+
assert.equal(cg.isReady, true);
|
|
40
|
+
assert.equal(CG.isReady, true);
|
|
41
|
+
assert.equal(api.listenerCount('realtime'), 1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('an installation check failure leaves readiness false and logs the error', async (t) => {
|
|
45
|
+
const { homey, api } = createHomey();
|
|
46
|
+
const failure = new Error('installation check failed');
|
|
47
|
+
const errors = [];
|
|
48
|
+
homey.error = (...args) => errors.push(args);
|
|
49
|
+
api.getInstalled = async () => { throw failure; };
|
|
50
|
+
t.after(() => CG.destroy());
|
|
51
|
+
|
|
52
|
+
const cg = await CG.init({ homey });
|
|
53
|
+
assert.equal(cg.isReady, false);
|
|
54
|
+
assert.equal(CG.isReady, false);
|
|
55
|
+
assert.equal(api.listenerCount('realtime'), 0);
|
|
56
|
+
assert.equal(errors[0][1], failure);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('readiness requires the installed app to respond to its API', async (t) => {
|
|
60
|
+
const { homey, api } = createHomey();
|
|
61
|
+
const failure = new Error('app is not running');
|
|
62
|
+
const errors = [];
|
|
63
|
+
homey.error = (...args) => errors.push(args);
|
|
64
|
+
let running = false;
|
|
65
|
+
const paths = [];
|
|
66
|
+
api.get = async (path) => {
|
|
67
|
+
paths.push(path);
|
|
68
|
+
if (!running) throw failure;
|
|
69
|
+
return [];
|
|
70
|
+
};
|
|
71
|
+
t.after(() => CG.destroy());
|
|
72
|
+
|
|
73
|
+
const cg = await CG.init({ homey });
|
|
74
|
+
assert.equal(cg.isReady, false);
|
|
75
|
+
assert.equal(CG.isReady, false);
|
|
76
|
+
assert.equal(api.listenerCount('realtime'), 0);
|
|
77
|
+
assert.equal(errors[0][1], failure);
|
|
78
|
+
assert.deepEqual(paths, ['/chronographs']);
|
|
79
|
+
|
|
80
|
+
running = true;
|
|
81
|
+
await cg.init();
|
|
82
|
+
await cg.ready;
|
|
83
|
+
assert.equal(cg.isReady, true);
|
|
84
|
+
assert.equal(CG.isReady, true);
|
|
85
|
+
assert.deepEqual(paths, ['/chronographs', '/chronographs']);
|
|
86
|
+
assert.equal(api.listenerCount('realtime'), 1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('readiness remains false while the running check is pending', async (t) => {
|
|
90
|
+
const { homey, api } = createHomey();
|
|
91
|
+
let finishRequest;
|
|
92
|
+
let requestStarted;
|
|
93
|
+
const started = new Promise(resolve => { requestStarted = resolve; });
|
|
94
|
+
api.get = () => {
|
|
95
|
+
requestStarted();
|
|
96
|
+
return new Promise(resolve => { finishRequest = resolve; });
|
|
97
|
+
};
|
|
98
|
+
t.after(() => CG.destroy());
|
|
99
|
+
|
|
100
|
+
const initializing = CG.init({ homey });
|
|
101
|
+
await started;
|
|
102
|
+
assert.equal(CG.Current.isReady, false);
|
|
103
|
+
assert.equal(CG.isReady, false);
|
|
104
|
+
assert.equal(api.listenerCount('realtime'), 0);
|
|
105
|
+
|
|
106
|
+
finishRequest([]);
|
|
107
|
+
const cg = await initializing;
|
|
108
|
+
assert.equal(cg.isReady, true);
|
|
109
|
+
assert.equal(CG.isReady, true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('a replaced initialization cannot become ready after its check completes', async (t) => {
|
|
113
|
+
const first = createHomey();
|
|
114
|
+
const second = createHomey();
|
|
115
|
+
let finishCheck;
|
|
116
|
+
first.api.getInstalled = () => new Promise(resolve => { finishCheck = resolve; });
|
|
117
|
+
second.api.getInstalled = async () => false;
|
|
118
|
+
second.homey.error = () => {};
|
|
119
|
+
t.after(() => CG.destroy());
|
|
120
|
+
|
|
121
|
+
const firstInit = CG.init({ homey: first.homey });
|
|
122
|
+
const secondCg = await CG.init({ homey: second.homey });
|
|
123
|
+
finishCheck(true);
|
|
124
|
+
const firstCg = await firstInit;
|
|
125
|
+
|
|
126
|
+
assert.equal(firstCg.isReady, false);
|
|
127
|
+
assert.equal(secondCg.isReady, false);
|
|
128
|
+
assert.equal(CG.isReady, false);
|
|
129
|
+
assert.equal(first.api.listenerCount('realtime'), 0);
|
|
130
|
+
});
|
|
131
|
+
|
|
15
132
|
test('unload removes the realtime listener without replacing app lifecycle methods', async (t) => {
|
|
16
133
|
const { homey, api } = createHomey();
|
|
17
134
|
const onUninit = homey.app.onUninit;
|