@pilotiq/pilotiq 0.17.0 → 0.18.0
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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +24 -0
- package/dist/react/CollabRoomContext.d.ts +27 -0
- package/dist/react/CollabRoomContext.d.ts.map +1 -1
- package/dist/react/CollabRoomContext.js +30 -0
- package/dist/react/CollabRoomContext.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/react/CollabRoomContext.ts +44 -0
- package/src/react/index.ts +2 -0
- package/src/react/onProviderSynced.test.ts +90 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
> @pilotiq/pilotiq@0.
|
|
2
|
+
> @pilotiq/pilotiq@0.18.0 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
|
|
3
3
|
> tsc -p tsconfig.build.json && pnpm run copy-assets
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @pilotiq/pilotiq@0.
|
|
6
|
+
> @pilotiq/pilotiq@0.18.0 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
|
|
7
7
|
> mkdir -p dist/styles && cp -R src/styles/*.css dist/styles/
|
|
8
8
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @pilotiq/pilotiq
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1b8c1bc: feat(pilotiq): extract `onProviderSynced(provider, fn)` helper for the seed-on-synced collab lifecycle pattern
|
|
8
|
+
|
|
9
|
+
Adapter packages that bind to a collab room (Tiptap-backed editors, the CodeMirror collab adapter) all need the same choreography on mount: if the provider's already streamed in the initial room state, run the seed callback now; otherwise register `provider.once('synced', fn)` and clean up via `provider.off?.('synced', fn)`. That gate was implemented separately in 4 renderers (`CollabTextRenderer`, `MarkdownEditor`, `TiptapEditor` in `@pilotiq/tiptap`; `CollabCodeMirrorEditor` in `@pilotiq/codemirror`).
|
|
10
|
+
|
|
11
|
+
This change extracts the pattern into a single helper in `@pilotiq/pilotiq/react` so future bug fixes in the gate logic (StrictMode double-fire, missing-off-method providers, etc.) fix in one place and so adapters from outside this monorepo can adopt the same pattern with one import.
|
|
12
|
+
|
|
13
|
+
**New public surface on `@pilotiq/pilotiq/react`:**
|
|
14
|
+
|
|
15
|
+
- `onProviderSynced(provider, fn): () => void` — runs `fn` synchronously if `provider.synced`, otherwise registers `provider.once('synced', fn)`. Returns a cleanup that safely unregisters via `try { provider.off?.('synced', fn) } catch {}`. Null/undefined provider returns a no-op cleanup.
|
|
16
|
+
- `SyncedProviderLike` — structural type with `synced?: boolean`, `once?(event: 'synced', fn): void`, `off?(event: 'synced', fn): void`. No yjs / y-websocket peer dep — callers cast their concrete provider via `provider as SyncedProviderLike`.
|
|
17
|
+
|
|
18
|
+
**Adapter package changes (patch-grade):**
|
|
19
|
+
|
|
20
|
+
- `@pilotiq/tiptap`: `CollabTextRenderer`, `MarkdownEditor`, and `TiptapEditor` each replace their ~10-line gate block with `return onProviderSynced(provider, trySeed)` (still inside the existing `useEffect`).
|
|
21
|
+
- `@pilotiq/codemirror`: `CollabCodeMirrorEditor` stores the cleanup and invokes it alongside `view.destroy()` inside the mount effect's combined cleanup.
|
|
22
|
+
|
|
23
|
+
Behavior is unchanged — no double-fire risk, no missed-cleanup risk, no API changes for callers of any of the affected renderers.
|
|
24
|
+
|
|
25
|
+
Test coverage: 6 new unit tests in `packages/pilotiq/src/react/onProviderSynced.test.ts` cover synced-now, defer-until-synced, cleanup-before-synced, null provider, off-throws, and provider-missing-once/off.
|
|
26
|
+
|
|
3
27
|
## 0.17.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
|
@@ -34,4 +34,31 @@ export interface CollabRoom {
|
|
|
34
34
|
export declare const CollabRoomContext: import("react").Context<CollabRoom | null>;
|
|
35
35
|
/** Read the active collab room for the surrounding record, or `null`. */
|
|
36
36
|
export declare function useCollabRoom(): CollabRoom | null;
|
|
37
|
+
/**
|
|
38
|
+
* Minimal structural shape every collab provider exposes for the
|
|
39
|
+
* "initial room state has streamed in" signal. Kept structural so callers
|
|
40
|
+
* (`@pilotiq/tiptap`, `@pilotiq/codemirror`, future adapters) can pass
|
|
41
|
+
* `provider as unknown as SyncedProviderLike` without taking a hard peer
|
|
42
|
+
* dep on yjs / y-websocket / y-webrtc.
|
|
43
|
+
*/
|
|
44
|
+
export interface SyncedProviderLike {
|
|
45
|
+
synced?: boolean;
|
|
46
|
+
once?(event: 'synced', fn: () => void): void;
|
|
47
|
+
off?(event: 'synced', fn: () => void): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run `fn` once the collab provider's initial room state has streamed in.
|
|
51
|
+
* If the provider is already synced, `fn` fires synchronously; otherwise
|
|
52
|
+
* it's registered via `provider.once('synced', fn)`. The returned cleanup
|
|
53
|
+
* unregisters the once handler safely (idempotent + try/catch) so callers
|
|
54
|
+
* can wire it directly into a React effect's cleanup return.
|
|
55
|
+
*
|
|
56
|
+
* Useful for the brand-new-record seed pattern: editors mounting against
|
|
57
|
+
* a freshly-created record want to push the SSR-rendered default into
|
|
58
|
+
* the empty `Y.Text` / `Y.XmlFragment` exactly once after sync, before
|
|
59
|
+
* the user types. Race caveat: two peers simultaneously mounting against
|
|
60
|
+
* a brand-new record can both see `length === 0` and both seed —
|
|
61
|
+
* accepted today across every adapter's seed path.
|
|
62
|
+
*/
|
|
63
|
+
export declare function onProviderSynced(provider: SyncedProviderLike | null | undefined, fn: () => void): () => void;
|
|
37
64
|
//# sourceMappingURL=CollabRoomContext.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollabRoomContext.d.ts","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,IAAI,EAAO,OAAO,CAAA;IAClB,4DAA4D;IAC5D,QAAQ,EAAG,OAAO,CAAA;IAClB,+EAA+E;IAC/E,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAG,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,4CAAyC,CAAA;AAEvE,yEAAyE;AACzE,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
|
|
1
|
+
{"version":3,"file":"CollabRoomContext.d.ts","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,IAAI,EAAO,OAAO,CAAA;IAClB,4DAA4D;IAC5D,QAAQ,EAAG,OAAO,CAAA;IAClB,+EAA+E;IAC/E,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAG,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,4CAAyC,CAAA;AAEvE,yEAAyE;AACzE,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC5C,GAAG,CAAC,CAAC,KAAK,EAAG,QAAQ,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAC7C;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,EAC/C,EAAE,EAAQ,MAAM,IAAI,GACnB,MAAM,IAAI,CAUZ"}
|
|
@@ -9,4 +9,34 @@ export const CollabRoomContext = createContext(null);
|
|
|
9
9
|
export function useCollabRoom() {
|
|
10
10
|
return useContext(CollabRoomContext);
|
|
11
11
|
}
|
|
12
|
+
const NOOP_CLEANUP = () => { };
|
|
13
|
+
/**
|
|
14
|
+
* Run `fn` once the collab provider's initial room state has streamed in.
|
|
15
|
+
* If the provider is already synced, `fn` fires synchronously; otherwise
|
|
16
|
+
* it's registered via `provider.once('synced', fn)`. The returned cleanup
|
|
17
|
+
* unregisters the once handler safely (idempotent + try/catch) so callers
|
|
18
|
+
* can wire it directly into a React effect's cleanup return.
|
|
19
|
+
*
|
|
20
|
+
* Useful for the brand-new-record seed pattern: editors mounting against
|
|
21
|
+
* a freshly-created record want to push the SSR-rendered default into
|
|
22
|
+
* the empty `Y.Text` / `Y.XmlFragment` exactly once after sync, before
|
|
23
|
+
* the user types. Race caveat: two peers simultaneously mounting against
|
|
24
|
+
* a brand-new record can both see `length === 0` and both seed —
|
|
25
|
+
* accepted today across every adapter's seed path.
|
|
26
|
+
*/
|
|
27
|
+
export function onProviderSynced(provider, fn) {
|
|
28
|
+
if (!provider)
|
|
29
|
+
return NOOP_CLEANUP;
|
|
30
|
+
if (provider.synced) {
|
|
31
|
+
fn();
|
|
32
|
+
return NOOP_CLEANUP;
|
|
33
|
+
}
|
|
34
|
+
provider.once?.('synced', fn);
|
|
35
|
+
return () => {
|
|
36
|
+
try {
|
|
37
|
+
provider.off?.('synced', fn);
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore */ }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
12
42
|
//# sourceMappingURL=CollabRoomContext.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollabRoomContext.js","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AA+BjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAoB,IAAI,CAAC,CAAA;AAEvE,yEAAyE;AACzE,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAA;AACtC,CAAC"}
|
|
1
|
+
{"version":3,"file":"CollabRoomContext.js","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AA+BjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAoB,IAAI,CAAC,CAAA;AAEvE,yEAAyE;AACzE,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAA;AACtC,CAAC;AAeD,MAAM,YAAY,GAAG,GAAS,EAAE,GAAE,CAAC,CAAA;AAEnC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA+C,EAC/C,EAAoB;IAEpB,IAAI,CAAC,QAAQ;QAAE,OAAO,YAAY,CAAA;IAClC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,EAAE,EAAE,CAAA;QACJ,OAAO,YAAY,CAAA;IACrB,CAAC;IACD,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC7B,OAAO,GAAG,EAAE;QACV,IAAI,CAAC;YAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAC7D,CAAC,CAAA;AACH,CAAC"}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { registerFieldLabelSlot, getFieldLabelSlot, type FieldLabelSlotProps } f
|
|
|
6
6
|
export { registerPendingSuggestionOverlay, getPendingSuggestionOverlay, type PendingSuggestionOverlayProps, } from './PendingSuggestionOverlayRegistry.js';
|
|
7
7
|
export { PendingSuggestionsContext, usePendingSuggestions, usePendingSuggestionsForField, type PendingSuggestion, type PendingSuggestionOrigin, type PendingSuggestionsApi, } from './PendingSuggestionsContext.js';
|
|
8
8
|
export { registerPendingSuggestionApplier, getPendingSuggestionApplier, type PendingSuggestionApplier, } from './PendingSuggestionApplierRegistry.js';
|
|
9
|
-
export { CollabRoomContext, useCollabRoom, type CollabRoom, } from './CollabRoomContext.js';
|
|
9
|
+
export { CollabRoomContext, useCollabRoom, onProviderSynced, type CollabRoom, type SyncedProviderLike, } from './CollabRoomContext.js';
|
|
10
10
|
export { registerCollabExtensions, getCollabExtensions, type CollabExtensionFactory, type CollabExtensionFactoryArgs, } from './CollabExtensionFactoryRegistry.js';
|
|
11
11
|
export { registerCollabTextRenderer, getCollabTextRenderer, type CollabTextRenderer, type CollabTextRendererProps, } from './CollabTextRendererRegistry.js';
|
|
12
12
|
export { registerMarkdownEditor, getMarkdownEditor, type MarkdownEditor, type MarkdownEditorProps, } from './MarkdownEditorRegistry.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,UAAU,EACV,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,EACxB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjH,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,KAAK,6BAA6B,GACnC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,6BAA6B,EAC7B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,GAC3B,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,KAAK,wBAAwB,GAC9B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { registerFieldLabelSlot, getFieldLabelSlot } from './FieldLabelSlotRegis
|
|
|
6
6
|
export { registerPendingSuggestionOverlay, getPendingSuggestionOverlay, } from './PendingSuggestionOverlayRegistry.js';
|
|
7
7
|
export { PendingSuggestionsContext, usePendingSuggestions, usePendingSuggestionsForField, } from './PendingSuggestionsContext.js';
|
|
8
8
|
export { registerPendingSuggestionApplier, getPendingSuggestionApplier, } from './PendingSuggestionApplierRegistry.js';
|
|
9
|
-
export { CollabRoomContext, useCollabRoom, } from './CollabRoomContext.js';
|
|
9
|
+
export { CollabRoomContext, useCollabRoom, onProviderSynced, } from './CollabRoomContext.js';
|
|
10
10
|
export { registerCollabExtensions, getCollabExtensions, } from './CollabExtensionFactoryRegistry.js';
|
|
11
11
|
export { registerCollabTextRenderer, getCollabTextRenderer, } from './CollabTextRendererRegistry.js';
|
|
12
12
|
export { registerMarkdownEditor, getMarkdownEditor, } from './MarkdownEditorRegistry.js';
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,eAAe,CAAA;AAC5D,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,UAAU,GAEX,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,cAAc,EAEd,UAAU,GAEX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAA2B,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAA4B,MAAM,6BAA6B,CAAA;AACjH,OAAO,EACL,gCAAgC,EAChC,2BAA2B,GAE5B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,6BAA6B,GAI9B,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,gCAAgC,EAChC,2BAA2B,GAE5B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,iBAAiB,EACjB,aAAa,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -40,3 +40,47 @@ export const CollabRoomContext = createContext<CollabRoom | null>(null)
|
|
|
40
40
|
export function useCollabRoom(): CollabRoom | null {
|
|
41
41
|
return useContext(CollabRoomContext)
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Minimal structural shape every collab provider exposes for the
|
|
46
|
+
* "initial room state has streamed in" signal. Kept structural so callers
|
|
47
|
+
* (`@pilotiq/tiptap`, `@pilotiq/codemirror`, future adapters) can pass
|
|
48
|
+
* `provider as unknown as SyncedProviderLike` without taking a hard peer
|
|
49
|
+
* dep on yjs / y-websocket / y-webrtc.
|
|
50
|
+
*/
|
|
51
|
+
export interface SyncedProviderLike {
|
|
52
|
+
synced?: boolean
|
|
53
|
+
once?(event: 'synced', fn: () => void): void
|
|
54
|
+
off?(event: 'synced', fn: () => void): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NOOP_CLEANUP = (): void => {}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run `fn` once the collab provider's initial room state has streamed in.
|
|
61
|
+
* If the provider is already synced, `fn` fires synchronously; otherwise
|
|
62
|
+
* it's registered via `provider.once('synced', fn)`. The returned cleanup
|
|
63
|
+
* unregisters the once handler safely (idempotent + try/catch) so callers
|
|
64
|
+
* can wire it directly into a React effect's cleanup return.
|
|
65
|
+
*
|
|
66
|
+
* Useful for the brand-new-record seed pattern: editors mounting against
|
|
67
|
+
* a freshly-created record want to push the SSR-rendered default into
|
|
68
|
+
* the empty `Y.Text` / `Y.XmlFragment` exactly once after sync, before
|
|
69
|
+
* the user types. Race caveat: two peers simultaneously mounting against
|
|
70
|
+
* a brand-new record can both see `length === 0` and both seed —
|
|
71
|
+
* accepted today across every adapter's seed path.
|
|
72
|
+
*/
|
|
73
|
+
export function onProviderSynced(
|
|
74
|
+
provider: SyncedProviderLike | null | undefined,
|
|
75
|
+
fn: () => void,
|
|
76
|
+
): () => void {
|
|
77
|
+
if (!provider) return NOOP_CLEANUP
|
|
78
|
+
if (provider.synced) {
|
|
79
|
+
fn()
|
|
80
|
+
return NOOP_CLEANUP
|
|
81
|
+
}
|
|
82
|
+
provider.once?.('synced', fn)
|
|
83
|
+
return () => {
|
|
84
|
+
try { provider.off?.('synced', fn) } catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { onProviderSynced, type SyncedProviderLike } from './CollabRoomContext.js'
|
|
5
|
+
|
|
6
|
+
/** Bare provider double — captures `once` registrations and supports
|
|
7
|
+
* flipping `synced` so we can assert the deferred path. */
|
|
8
|
+
function mockProvider(initialSynced = false): SyncedProviderLike & {
|
|
9
|
+
fire(): void
|
|
10
|
+
registered: number
|
|
11
|
+
unregistered: number
|
|
12
|
+
offThrows: boolean
|
|
13
|
+
} {
|
|
14
|
+
let synced = initialSynced
|
|
15
|
+
let pending: (() => void) | null = null
|
|
16
|
+
const inst = {
|
|
17
|
+
get synced() { return synced },
|
|
18
|
+
set synced(v: boolean) { synced = v },
|
|
19
|
+
once(event: 'synced', fn: () => void) {
|
|
20
|
+
assert.equal(event, 'synced', 'only synced is registered')
|
|
21
|
+
pending = fn
|
|
22
|
+
inst.registered++
|
|
23
|
+
},
|
|
24
|
+
off(event: 'synced', fn: () => void) {
|
|
25
|
+
assert.equal(event, 'synced', 'only synced is unregistered')
|
|
26
|
+
if (inst.offThrows) throw new Error('boom')
|
|
27
|
+
if (pending === fn) pending = null
|
|
28
|
+
inst.unregistered++
|
|
29
|
+
},
|
|
30
|
+
fire() { if (pending) { const f = pending; pending = null; f() } },
|
|
31
|
+
registered: 0,
|
|
32
|
+
unregistered: 0,
|
|
33
|
+
offThrows: false,
|
|
34
|
+
}
|
|
35
|
+
return inst
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('onProviderSynced', () => {
|
|
39
|
+
it('fires fn synchronously when provider is already synced', () => {
|
|
40
|
+
const provider = mockProvider(true)
|
|
41
|
+
let calls = 0
|
|
42
|
+
const cleanup = onProviderSynced(provider, () => { calls++ })
|
|
43
|
+
assert.equal(calls, 1, 'fn ran synchronously')
|
|
44
|
+
assert.equal(provider.registered, 0, 'once never registered')
|
|
45
|
+
cleanup()
|
|
46
|
+
assert.equal(provider.unregistered, 0, 'no-op cleanup did not call off')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('defers fn until synced event when provider not yet synced', () => {
|
|
50
|
+
const provider = mockProvider(false)
|
|
51
|
+
let calls = 0
|
|
52
|
+
const cleanup = onProviderSynced(provider, () => { calls++ })
|
|
53
|
+
assert.equal(calls, 0, 'fn not yet called')
|
|
54
|
+
assert.equal(provider.registered, 1, 'once was registered')
|
|
55
|
+
provider.fire()
|
|
56
|
+
assert.equal(calls, 1, 'fn ran after synced fired')
|
|
57
|
+
cleanup()
|
|
58
|
+
assert.equal(provider.unregistered, 1, 'cleanup called off')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('cleanup unregisters the once handler when called before synced fires', () => {
|
|
62
|
+
const provider = mockProvider(false)
|
|
63
|
+
let calls = 0
|
|
64
|
+
const cleanup = onProviderSynced(provider, () => { calls++ })
|
|
65
|
+
cleanup()
|
|
66
|
+
assert.equal(provider.unregistered, 1, 'off was called')
|
|
67
|
+
provider.fire()
|
|
68
|
+
assert.equal(calls, 0, 'fn never fired after cleanup')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns a no-op cleanup when provider is null or undefined', () => {
|
|
72
|
+
const noopCleanup = onProviderSynced(null, () => { throw new Error('unreachable') })
|
|
73
|
+
assert.doesNotThrow(() => noopCleanup())
|
|
74
|
+
const noopCleanup2 = onProviderSynced(undefined, () => { throw new Error('unreachable') })
|
|
75
|
+
assert.doesNotThrow(() => noopCleanup2())
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('swallows errors thrown by provider.off so cleanup never breaks an effect', () => {
|
|
79
|
+
const provider = mockProvider(false)
|
|
80
|
+
provider.offThrows = true
|
|
81
|
+
const cleanup = onProviderSynced(provider, () => {})
|
|
82
|
+
assert.doesNotThrow(cleanup, 'cleanup did not propagate the throw')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('gracefully handles a provider missing once/off methods', () => {
|
|
86
|
+
const sparse: SyncedProviderLike = { synced: false }
|
|
87
|
+
const cleanup = onProviderSynced(sparse, () => {})
|
|
88
|
+
assert.doesNotThrow(cleanup, 'cleanup is safe when off is undefined')
|
|
89
|
+
})
|
|
90
|
+
})
|