chronograph-npm 0.1.4 → 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.
@@ -0,0 +1,15 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: XeroxQ
4
+ patreon: QlusterIT
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: # Replace with a single Ko-fi username
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12
+ polar: # Replace with a single Polar username
13
+ buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14
+ thanks_dev: # Replace with a single thanks.dev username
15
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,19 @@
1
+ name: Add issues/PRs to project
2
+
3
+ on:
4
+ issues:
5
+ types:
6
+ - opened
7
+ pull_request:
8
+ types:
9
+ - opened
10
+
11
+ jobs:
12
+ add-to-project:
13
+ name: Add issue/PR to project
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/add-to-project@v2
17
+ with:
18
+ project-url: https://github.com/orgs/Qluster-IT/projects/1
19
+ github-token: ${{ secrets.PAT }}
@@ -0,0 +1,22 @@
1
+ name: Issue/PR assignment
2
+
3
+ on:
4
+ issues:
5
+ types: [opened]
6
+ pull_request:
7
+ types: [opened] #edited, synchronize, reopened
8
+
9
+ jobs:
10
+ auto-assign:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ issues: write
14
+ pull-requests: write
15
+ steps:
16
+ - name: 'Auto-assign issue/PR'
17
+ uses: pozil/auto-assign-issue@v4
18
+ with:
19
+ assignees: XeroxQ
20
+ numOfAssignee: 1
21
+ allowSelfAssign: true
22
+ repo-token: ${{ secrets.PAT }}
@@ -0,0 +1,82 @@
1
+ name: Project Item Unarchiveren en Status naar Ready
2
+
3
+ on:
4
+ issues:
5
+ types: [reopened]
6
+ pull_request:
7
+ types: [reopened]
8
+
9
+ jobs:
10
+ unarchive-and-move:
11
+ runs-on: ubuntu-latest
12
+ env:
13
+ # Hier splitsen we de URL op om de CLI-fout te voorkomen
14
+ PROJECT_OWNER: "Qluster-IT"
15
+ PROJECT_NUMBER: 1
16
+ GITHUB_TOKEN: ${{ secrets.PAT }}
17
+ ISSUE_NODE_ID: ${{ github.event.issue.node_id }}
18
+
19
+ steps:
20
+ - name: Zoek gekoppeld Project en Item via GraphQL
21
+ id: item_info
22
+ run: |
23
+ RESPONSE=$(gh api graphql -f query='
24
+ query($issueId: ID!) {
25
+ node(id: $issueId) {
26
+ ... on Issue {
27
+ projectItems(first: 10) {
28
+ nodes {
29
+ id
30
+ isArchived
31
+ project {
32
+ id
33
+ number
34
+ owner {
35
+ ... on Organization { login }
36
+ ... on User { login }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ' -f issueId="$ISSUE_NODE_ID")
45
+
46
+ # Filter de data voor het juiste project
47
+ ITEM_DATA=$(echo "$RESPONSE" | jq -r --argjson num "${{ env.PROJECT_NUMBER }}" --arg owner "${{ env.PROJECT_OWNER }}" \
48
+ '.data.node.projectItems.nodes[] | select(.project.number == $num and (.project.owner.login | ascii_downcase) == ($owner | ascii_downcase))')
49
+
50
+ ITEM_ID=$(echo "$ITEM_DATA" | jq -r '.id // empty')
51
+ PROJECT_ID=$(echo "$ITEM_DATA" | jq -r '.project.id // empty')
52
+ IS_ARCHIVED=$(echo "$ITEM_DATA" | jq -r '.isArchived // false')
53
+
54
+ echo "Gevonden Item ID: $ITEM_ID"
55
+ echo "Gevonden Project ID: $PROJECT_ID"
56
+ echo "Is gearchiveerd: $IS_ARCHIVED"
57
+
58
+ echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
59
+ echo "project_id=$PROJECT_ID" >> $GITHUB_OUTPUT
60
+ echo "is_archived=$IS_ARCHIVED" >> $GITHUB_OUTPUT
61
+
62
+ - name: Unarchive het projectitem
63
+ if: steps.item_info.outputs.item_id != '' && steps.item_info.outputs.is_archived == 'true'
64
+ run: |
65
+ # We sturen nu verplicht projectId én itemId mee, en vragen 'item { id }' op als return
66
+ gh api graphql -f query='
67
+ mutation($projectId: ID!, $itemId: ID!) {
68
+ unarchiveProjectV2Item(input: {projectId: $projectId, itemId: $itemId}) {
69
+ item { id }
70
+ }
71
+ }
72
+ ' -f projectId="${{ steps.item_info.outputs.project_id }}" -f itemId="${{ steps.item_info.outputs.item_id }}"
73
+
74
+ - name: Zet status op Ready
75
+ if: steps.item_info.outputs.item_id != ''
76
+ run: |
77
+ # We gebruiken nu de URL van het issue in plaats van de --id vlag
78
+ gh project item-edit ${{ env.PROJECT_NUMBER }} \
79
+ --owner "${{ env.PROJECT_OWNER }}" \
80
+ --url "https://github.com/${{ env.PROJECT_OWNER }}/${{ github.event.repository.name }}/issues/${{ github.event.issue.number }}" \
81
+ --field "Status" \
82
+ --value "Ready"
package/README.md CHANGED
@@ -1 +1,96 @@
1
- ## NPM Package for the Chronograph App for Athom's Homey.
1
+ # Chronograph for Homey
2
+
3
+ `chronograph-npm` lets a Homey app access chronographs provided by the [Chronograph app](https://homey.app/a/nl.fellownet.chronograph/). It provides helpers for retrieving chronographs and receiving realtime updates.
4
+
5
+ ## Installation
6
+
7
+ Install the package in your Homey app project:
8
+
9
+ ```sh
10
+ npm install chronograph-npm
11
+ ```
12
+
13
+ Add Chronograph to the permissions in your app's `app.json`:
14
+
15
+ ```json
16
+ {
17
+ "permissions": [
18
+ "homey:app:nl.fellownet.chronograph"
19
+ ]
20
+ }
21
+ ```
22
+
23
+ ## Initialize
24
+
25
+ Import `CG` and initialize it from your app's `onInit` method:
26
+
27
+ ```js
28
+ const { CG } = require('chronograph-npm');
29
+
30
+ class MyApp extends Homey.App {
31
+ async onInit() {
32
+ this.chronograph = await CG.init({ homey: this.homey });
33
+ }
34
+ }
35
+
36
+ module.exports = MyApp;
37
+ ```
38
+
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
+
41
+ ## Available API
42
+
43
+ The package exports the `CG` class:
44
+
45
+ | Method or property | Description |
46
+ | --- | --- |
47
+ | `CG.init({ homey })` | Initializes the integration and returns a `CG` instance. |
48
+ | `CG.getChronographs()` | Returns the current active chronographs as an array. Each snapshot contains `name`, `data`, `duration`, `targetDuration`, and `running`. |
49
+ | `CG.getAllChronographs()` | Returns configured chronograph names grouped by type, such as `timer`, `stopwatch`, and `transition`. |
50
+ | `CG.onChronographUpdate({ chronograph })` | Update hook. Override it to handle realtime updates; the default implementation does nothing. |
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 the installation and API checks and realtime-listener registration succeed. |
53
+ | `instance.isReady` / `CG.isReady` | Indicates whether the integration has become ready. |
54
+
55
+ For example, retrieve active chronographs after initialization:
56
+
57
+ ```js
58
+ const chronographs = await CG.getChronographs();
59
+ for (const chronograph of chronographs) {
60
+ this.log(chronograph.name, chronograph.running, chronograph.duration);
61
+ }
62
+ ```
63
+
64
+ Use `getAllChronographs()` when you need the configured names (including chronographs that are not currently active):
65
+
66
+ ```js
67
+ const configured = await CG.getAllChronographs();
68
+ this.log(configured.timer);
69
+ ```
70
+
71
+ ## Realtime updates
72
+
73
+ Override the update hook before initializing `CG`. The hook is called with `{ chronograph }` for each realtime update. If an update contains a `now` value, the package converts it to a JavaScript `Date`.
74
+
75
+ ```js
76
+ const { CG } = require('chronograph-npm');
77
+
78
+ class MyApp extends Homey.App {
79
+ async onInit() {
80
+ CG.onChronographUpdate = async ({ chronograph }) => {
81
+ this.log('Chronograph updated:', chronograph.name, chronograph.now);
82
+ };
83
+
84
+ this.chronograph = await CG.init({ homey: this.homey });
85
+ }
86
+ }
87
+
88
+ module.exports = MyApp;
89
+ ```
90
+
91
+ ## Requirements
92
+
93
+ - The Chronograph app must be installed on the same Homey.
94
+ - Your app must declare the Chronograph permission in `app.json`.
95
+ - Initialize the package with your app's Homey instance before calling the retrieval methods.
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
@@ -2,19 +2,60 @@ export class CG {
2
2
  static init({ homey }?: {
3
3
  homey: any;
4
4
  }): Promise<CG>;
5
+ /**
6
+ * Removes the realtime and unload listeners. Called automatically when Homey emits unload.
7
+ */
5
8
  static destroy(): void;
6
9
  static getAllChronographs(): Promise<any>;
7
10
  static getChronographs(): Promise<any>;
8
11
  /**
9
- * @description Setable member, when set, will be activated on any variable update.
10
- * @param {Object} param0.variable
12
+ * Handles a realtime update from the Chronograph app. Override this method
13
+ * to receive updated chronograph data.
14
+ * @param {Object} param0 Update payload.
15
+ * @param {Object} param0.chronograph Realtime state snapshot, not a Chronograph class instance.
16
+ * @param {string} [param0.chronograph.id] Generated identifier; omitted from single-event updates.
17
+ * @param {string} param0.chronograph.name Configured chronograph name.
18
+ * @param {number} param0.chronograph.duration Elapsed duration in milliseconds, also exposed as elapsed.
19
+ * @param {number} param0.chronograph.elapsed Elapsed duration in milliseconds.
20
+ * @param {number|null} param0.chronograph.targetDuration Target duration in milliseconds.
21
+ * Stopwatches have no target (NaN before serialization, normally null over the API).
22
+ * @param {number|null} param0.chronograph.remaining Target duration minus elapsed duration, in milliseconds.
23
+ * Not meaningful for stopwatches; may be NaN or null after serialization.
24
+ * @param {boolean} param0.chronograph.running Whether the chronograph is running.
25
+ * @param {boolean} param0.chronograph.removed Whether this update reports removal of the chronograph.
26
+ * @param {Date} param0.chronograph.now Snapshot timestamp, converted to a Date by this package.
27
+ * @param {Object} param0.chronograph.data Chronograph metadata, excluding flowValues; may be incomplete during creation.
28
+ * @param {'Timer'|'Stopwatch'|'Transition'} [param0.chronograph.data.type] Chronograph kind.
29
+ * @param {number} [param0.chronograph.data.from] Transition starting value.
30
+ * @param {number} [param0.chronograph.data.to] Transition ending value.
31
+ * @param {number} [param0.chronograph.data.steps] Transition step count, limited by its duration.
11
32
  */
12
- static onChronographUpdate({ chronograph }: any): Promise<void>;
33
+ static onChronographUpdate({ chronograph }: {
34
+ chronograph: {
35
+ id?: string;
36
+ name: string;
37
+ duration: number;
38
+ elapsed: number;
39
+ targetDuration: number | null;
40
+ remaining: number | null;
41
+ running: boolean;
42
+ removed: boolean;
43
+ now: Date;
44
+ data: {
45
+ type?: 'Timer' | 'Stopwatch' | 'Transition';
46
+ from?: number;
47
+ to?: number;
48
+ steps?: number;
49
+ };
50
+ };
51
+ }): Promise<void>;
13
52
  constructor({ homey }: {
14
53
  homey: any;
15
54
  });
16
55
  homey: any;
17
56
  isReady: boolean;
57
+ _destroyed: boolean;
58
+ _defer: Defer;
18
59
  ready: any;
19
60
  init({}?: {}): Promise<CG>;
20
61
  api: any;
@@ -22,4 +63,25 @@ export class CG {
22
63
  /** @private */
23
64
  private getAppApi;
24
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 {};
25
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;;oBAyBC;IA+BD,uBAQC;IAuCD,0CAEC;IAED,uCAEC;IAKD;;;OAGG;IACH,gEAGC;IA/FD;;OAWC;IATG,WAA6B;IAC7B,iBAAoB;IAIpB,WAAyC;IAM7C,2BAaC;IAVW,SAAqC;IAyBjD,uDAcC;IAGD,eAAe;IACf,kBAeC;CAyBJ"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chronograph-npm",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "The Chronograph App for Athom's Homey",
5
5
  "main": "src/cg.js",
6
6
  "types": "./dist/cg.d.ts",
package/src/cg.js CHANGED
@@ -2,51 +2,36 @@ class CG {
2
2
 
3
3
 
4
4
  static async init({ homey } = {}) {
5
+ if (CG.Current) CG.destroy();
5
6
  let cg = new CG({ homey });
6
7
 
7
- if (!homey.app.__CG_OnUninit) {
8
- homey.app.__CG_OnUninit = homey.app.onUninit;
9
- homey.app.onUninit = async function (args) {
10
-
11
- try {
12
- if (CG) await CG.destroy();
13
- } catch (error) {
14
- console.log(error);
15
- }
16
- try {
17
- if (args) await CG.homey.__CG_OnUninit(...args); else await CG.homey.__CG_OnUninit();
18
- } catch (error) {
19
- let a = error;
20
- }
21
- };
22
- }
23
- try {
24
- await cg.init();
25
- } catch (error) {
26
-
27
- }
8
+ homey.once('unload', CG.destroy);
9
+ await cg.init();
28
10
  return cg;
29
11
  }
30
12
  constructor({ homey }) {
31
13
  CG.Current = this;
32
14
  this.homey = CG.homey = homey;
33
- this.isReady = false;
15
+ CG.isReady = this.isReady = false;
16
+ this._destroyed = false;
34
17
 
35
- CG._defer = new Defer();
18
+ this._defer = CG._defer = new Defer();
36
19
 
37
- this.ready = CG.ready = CG._defer.promise;
38
- CG.ready.then(() => {
39
- CG.isReady = this.isReady = true;
20
+ this.ready = CG.ready = this._defer.promise;
21
+ this.ready.then(() => {
22
+ this.isReady = true;
23
+ if (CG.Current === this) CG.isReady = true;
40
24
  });
41
25
  }
42
26
 
43
27
  async init({ } = {}) {
44
28
  if (!this.api) {
45
29
  try {
46
- this.api = await this.getAppApi(true);
47
- this.api.on('realtime', this.realtimeFunction);
48
- await this.api.get('/chronographs')
49
- if (!CG.isReady && CG._defer) CG._defer.resolve();
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();
50
35
  } catch (error) {
51
36
  this.homey.error('CG.init', error);
52
37
  return this;
@@ -57,9 +42,14 @@ class CG {
57
42
 
58
43
 
59
44
 
45
+ /**
46
+ * Removes the realtime and unload listeners. Called automatically when Homey emits unload.
47
+ */
60
48
  static destroy() {
61
49
  try {
62
50
  if (!CG.Current) return;
51
+ CG.Current._destroyed = true;
52
+ CG.Current.homey.removeListener('unload', CG.destroy);
63
53
  if (CG.Current.api) {
64
54
  CG.Current.api.off('realtime', CG.Current.realtimeFunction);
65
55
  }
@@ -69,8 +59,6 @@ class CG {
69
59
 
70
60
 
71
61
  async realtimeFunction(event, data) {
72
- if (!CG.isReady && CG._defer) CG._defer.resolve();
73
-
74
62
  if (event === 'chronograph_event') {
75
63
  if (data && data.now) data.now = new Date(data.now);
76
64
  CG.onChronographUpdate({ chronograph: data });
@@ -86,21 +74,11 @@ class CG {
86
74
 
87
75
 
88
76
  /** @private */
89
- async getAppApi(onlyApi) {
90
- try {
91
- if (!this.api) this.api = this.homey.api.getApiApp('nl.fellownet.chronograph');
92
- } catch (error) {
93
- throw new Error(error);
94
- }
95
- if (onlyApi === true) return this.api;
96
- if (!this.api) throw new Error('Chronograph is not installed.');
97
- else {
98
- try {
99
- BL._isLoaded = await this.api.getInstalled();
100
- } catch (error) { }
101
- if (!BL._isLoaded) throw new Error('Chronograph is not installed.');
102
- }
103
- 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;
104
82
  }
105
83
 
106
84
 
@@ -116,8 +94,26 @@ class CG {
116
94
 
117
95
 
118
96
  /**
119
- * @description Setable member, when set, will be activated on any variable update.
120
- * @param {Object} param0.variable
97
+ * Handles a realtime update from the Chronograph app. Override this method
98
+ * to receive updated chronograph data.
99
+ * @param {Object} param0 Update payload.
100
+ * @param {Object} param0.chronograph Realtime state snapshot, not a Chronograph class instance.
101
+ * @param {string} [param0.chronograph.id] Generated identifier; omitted from single-event updates.
102
+ * @param {string} param0.chronograph.name Configured chronograph name.
103
+ * @param {number} param0.chronograph.duration Elapsed duration in milliseconds, also exposed as elapsed.
104
+ * @param {number} param0.chronograph.elapsed Elapsed duration in milliseconds.
105
+ * @param {number|null} param0.chronograph.targetDuration Target duration in milliseconds.
106
+ * Stopwatches have no target (NaN before serialization, normally null over the API).
107
+ * @param {number|null} param0.chronograph.remaining Target duration minus elapsed duration, in milliseconds.
108
+ * Not meaningful for stopwatches; may be NaN or null after serialization.
109
+ * @param {boolean} param0.chronograph.running Whether the chronograph is running.
110
+ * @param {boolean} param0.chronograph.removed Whether this update reports removal of the chronograph.
111
+ * @param {Date} param0.chronograph.now Snapshot timestamp, converted to a Date by this package.
112
+ * @param {Object} param0.chronograph.data Chronograph metadata, excluding flowValues; may be incomplete during creation.
113
+ * @param {'Timer'|'Stopwatch'|'Transition'} [param0.chronograph.data.type] Chronograph kind.
114
+ * @param {number} [param0.chronograph.data.from] Transition starting value.
115
+ * @param {number} [param0.chronograph.data.to] Transition ending value.
116
+ * @param {number} [param0.chronograph.data.steps] Transition step count, limited by its duration.
121
117
  */
122
118
  static async onChronographUpdate({ chronograph }) {
123
119
  //BL.homey.log('onChronographUpdate: ', chronograph);
@@ -0,0 +1,167 @@
1
+ const assert = require('node:assert/strict');
2
+ const { EventEmitter } = require('node:events');
3
+ const { test } = require('node:test');
4
+ const { CG } = require('../src/cg');
5
+
6
+ function createHomey() {
7
+ const homey = new EventEmitter();
8
+ const api = new EventEmitter();
9
+ api.getInstalled = async () => true;
10
+ api.get = async () => [];
11
+ homey.api = { getApiApp: () => api };
12
+ homey.app = { onUninit() {} };
13
+ homey.error = (...args) => assert.fail(args.join(' '));
14
+ return { homey, api };
15
+ }
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
+
132
+ test('unload removes the realtime listener without replacing app lifecycle methods', async (t) => {
133
+ const { homey, api } = createHomey();
134
+ const onUninit = homey.app.onUninit;
135
+ t.after(() => CG.destroy());
136
+ await CG.init({ homey });
137
+
138
+ assert.equal(homey.app.onUninit, onUninit);
139
+ assert.equal(api.listenerCount('realtime'), 1);
140
+ assert.equal(homey.listenerCount('unload'), 1);
141
+
142
+ homey.emit('unload');
143
+
144
+ assert.equal(api.listenerCount('realtime'), 0);
145
+ assert.equal(homey.listenerCount('unload'), 0);
146
+ });
147
+
148
+ test('reinitialization and manual cleanup do not leave listeners behind', async (t) => {
149
+ const first = createHomey();
150
+ const second = createHomey();
151
+ t.after(() => CG.destroy());
152
+ await CG.init({ homey: first.homey });
153
+ await CG.init({ homey: second.homey });
154
+
155
+ assert.equal(first.api.listenerCount('realtime'), 0);
156
+ assert.equal(first.homey.listenerCount('unload'), 0);
157
+ assert.equal(second.api.listenerCount('realtime'), 1);
158
+ assert.equal(second.homey.listenerCount('unload'), 1);
159
+
160
+ first.homey.emit('unload');
161
+ assert.equal(second.api.listenerCount('realtime'), 1);
162
+
163
+ CG.destroy();
164
+ CG.destroy();
165
+ assert.equal(second.api.listenerCount('realtime'), 0);
166
+ assert.equal(second.homey.listenerCount('unload'), 0);
167
+ });