@nimbalyst/extension-sdk 0.3.0 → 0.5.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/__tests__/buildEmitStability.test.d.ts +2 -0
  3. package/dist/__tests__/buildEmitStability.test.d.ts.map +1 -0
  4. package/dist/__tests__/buildEmitStability.test.js +33 -0
  5. package/dist/__tests__/createTextCollabContentAdapter.test.js +1 -0
  6. package/dist/__tests__/editorHostCapabilities.test.d.ts +2 -0
  7. package/dist/__tests__/editorHostCapabilities.test.d.ts.map +1 -0
  8. package/dist/__tests__/editorHostCapabilities.test.js +24 -0
  9. package/dist/__tests__/fileDirectoryTree.test.js +1 -0
  10. package/dist/__tests__/fileMask.test.d.ts +2 -0
  11. package/dist/__tests__/fileMask.test.d.ts.map +1 -0
  12. package/dist/__tests__/fileMask.test.js +33 -0
  13. package/dist/__tests__/manifestValidation.agentProviders.test.js +1 -0
  14. package/dist/__tests__/textReplacement.test.d.ts +2 -0
  15. package/dist/__tests__/textReplacement.test.d.ts.map +1 -0
  16. package/dist/__tests__/textReplacement.test.js +31 -0
  17. package/dist/__tests__/useCollaborativeEditor.offline.test.d.ts +2 -0
  18. package/dist/__tests__/useCollaborativeEditor.offline.test.d.ts.map +1 -0
  19. package/dist/__tests__/useCollaborativeEditor.offline.test.js +76 -0
  20. package/dist/__tests__/useEditorLifecycle.test.d.ts +2 -0
  21. package/dist/__tests__/useEditorLifecycle.test.d.ts.map +1 -0
  22. package/dist/__tests__/useEditorLifecycle.test.js +60 -0
  23. package/dist/agents/AgentProtocol.d.ts +12 -0
  24. package/dist/agents/AgentProtocol.d.ts.map +1 -1
  25. package/dist/collab/index.d.ts +14 -0
  26. package/dist/collab/index.d.ts.map +1 -0
  27. package/dist/collab/index.js +13 -0
  28. package/dist/collab/origins.d.ts +15 -0
  29. package/dist/collab/origins.d.ts.map +1 -0
  30. package/dist/collab/origins.js +14 -0
  31. package/dist/collab/textReplacement.d.ts +7 -0
  32. package/dist/collab/textReplacement.d.ts.map +1 -0
  33. package/dist/collab/textReplacement.js +27 -0
  34. package/dist/editorHostCapabilities.d.ts +31 -0
  35. package/dist/editorHostCapabilities.d.ts.map +1 -0
  36. package/dist/editorHostCapabilities.js +35 -0
  37. package/dist/electronMain.d.ts +12 -0
  38. package/dist/electronMain.d.ts.map +1 -0
  39. package/dist/electronMain.js +11 -0
  40. package/dist/fileMask.d.ts +17 -0
  41. package/dist/fileMask.d.ts.map +1 -0
  42. package/dist/fileMask.js +45 -0
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +2 -0
  46. package/dist/types/editor.d.ts +126 -2
  47. package/dist/types/editor.d.ts.map +1 -1
  48. package/dist/types/extension.d.ts +50 -5
  49. package/dist/types/extension.d.ts.map +1 -1
  50. package/dist/types/panel.d.ts +43 -0
  51. package/dist/types/panel.d.ts.map +1 -1
  52. package/dist/useCollaborativeEditor.d.ts +22 -29
  53. package/dist/useCollaborativeEditor.d.ts.map +1 -1
  54. package/dist/useCollaborativeEditor.js +105 -95
  55. package/dist/useEditorLifecycle.d.ts.map +1 -1
  56. package/dist/useEditorLifecycle.js +2 -0
  57. package/package.json +12 -3
@@ -29,29 +29,16 @@
29
29
  * }
30
30
  * ```
31
31
  *
32
- * Bootstrap-race safety: if two clients both open an empty document, both
33
- * will call `initializeFromContent` and their CRDT updates merge. To avoid
34
- * duplicate elements your seeded shared types MUST use **content-derived
35
- * stable IDs** (e.g. element `id` from the file), not random IDs. The same
36
- * input yields the same Y.Doc state; merged duplicates collapse.
32
+ * Bootstrap-race safety: two clients seeding the same file must assign the
33
+ * same keys, so seed identity must be deterministic from stable file data or
34
+ * order, not random. In a keyed structured document, assign each key once and
35
+ * never recompute it from mutable content; changing it replaces the shared
36
+ * type and loses its history and concurrent edits.
37
37
  */
38
38
  import * as Y from 'yjs';
39
39
  import type { CollaborationStatus, CollaboratorInfo, EditorHost } from './types/editor.js';
40
40
  import type { CollabCodec } from './types/collab.js';
41
- /**
42
- * Origin tag used when the SDK wraps `initializeFromContent` in a Y.Doc
43
- * transaction. Extension bindings can compare a transaction's origin
44
- * against this to suppress their own change handlers during seeding
45
- * (otherwise the binding would echo the seed back into local-edit state).
46
- *
47
- * ```ts
48
- * yDoc.on('update', (update, origin) => {
49
- * if (origin === COLLAB_INIT_ORIGIN) return;
50
- * // ... apply remote change
51
- * });
52
- * ```
53
- */
54
- export declare const COLLAB_INIT_ORIGIN: unique symbol;
41
+ export { COLLAB_INIT_ORIGIN } from './collab/origins.js';
55
42
  /** Context passed to the binding factory (`bind` / `createBinding`). */
56
43
  export interface CollabBindContext {
57
44
  yDoc: Y.Doc;
@@ -62,11 +49,7 @@ export interface CollabBindContext {
62
49
  color: string;
63
50
  };
64
51
  }
65
- export type CollabBindResult = {
66
- destroy: () => void;
67
- } | Promise<{
68
- destroy: () => void;
69
- }>;
52
+ export type CollabBindResult = CollaborativeBindingHandle | Promise<CollaborativeBindingHandle>;
70
53
  /**
71
54
  * Preferred config: pass the SAME pure {@link CollabCodec} the extension
72
55
  * registered via `context.services.collab.registerContentAdapter(...)`, plus
@@ -119,8 +102,9 @@ export interface UseCollaborativeEditorLegacyConfig {
119
102
  /**
120
103
  * @deprecated Provide `seedFromFile` on the codec. Populate the Y.Doc from
121
104
  * raw file content when this client is first. Called inside a
122
- * `yDoc.transact(..., COLLAB_INIT_ORIGIN)`. Use content-derived stable IDs
123
- * (see file-level docs) to keep the bootstrap race deterministic.
105
+ * `yDoc.transact(..., COLLAB_INIT_ORIGIN)`. Seed keys deterministically from
106
+ * the file so concurrent seeds converge. In keyed structured documents,
107
+ * never recompute an assigned key from mutable content.
124
108
  */
125
109
  initializeFromContent(yDoc: Y.Doc, content: string | ArrayBuffer): void;
126
110
  onSeedOutcome?(outcome: {
@@ -129,6 +113,17 @@ export interface UseCollaborativeEditorLegacyConfig {
129
113
  }): void;
130
114
  }
131
115
  export type UseCollaborativeEditorConfig = UseCollaborativeEditorCodecConfig | UseCollaborativeEditorLegacyConfig;
116
+ /**
117
+ * What a binding factory returns. `syncNow` is optional and opt-in: a binding
118
+ * that debounces its local pushes should expose it so the hook can register it
119
+ * as the host's pending-content drain (see
120
+ * `CollaborationContext.registerContentFlush`). Bindings that write through to
121
+ * the Y.Doc synchronously can omit it.
122
+ */
123
+ export interface CollaborativeBindingHandle {
124
+ destroy: () => void;
125
+ syncNow?: () => void | Promise<void>;
126
+ }
132
127
  export interface UseCollaborativeEditorResult {
133
128
  /** True when `host.collaboration` is defined. */
134
129
  isCollaborative: boolean;
@@ -143,9 +138,7 @@ export interface UseCollaborativeEditorResult {
143
138
  * The binding handle once the binding factory has run, or `null` until
144
139
  * collaboration is ready / when not collab.
145
140
  */
146
- binding: {
147
- destroy: () => void;
148
- } | null;
141
+ binding: CollaborativeBindingHandle | null;
149
142
  /**
150
143
  * Non-null when the first-open seed failed or its flush was not confirmed by
151
144
  * the server. Hosts read this to surface the failure (pending-seed toast)
@@ -1 +1 @@
1
- {"version":3,"file":"useCollaborativeEditor.d.ts","sourceRoot":"","sources":["../src/useCollaborativeEditor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAEV,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AAE9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,eAAkC,CAAC;AAElE,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC;IACZ,SAAS,EAAE,OAAO,uBAAuB,EAAE,SAAS,CAAC;IACrD,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iCAAiC;IAChD,kFAAkF;IAClF,KAAK,EAAE,WAAW,CAAC;IAEnB;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAE/C;;;;OAIG;IACH,aAAa,CAAC,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,kCAAkC;IACjD,kDAAkD;IAClD,aAAa,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAExD;;;OAGG;IACH,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,qBAAqB,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAExE,aAAa,CAAC,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,MAAM,4BAA4B,GACpC,iCAAiC,GACjC,kCAAkC,CAAC;AAEvC,MAAM,WAAW,4BAA4B;IAC3C,iDAAiD;IACjD,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,MAAM,EAAE,mBAAmB,CAAC;IAC5B;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C;;;OAGG;IACH,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACxC;;;;OAIG;IACH,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3B;AAyCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,GAC5D,OAAO,CAIT;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,4BAA4B,GACnC,4BAA4B,CAgL9B"}
1
+ {"version":3,"file":"useCollaborativeEditor.d.ts","sourceRoot":"","sources":["../src/useCollaborativeEditor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAEV,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC;IACZ,SAAS,EAAE,OAAO,uBAAuB,EAAE,SAAS,CAAC;IACrD,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,MAAM,gBAAgB,GACxB,0BAA0B,GAC1B,OAAO,CAAC,0BAA0B,CAAC,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iCAAiC;IAChD,kFAAkF;IAClF,KAAK,EAAE,WAAW,CAAC;IAEnB;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAE/C;;;;OAIG;IACH,aAAa,CAAC,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,kCAAkC;IACjD,kDAAkD;IAClD,aAAa,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAExD;;;OAGG;IACH,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC;IAE/B;;;;;;OAMG;IACH,qBAAqB,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAExE,aAAa,CAAC,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,MAAM,4BAA4B,GACpC,iCAAiC,GACjC,kCAAkC,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,4BAA4B;IAC3C,iDAAiD;IACjD,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,MAAM,EAAE,mBAAmB,CAAC;IAC5B;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C;;;OAGG;IACH,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAAC;IAC3C;;;;OAIG;IACH,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3B;AAyCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,GAC5D,OAAO,CAIT;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,4BAA4B,GACnC,4BAA4B,CAoM9B"}
@@ -29,28 +29,16 @@
29
29
  * }
30
30
  * ```
31
31
  *
32
- * Bootstrap-race safety: if two clients both open an empty document, both
33
- * will call `initializeFromContent` and their CRDT updates merge. To avoid
34
- * duplicate elements your seeded shared types MUST use **content-derived
35
- * stable IDs** (e.g. element `id` from the file), not random IDs. The same
36
- * input yields the same Y.Doc state; merged duplicates collapse.
32
+ * Bootstrap-race safety: two clients seeding the same file must assign the
33
+ * same keys, so seed identity must be deterministic from stable file data or
34
+ * order, not random. In a keyed structured document, assign each key once and
35
+ * never recompute it from mutable content; changing it replaces the shared
36
+ * type and loses its history and concurrent edits.
37
37
  */
38
38
  import { useEffect, useRef, useState } from 'react';
39
39
  import * as Y from 'yjs';
40
- /**
41
- * Origin tag used when the SDK wraps `initializeFromContent` in a Y.Doc
42
- * transaction. Extension bindings can compare a transaction's origin
43
- * against this to suppress their own change handlers during seeding
44
- * (otherwise the binding would echo the seed back into local-edit state).
45
- *
46
- * ```ts
47
- * yDoc.on('update', (update, origin) => {
48
- * if (origin === COLLAB_INIT_ORIGIN) return;
49
- * // ... apply remote change
50
- * });
51
- * ```
52
- */
53
- export const COLLAB_INIT_ORIGIN = Symbol('nimbalyst:collab-init');
40
+ import { COLLAB_INIT_ORIGIN } from './collab/origins.js';
41
+ export { COLLAB_INIT_ORIGIN } from './collab/origins.js';
54
42
  function defaultIsEmpty(yDoc) {
55
43
  // A fully empty Y.Doc encodes to ~2 bytes (header only).
56
44
  return Y.encodeStateAsUpdate(yDoc).byteLength <= 2;
@@ -151,92 +139,112 @@ export function useCollaborativeEditor(host, config) {
151
139
  return;
152
140
  let cancelled = false;
153
141
  let handle = null;
142
+ let starting = false;
143
+ let unregisterContentFlush = null;
154
144
  const tryStart = async () => {
155
- if (cancelled || handle)
156
- return;
157
- if (collab.getStatus() !== 'connected')
145
+ if (cancelled || handle || starting)
158
146
  return;
159
147
  const cfg = resolveConfig(configRef.current);
160
- // NEVER seed when the transport skipped payloads it could not decode:
161
- // the Y.Doc looking empty then means "content exists but is unreadable
162
- // on this client", and seeding would write a default document over the
163
- // real content for every client (the "Untitled map" clobber).
164
- const undecoded = collab.hasUndecodedContent?.() === true;
165
- if (!undecoded && cfg.isEmpty(collab.yDoc)) {
166
- try {
167
- const content = await collab.loadInitialContent();
168
- if (cancelled)
169
- return;
170
- // NEVER seed from empty content. A host that has no bytes for this
171
- // document (reopen of an already-shared doc: no in-memory share
172
- // payload, no file) returns ''/empty -- seeding from that writes a
173
- // default document into the shared room and clobbers whatever the
174
- // room's real content is for every client. Fall through to bind:
175
- // the room content (or a teammate's seed) will populate the doc.
176
- if (!hasSeedableContent(content)) {
177
- console.warn('[useCollaborativeEditor] Skipping first-open seed: host returned empty initial content.');
178
- }
179
- // Re-check emptiness in case another client seeded while we were
180
- // awaiting -- they would have raced through the WebSocket and
181
- // applied their update during our await gap. Avoid double-seeding
182
- // in that case; CRDT merge would otherwise insert duplicate
183
- // content unless the seed is fully deterministic.
184
- else if (cfg.isEmpty(collab.yDoc)) {
185
- collab.yDoc.transact(() => {
186
- cfg.seed(collab.yDoc, content);
187
- }, COLLAB_INIT_ORIGIN);
188
- // Durability: the seed the user sees locally must reach the server
189
- // before this provider can tear down. flushWithAck resolves only
190
- // after a server-persisted ack; flushLocalState is the deprecated
191
- // fire-and-forget fallback for older hosts.
192
- const flushed = collab.flushWithAck
193
- ? await collab.flushWithAck()
194
- : (await collab.flushLocalState?.(), true);
195
- if (!flushed) {
196
- const err = new Error('Seed flush was not confirmed by the server before timeout; content may not have persisted.');
197
- console.warn('[useCollaborativeEditor]', err.message);
198
- setSeedError(err);
199
- collab.reportSeedOutcome?.({ ok: false, error: err });
200
- cfg.onSeedOutcome?.({ ok: false, error: err });
148
+ const connected = collab.getStatus() === 'connected';
149
+ // A complete local replica can hydrate the Y.Doc before any network is
150
+ // available. Bind that non-empty document immediately so custom editors
151
+ // can reopen offline; only first-open seeding requires a live transport.
152
+ if (!connected && cfg.isEmpty(collab.yDoc))
153
+ return;
154
+ starting = true;
155
+ try {
156
+ // NEVER seed when the transport skipped payloads it could not decode:
157
+ // the Y.Doc looking empty then means "content exists but is unreadable
158
+ // on this client", and seeding would write a default document over the
159
+ // real content for every client (the "Untitled map" clobber).
160
+ const undecoded = collab.hasUndecodedContent?.() === true;
161
+ if (connected && !undecoded && cfg.isEmpty(collab.yDoc)) {
162
+ try {
163
+ const content = await collab.loadInitialContent();
164
+ if (cancelled)
165
+ return;
166
+ // NEVER seed from empty content. A host that has no bytes for this
167
+ // document (reopen of an already-shared doc: no in-memory share
168
+ // payload, no file) returns ''/empty -- seeding from that writes a
169
+ // default document into the shared room and clobbers whatever the
170
+ // room's real content is for every client. Fall through to bind:
171
+ // the room content (or a teammate's seed) will populate the doc.
172
+ if (!hasSeedableContent(content)) {
173
+ console.warn('[useCollaborativeEditor] Skipping first-open seed: host returned empty initial content.');
201
174
  }
202
- else {
203
- setSeedError(null);
204
- collab.reportSeedOutcome?.({ ok: true });
205
- cfg.onSeedOutcome?.({ ok: true });
175
+ // Re-check emptiness in case another client seeded while we were
176
+ // awaiting -- they would have raced through the WebSocket and
177
+ // applied their update during our await gap. Avoid double-seeding
178
+ // in that case; CRDT merge would otherwise insert duplicate
179
+ // content unless the seed is fully deterministic.
180
+ else if (cfg.isEmpty(collab.yDoc)) {
181
+ collab.yDoc.transact(() => {
182
+ cfg.seed(collab.yDoc, content);
183
+ }, COLLAB_INIT_ORIGIN);
184
+ // Durability: the seed the user sees locally must reach the server
185
+ // before this provider can tear down. flushWithAck resolves only
186
+ // after a server-persisted ack; flushLocalState is the deprecated
187
+ // fire-and-forget fallback for older hosts.
188
+ const flushed = collab.flushWithAck
189
+ ? await collab.flushWithAck()
190
+ : (await collab.flushLocalState?.(), true);
191
+ if (!flushed) {
192
+ const err = new Error('Seed flush was not confirmed by the server before timeout; content may not have persisted.');
193
+ console.warn('[useCollaborativeEditor]', err.message);
194
+ setSeedError(err);
195
+ collab.reportSeedOutcome?.({ ok: false, error: err });
196
+ cfg.onSeedOutcome?.({ ok: false, error: err });
197
+ }
198
+ else {
199
+ setSeedError(null);
200
+ collab.reportSeedOutcome?.({ ok: true });
201
+ cfg.onSeedOutcome?.({ ok: true });
202
+ }
206
203
  }
207
204
  }
205
+ catch (err) {
206
+ console.error('[useCollaborativeEditor] Failed to load/seed initial content:', err);
207
+ setSeedError(err);
208
+ collab.reportSeedOutcome?.({ ok: false, error: err });
209
+ cfg.onSeedOutcome?.({ ok: false, error: err });
210
+ // Continue with bind -- the doc may still be usable once another
211
+ // client seeds it.
212
+ }
208
213
  }
209
- catch (err) {
210
- console.error('[useCollaborativeEditor] Failed to load/seed initial content:', err);
211
- setSeedError(err);
212
- collab.reportSeedOutcome?.({ ok: false, error: err });
213
- cfg.onSeedOutcome?.({ ok: false, error: err });
214
- // Continue with bind -- the doc may still be usable once another
215
- // client seeds it.
216
- }
217
- }
218
- if (cancelled)
219
- return;
220
- const created = cfg.bind({
221
- yDoc: collab.yDoc,
222
- awareness: collab.awareness,
223
- user: collab.user,
224
- });
225
- const resolved = created instanceof Promise ? await created : created;
226
- if (cancelled) {
227
- // Effect unmounted while we were awaiting the extension's
228
- // imperative API. Destroy the freshly-built handle so any
229
- // observers/awareness subscriptions inside it get cleaned up.
230
- try {
231
- resolved.destroy();
214
+ if (cancelled)
215
+ return;
216
+ const created = cfg.bind({
217
+ yDoc: collab.yDoc,
218
+ awareness: collab.awareness,
219
+ user: collab.user,
220
+ });
221
+ const resolved = created instanceof Promise ? await created : created;
222
+ if (cancelled) {
223
+ // Effect unmounted while we were awaiting the extension's
224
+ // imperative API. Destroy the freshly-built handle so any
225
+ // observers/awareness subscriptions inside it get cleaned up.
226
+ try {
227
+ resolved.destroy();
228
+ }
229
+ catch (err) {
230
+ console.error('[useCollaborativeEditor] post-cancel destroy failed:', err);
231
+ }
232
+ return;
232
233
  }
233
- catch (err) {
234
- console.error('[useCollaborativeEditor] post-cancel destroy failed:', err);
234
+ handle = resolved;
235
+ // A binding that debounces its local pushes holds the newest edit
236
+ // outside the CRDT. Hand the host a drain so it can complete a write
237
+ // before reporting it done -- notably after a mutating AI tool, which
238
+ // otherwise returns success while the edit is still in the debounce.
239
+ if (typeof handle.syncNow === 'function') {
240
+ const syncNow = handle.syncNow.bind(handle);
241
+ unregisterContentFlush = collab.registerContentFlush?.(syncNow) ?? null;
235
242
  }
236
- return;
243
+ setBinding(handle);
244
+ }
245
+ finally {
246
+ starting = false;
237
247
  }
238
- handle = resolved;
239
- setBinding(handle);
240
248
  };
241
249
  void tryStart();
242
250
  const unsubscribe = collab.onStatusChange(() => {
@@ -245,6 +253,8 @@ export function useCollaborativeEditor(host, config) {
245
253
  return () => {
246
254
  cancelled = true;
247
255
  unsubscribe();
256
+ unregisterContentFlush?.();
257
+ unregisterContentFlush = null;
248
258
  if (handle) {
249
259
  handle.destroy();
250
260
  handle = null;
@@ -1 +1 @@
1
- {"version":3,"file":"useEditorLifecycle.d.ts","sourceRoot":"","sources":["../src/useEditorLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMhE,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAEnC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAE/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,iCAAiC;IACjC,QAAQ,EAAE,CAAC,CAAC;IAEZ,qDAAqD;IACrD,QAAQ,EAAE,CAAC,CAAC;IAEZ,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IAEd,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAElB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC;;;OAGG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IAEnB,wCAAwC;IACxC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IAEd,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE/B,iFAAiF;IACjF,gBAAgB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAE3C,+CAA+C;IAC/C,YAAY,EAAE,OAAO,CAAC;CACvB;AAMD,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,EAC3C,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GACpC,wBAAwB,CAAC,CAAC,CAAC,CAiS7B"}
1
+ {"version":3,"file":"useEditorLifecycle.d.ts","sourceRoot":"","sources":["../src/useEditorLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMhE,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAEnC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAE/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,iCAAiC;IACjC,QAAQ,EAAE,CAAC,CAAC;IAEZ,qDAAqD;IACrD,QAAQ,EAAE,CAAC,CAAC;IAEZ,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IAEd,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAElB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC;;;OAGG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IAEnB,wCAAwC;IACxC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IAEd,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE/B,iFAAiF;IACjF,gBAAgB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAE3C,+CAA+C;IAC/C,YAAY,EAAE,OAAO,CAAC;CACvB;AAMD,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,EAC3C,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GACpC,wBAAwB,CAAC,CAAC,CAAC,CAmS7B"}
@@ -146,6 +146,7 @@ export function useEditorLifecycle(host, options) {
146
146
  }
147
147
  if (!mounted)
148
148
  return;
149
+ setError(null);
149
150
  setIsLoading(false);
150
151
  optionsRef.current.onLoaded?.();
151
152
  }
@@ -204,6 +205,7 @@ export function useEditorLifecycle(host, options) {
204
205
  const parsed = parseContent(newRawContent);
205
206
  lastSavedContentRef.current = newRawContent;
206
207
  opts.applyContent(parsed);
208
+ setError(null);
207
209
  clearDirty();
208
210
  opts.onExternalChange?.(parsed);
209
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbalyst/extension-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "SDK for building Nimbalyst extensions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,6 +29,10 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.js"
31
31
  },
32
+ "./collab": {
33
+ "types": "./dist/collab/index.d.ts",
34
+ "import": "./dist/collab/index.js"
35
+ },
32
36
  "./vite": {
33
37
  "types": "./dist/vite.d.ts",
34
38
  "import": "./dist/vite.js"
@@ -44,6 +48,10 @@
44
48
  "./file-tree": {
45
49
  "types": "./dist/fileDirectoryTree.d.ts",
46
50
  "import": "./dist/fileDirectoryTree.js"
51
+ },
52
+ "./file-mask": {
53
+ "types": "./dist/fileMask.d.ts",
54
+ "import": "./dist/fileMask.js"
47
55
  }
48
56
  },
49
57
  "files": [
@@ -52,10 +60,11 @@
52
60
  "CHANGELOG.md"
53
61
  ],
54
62
  "scripts": {
55
- "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc",
63
+ "build": "tsc",
64
+ "build:clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('.tsbuildinfo',{force:true})\" && tsc",
56
65
  "dev": "tsc --watch",
57
66
  "typecheck": "tsc --noEmit",
58
- "prepublishOnly": "npm run build"
67
+ "prepublishOnly": "npm run build:clean"
59
68
  },
60
69
  "dependencies": {},
61
70
  "peerDependencies": {