@voltro/local-first 0.52.0 → 0.54.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/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
- import { a as e, c as t, d as n, f as r, h as i, i as a, l as o, m as s, n as c, o as l, p as u, r as d, s as f, t as p, u as m } from "./room-wFNUwEgi.js";
1
+ import { a as e, c as t, d as n, f as r, g as i, h as a, i as o, l as s, m as c, n as l, o as u, p as d, r as f, s as p, t as m, u as h } from "./room-24dGMM80.js";
2
2
  //#region src/conflict/policy.ts
3
- var h = (e) => {
3
+ var g = (e) => {
4
4
  if (e.writer !== void 0) return `w:${e.writer}`;
5
5
  try {
6
6
  return `v:${JSON.stringify(e.value)}`;
7
7
  } catch {
8
8
  return `v:${String(e.value)}`;
9
9
  }
10
- }, g = (e, t) => t.updatedAt > e.updatedAt ? t.value : t.updatedAt < e.updatedAt ? e.value : h(t) > h(e) ? t.value : e.value, _ = (e) => e === "lastWriteWins" ? g : e, v = (e) => {
10
+ }, _ = (e, t) => t.updatedAt > e.updatedAt ? t.value : t.updatedAt < e.updatedAt ? e.value : g(t) > g(e) ? t.value : e.value, v = (e) => e === "lastWriteWins" ? _ : e, y = (e) => {
11
11
  let t = (t, n, r) => {
12
12
  let i = e[t];
13
- return (i === void 0 ? g : _(i))(n, r);
13
+ return (i === void 0 ? _ : v(i))(n, r);
14
14
  };
15
15
  return {
16
16
  resolveField: t,
@@ -31,32 +31,32 @@ var h = (e) => {
31
31
  return r;
32
32
  }
33
33
  };
34
- }, y = (e, t) => `${e}${t}`, b = () => {
34
+ }, b = (e, t) => `${e}\u0001${t}`, x = () => {
35
35
  let e = /* @__PURE__ */ new Map(), t = [], n = /* @__PURE__ */ new Set(), r = (e) => e.slice();
36
36
  return {
37
37
  kind: "in-memory",
38
38
  loadCrdtState: (t, n) => {
39
- let i = e.get(y(t, n));
39
+ let i = e.get(b(t, n));
40
40
  return Promise.resolve(i === void 0 ? void 0 : r(i));
41
41
  },
42
- saveCrdtState: (t, n, i) => (e.set(y(t, n), r(i)), Promise.resolve()),
42
+ saveCrdtState: (t, n, i) => (e.set(b(t, n), r(i)), Promise.resolve()),
43
43
  loadQueue: () => Promise.resolve(t.map((e) => ({ ...e }))),
44
44
  saveQueue: (e) => (t = e.map((e) => ({ ...e })), Promise.resolve()),
45
- pin: (e, t) => (n.add(y(e, t)), Promise.resolve()),
46
- isPinned: (e, t) => Promise.resolve(n.has(y(e, t)))
45
+ pin: (e, t) => (n.add(b(e, t)), Promise.resolve()),
46
+ isPinned: (e, t) => Promise.resolve(n.has(b(e, t)))
47
47
  };
48
- }, x = "voltro_local_first", S = "\0queue", C = "\0pin\0", w = (e, t) => `cell\u0000${e}\u0000${t}`, T = (e, t) => `${C}${e}\u0000${t}`, E = (e) => new Promise((t, n) => {
48
+ }, S = "voltro_local_first", C = "\0queue", w = "\0pin\0", T = (e, t) => `cell\u0000${e}\u0000${t}`, E = (e, t) => `${w}${e}\u0000${t}`, D = (e) => new Promise((t, n) => {
49
49
  e.onsuccess = () => t(e.result), e.onerror = () => n(e.error ?? /* @__PURE__ */ Error("IndexedDB request failed"));
50
- }), D = async (e) => {
50
+ }), O = async (e) => {
51
51
  let t = e?.factory ?? globalThis.indexedDB;
52
52
  if (t === void 0) throw Error("createIndexedDbPersistence: no IndexedDB available. Pass options.factory (e.g. in a non-browser host) or use createInMemoryPersistence().");
53
53
  let n = e?.databaseName ?? "voltro-local-first", r = t.open(n, 1), i = await new Promise((e, t) => {
54
54
  r.onupgradeneeded = () => {
55
55
  let e = r.result;
56
- e.objectStoreNames.contains(x) || e.createObjectStore(x);
56
+ e.objectStoreNames.contains(S) || e.createObjectStore(S);
57
57
  }, r.onsuccess = () => e(r.result), r.onerror = () => t(r.error ?? /* @__PURE__ */ Error("IndexedDB open failed"));
58
- }), a = async (e) => E(i.transaction(x, "readonly").objectStore(x).get(e)), o = async (e, t) => {
59
- await E(i.transaction(x, "readwrite").objectStore(x).put(t, e));
58
+ }), a = async (e) => D(i.transaction(S, "readonly").objectStore(S).get(e)), o = async (e, t) => {
59
+ await D(i.transaction(S, "readwrite").objectStore(S).put(t, e));
60
60
  }, s = (e) => {
61
61
  if (e instanceof Uint8Array) return e.slice();
62
62
  if (e instanceof ArrayBuffer) return new Uint8Array(e.slice(0));
@@ -64,33 +64,198 @@ var h = (e) => {
64
64
  };
65
65
  return {
66
66
  kind: "indexeddb",
67
- loadCrdtState: async (e, t) => s(await a(w(e, t))),
67
+ loadCrdtState: async (e, t) => s(await a(T(e, t))),
68
68
  saveCrdtState: async (e, t, n) => {
69
- await o(w(e, t), n.slice());
69
+ await o(T(e, t), n.slice());
70
70
  },
71
71
  loadQueue: async () => {
72
- let e = await a(S);
72
+ let e = await a(C);
73
73
  return e === void 0 || !Array.isArray(e.writes) ? [] : e.writes.map((e) => ({ ...e }));
74
74
  },
75
75
  saveQueue: async (e) => {
76
76
  let t = { writes: e.map((e) => ({ ...e })) };
77
- await o(S, t);
77
+ await o(C, t);
78
78
  },
79
79
  pin: async (e, t) => {
80
- await o(T(e, t), !0);
80
+ await o(E(e, t), !0);
81
81
  },
82
- isPinned: async (e, t) => await a(T(e, t)) === !0
82
+ isPinned: async (e, t) => await a(E(e, t)) === !0
83
83
  };
84
- }, O = (e) => e.inFlight === void 0 ? e.pending : [e.inFlight, ...e.pending], k = async (e, t = !0) => {
84
+ }, k = (e) => e.inFlight === void 0 ? e.pending : [e.inFlight, ...e.pending], A = async (e, t = !0) => {
85
85
  let n = await e.loadQueue(), r = {
86
- ...f(t),
86
+ ...p(t),
87
87
  pending: n
88
88
  };
89
89
  return {
90
90
  getState: () => r,
91
- dispatch: async (t) => (r = o(r, t), await e.saveQueue(O(r)), r)
91
+ dispatch: async (t) => (r = s(r, t), await e.saveQueue(k(r)), r)
92
92
  };
93
- }, A = () => ({ rooms: /* @__PURE__ */ new Map() }), j = (e = A()) => {
93
+ }, j = (e) => {
94
+ let t = e;
95
+ return typeof t == "object" && !!t && typeof t.id == "string" && typeof t.tag == "string" && (t.status === "pending" || t.status === "sent" || t.status === "failed" || t.status === "conflict");
96
+ }, M = (e, t = Date.now) => ({
97
+ load: async () => (await e.loadQueue()).map((e) => e.payload).filter(j),
98
+ save: async (n) => {
99
+ let r = n.map((e) => ({
100
+ id: e.id,
101
+ payload: e,
102
+ enqueuedAt: t(),
103
+ attempts: e.attempts
104
+ }));
105
+ await e.saveQueue(r);
106
+ }
107
+ }), N = () => {
108
+ let e = /* @__PURE__ */ new Map();
109
+ return {
110
+ kind: "in-memory",
111
+ get: (t) => Promise.resolve(e.get(t)),
112
+ put: (t, n) => (e.set(t, n), Promise.resolve()),
113
+ del: (t) => (e.delete(t), Promise.resolve()),
114
+ keysWithPrefix: (t) => Promise.resolve([...e.keys()].filter((e) => e.startsWith(t)))
115
+ };
116
+ }, P = (e) => new Promise((t, n) => {
117
+ e.onsuccess = () => t(e.result), e.onerror = () => n(e.error ?? /* @__PURE__ */ Error("IndexedDB request failed"));
118
+ }), F = "kv", I = async (e) => {
119
+ let t = e?.factory ?? globalThis.indexedDB;
120
+ if (t === void 0) throw Error("createIndexedDbKv: no IndexedDB available. Pass options.factory or use createInMemoryKv().");
121
+ let n = t.open(e?.databaseName ?? "voltro-local-first-kv", 1), r = await new Promise((e, t) => {
122
+ n.onupgradeneeded = () => {
123
+ n.result.objectStoreNames.contains(F) || n.result.createObjectStore(F);
124
+ }, n.onsuccess = () => e(n.result), n.onerror = () => t(n.error ?? /* @__PURE__ */ Error("IndexedDB open failed"));
125
+ }), i = (e) => r.transaction(F, e).objectStore(F);
126
+ return {
127
+ kind: "indexeddb",
128
+ get: (e) => P(i("readonly").get(e)),
129
+ put: async (e, t) => {
130
+ await P(i("readwrite").put(t, e));
131
+ },
132
+ del: async (e) => {
133
+ await P(i("readwrite").delete(e));
134
+ },
135
+ keysWithPrefix: async (e) => {
136
+ let t = globalThis.IDBKeyRange;
137
+ return t === void 0 ? (await P(i("readonly").getAllKeys())).filter((t) => t.startsWith(e)) : await P(i("readonly").getAllKeys(t.bound(e, `${e}\uffff`)));
138
+ }
139
+ };
140
+ }, L = async (e) => {
141
+ try {
142
+ return {
143
+ kv: await I(e),
144
+ durability: "durable"
145
+ };
146
+ } catch {
147
+ return {
148
+ kv: N(),
149
+ durability: "memory"
150
+ };
151
+ }
152
+ }, R = "mirror", z = "\0", B = (e) => `${R}${z}${e.subjectId}${z}${e.tenantId}${z}`, V = (e, t) => `${B(e)}q${z}${t}`, H = (e, t) => {
153
+ if (t.length === 0) return e;
154
+ let n = (e) => {
155
+ if (typeof e != "object" || !e || Array.isArray(e)) return e;
156
+ let n = {};
157
+ for (let [r, i] of Object.entries(e)) t.includes(r) || (n[r] = i);
158
+ return n;
159
+ };
160
+ return Array.isArray(e) ? e.map(n) : n(e);
161
+ }, U = (e) => {
162
+ try {
163
+ return JSON.stringify(e)?.length ?? 0;
164
+ } catch {
165
+ return 0;
166
+ }
167
+ }, W = (e, t, n = {}, r = Date.now) => ({
168
+ partition: t,
169
+ save: async (i, a) => {
170
+ let o = n.encryptedColumnsOf?.(a.tag) ?? [], s = a.fingerprint, c = {
171
+ tag: a.tag,
172
+ data: H(a.data, o),
173
+ revision: a.revision,
174
+ savedAt: r(),
175
+ ...s === void 0 ? {} : { fingerprint: s }
176
+ };
177
+ await e.put(V(t, i), c);
178
+ },
179
+ load: async (n) => {
180
+ let r = await e.get(V(t, n));
181
+ if (typeof r != "object" || !r) return;
182
+ let i = r;
183
+ return typeof i.revision == "number" && typeof i.tag == "string" ? i : void 0;
184
+ },
185
+ purge: async () => {
186
+ let n = await e.keysWithPrefix(B(t));
187
+ for (let t of n) await e.del(t);
188
+ },
189
+ enforceBudget: async (n) => {
190
+ let r = await e.keysWithPrefix(B(t)), i = [];
191
+ for (let t of r) {
192
+ let n = await e.get(t);
193
+ i.push({
194
+ key: t,
195
+ savedAt: n?.savedAt ?? 0,
196
+ bytes: U(n)
197
+ });
198
+ }
199
+ let a = i.reduce((e, t) => e + t.bytes, 0), o = [];
200
+ i.sort((e, t) => e.savedAt - t.savedAt);
201
+ for (let t of i) {
202
+ if (a <= n) break;
203
+ await e.del(t.key), a -= t.bytes, o.push(t.key);
204
+ }
205
+ return o;
206
+ }
207
+ }), G = (e, t) => {
208
+ let n = (e) => {
209
+ let n = t.tags[e];
210
+ if (n !== void 0) return t.metadata?.[n]?.encryptedColumns;
211
+ };
212
+ return {
213
+ load: async (n) => {
214
+ let r = await e.load(n);
215
+ if (r === void 0 || t.tags[r.tag] === void 0) return;
216
+ let i = r.fingerprint;
217
+ if (t.schemaFingerprint === void 0 || i === t.schemaFingerprint) return {
218
+ data: r.data,
219
+ revision: r.revision
220
+ };
221
+ },
222
+ save: async (r, i) => {
223
+ if (i.tag === void 0 || t.tags[i.tag] === void 0) return;
224
+ let a = n(i.tag) ?? [], o = a.length === 0 ? i.data : K(i.data, a);
225
+ await e.save(r, {
226
+ tag: i.tag,
227
+ data: o,
228
+ revision: i.revision,
229
+ ...t.schemaFingerprint === void 0 ? {} : { fingerprint: t.schemaFingerprint }
230
+ });
231
+ }
232
+ };
233
+ }, K = (e, t) => {
234
+ let n = (e) => {
235
+ if (typeof e != "object" || !e || Array.isArray(e)) return e;
236
+ let n = {};
237
+ for (let [r, i] of Object.entries(e)) t.includes(r) || (n[r] = i);
238
+ return n;
239
+ };
240
+ return Array.isArray(e) ? e.map(n) : n(e);
241
+ }, q = (e, t, n, r = {}) => {
242
+ let i = new Set(r.crdtColumns ?? []), o = r.backend ?? d, s = e.resolveRecord(Object.fromEntries(Object.entries(t).filter(([e]) => !i.has(e))), Object.fromEntries(Object.entries(n).filter(([e]) => !i.has(e))));
243
+ for (let e of i) {
244
+ let r = t[e]?.value, i = n[e]?.value;
245
+ r !== void 0 && i !== void 0 ? s[e] = a(r, i, o) : r === void 0 ? i !== void 0 && (s[e] = i) : s[e] = r;
246
+ }
247
+ return s;
248
+ }, J = async (e, t) => {
249
+ let n = e.locks === null ? void 0 : e.locks ?? globalThis.navigator?.locks;
250
+ if (n === void 0) return await t(), "no-locks";
251
+ let r = "held-elsewhere";
252
+ return await n.request(`voltro:outbox:${e.name}`, {
253
+ mode: "exclusive",
254
+ ifAvailable: !0
255
+ }, async (e) => {
256
+ e !== null && (await t(), r = "drained");
257
+ }), r;
258
+ }, Y = () => ({ rooms: /* @__PURE__ */ new Map() }), X = (e = Y()) => {
94
259
  let t = /* @__PURE__ */ new Set(), n = (t) => {
95
260
  let n = e.rooms.get(t);
96
261
  return n === void 0 && (n = /* @__PURE__ */ new Set(), e.rooms.set(t, n)), n;
@@ -112,16 +277,12 @@ var h = (e) => {
112
277
  };
113
278
  }
114
279
  };
115
- }, M = [
280
+ }, Z = [
116
281
  "durable-persistence-indexeddb",
117
282
  "sync-transport-syncclient",
118
283
  "sync-transport-react-binding",
119
284
  "presence-room",
120
285
  "localFirst-mixin-discovery"
121
- ], N = [
122
- "sync-transport-app-tags",
123
- "presence-broker-binding",
124
- "wasm-sqlite-durable-adapter"
125
- ];
286
+ ], Q = ["sync-transport-app-tags"];
126
287
  //#endregion
127
- export { M as IMPLEMENTED_SINCE_SEAMS, N as RUNTIME_SEAMS, l as canDrain, v as conflictPolicy, d as connectionReducer, m as crdtText, b as createInMemoryPersistence, A as createInMemoryPresenceBus, j as createInMemoryPresenceChannel, D as createIndexedDbPersistence, p as createPresenceRoom, c as createSyncClient, n as decodeCrdtText, r as defaultCrdtBackend, a as deriveSyncStatus, u as emptyCrdtState, e as initialConnection, f as initialSyncQueue, g as lastWriteWins, k as loadPersistedSyncQueue, s as mergeCrdtStates, t as outstandingCount, o as syncQueueReducer, i as yjsBackend };
288
+ export { Z as IMPLEMENTED_SINCE_SEAMS, Q as RUNTIME_SEAMS, u as canDrain, y as conflictPolicy, f as connectionReducer, h as crdtMergeCell, n as crdtText, L as createDurableKv, N as createInMemoryKv, x as createInMemoryPersistence, Y as createInMemoryPresenceBus, X as createInMemoryPresenceChannel, I as createIndexedDbKv, O as createIndexedDbPersistence, m as createPresenceRoom, W as createQueryMirror, G as createSubscriptionMirrorBinding, l as createSyncClient, r as decodeCrdtText, d as defaultCrdtBackend, o as deriveSyncStatus, c as emptyCrdtState, e as initialConnection, p as initialSyncQueue, _ as lastWriteWins, A as loadPersistedSyncQueue, a as mergeCrdtStates, M as outboxPersistence, t as outstandingCount, q as resolveWithPolicy, s as syncQueueReducer, J as withDrainLock, i as yjsBackend };
package/dist/react.d.ts CHANGED
@@ -6,18 +6,29 @@ declare interface ConnectionState {
6
6
  readonly attempt: number;
7
7
  }
8
8
 
9
- /**
10
- * A CRDT implementation. One per library. The default is {@link yjsBackend}.
11
- *
12
- * `merge` and `decodeText` are STATELESS with respect to any live handle: they
13
- * operate purely on encoded bytes, which is what lets the sync layer and tests
14
- * converge two states without holding a document open.
15
- */
16
9
  declare interface CrdtBackend {
17
10
  /** Identifies the library, e.g. `"yjs"`. Surfaced for diagnostics/telemetry. */
18
11
  readonly name: string;
19
12
  /** Create a fresh text buffer, optionally seeded with `initial`. */
20
13
  createText: (initial?: string) => CrdtTextHandle;
14
+ /** Create a whole-document handle, optionally seeded from encoded state. */
15
+ createDoc: (initial?: CrdtState) => CrdtDocHandle;
16
+ /**
17
+ * Re-encode a state through a live document so the backend garbage-collects
18
+ * what it can (Yjs: `mergeUpdates` never GCs tombstones, so a hot document's
19
+ * blob grows monotonically without this). Loses UPDATE HISTORY by design —
20
+ * a version-history feature must snapshot BEFORE compacting.
21
+ */
22
+ compact: (state: CrdtState) => CrdtState;
23
+ /**
24
+ * HARD compaction for a text state: re-create the document from its
25
+ * MATERIALISED text, dropping all history, tombstones and client ids. The
26
+ * result is minimal — and a NEW EPOCH: it does not merge with updates from
27
+ * the old lineage, so the server treats a rebase as a fresh snapshot for
28
+ * every subscriber (which the subscription lane already handles as a
29
+ * reset). Use when `compact` no longer bounds a hot document.
30
+ */
31
+ rebaseText: (state: CrdtState) => CrdtState;
21
32
  /**
22
33
  * Merge two encoded states into one converged encoded state.
23
34
  *
@@ -33,6 +44,70 @@ declare interface CrdtBackend {
33
44
  emptyState: () => CrdtState;
34
45
  }
35
46
 
47
+ /**
48
+ * A CRDT implementation. One per library. The default is {@link yjsBackend}.
49
+ *
50
+ * `merge` and `decodeText` are STATELESS with respect to any live handle: they
51
+ * operate purely on encoded bytes, which is what lets the sync layer and tests
52
+ * converge two states without holding a document open.
53
+ */
54
+ /**
55
+ * A live, mutable CRDT DOCUMENT — the whole-doc generalisation `crdtDoc()`
56
+ * stores (rich text, maps, arrays; whatever the backend's document model
57
+ * holds). `crdtText()` remains the single-field specialisation over the same
58
+ * backend.
59
+ */
60
+ declare interface CrdtDocHandle {
61
+ /**
62
+ * The underlying library document (a `Y.Doc` for the yjs backend), typed
63
+ * opaquely: an EDITOR BINDING needs the real object (Tiptap's Collaboration
64
+ * extension takes a Y.Doc), while every sync-layer consumer only moves
65
+ * encoded bytes and must never reach in. Cast at the binding, nowhere else.
66
+ */
67
+ readonly raw: unknown;
68
+ /** Full state, encoded for transport or merge. */
69
+ encodeState: () => CrdtState;
70
+ /** This doc's state VECTOR — the compact "what I have" summary a peer
71
+ * diffs against. */
72
+ stateVector: () => Uint8Array;
73
+ /**
74
+ * Encode only what a peer holding `sinceVector` is missing — the
75
+ * INCREMENTAL update lane. A 1-character edit against a 100 KB doc encodes
76
+ * to a few dozen bytes, not the full state.
77
+ */
78
+ encodeUpdateSince: (sinceVector: Uint8Array) => CrdtState;
79
+ /** Fold an encoded state or incremental update into this doc, in place. */
80
+ applyState: (state: CrdtState) => void;
81
+ /**
82
+ * Subscribe to update blobs this doc produces. Returns the unsubscribe.
83
+ *
84
+ * `local` distinguishes an edit made HERE from one produced by folding a
85
+ * remote state through `applyState`, and it is the difference between a
86
+ * working push loop and an echo: without it, applying a peer's update fires
87
+ * this handler, the app pushes it back, and every client re-broadcasts what
88
+ * it just received. Measured with three tabs open: one keystroke produced
89
+ * three server writes instead of one. It converges — the merge is
90
+ * idempotent — but the amplification scales with the session.
91
+ *
92
+ * It had to come from the BACKEND rather than an `applying` flag around
93
+ * `applyState` in app code: that flag is only correct while the backend emits
94
+ * synchronously, which `CrdtBackend` deliberately does not promise ("the
95
+ * backend decision lives behind our abstraction so it can change").
96
+ */
97
+ onUpdate: (handler: (update: CrdtState, meta: {
98
+ readonly local: boolean;
99
+ }) => void) => () => void;
100
+ /**
101
+ * Encode a stable ANCHOR at `index` of the named text field — a position
102
+ * that survives concurrent edits (the primitive an inline-comment UI pins
103
+ * threads with; the UI itself is the comments plugin's business).
104
+ */
105
+ encodeAnchor: (field: string, index: number) => Uint8Array;
106
+ /** Resolve an anchor back to its current index, or `undefined` when the
107
+ * anchored region was deleted. */
108
+ resolveAnchor: (encoded: Uint8Array) => number | undefined;
109
+ }
110
+
36
111
  /** Identifies one CRDT-managed cell: a `crdtText()` COLUMN of one ROW of one TABLE. */
37
112
  declare interface CrdtDocKey {
38
113
  readonly table: string;
@@ -41,9 +116,9 @@ declare interface CrdtDocKey {
41
116
  }
42
117
 
43
118
  /**
44
- * One local edit, as {@link UseCrdtTextOptions.push} receives it: the cell it
45
- * belongs to, the encoded update to write, and how many times delivering it has
46
- * already failed.
119
+ * One local edit, as a hook's `push` option receives it: the cell it belongs
120
+ * to, the encoded update to write, and how many times delivering it has already
121
+ * failed.
47
122
  *
48
123
  * `attempts` is carried because the sync client deliberately has no built-in
49
124
  * backoff delay — spacing or jittering a retry is the transport's lever, and it
@@ -199,6 +274,95 @@ export declare interface UseConnectionStatus {
199
274
  */
200
275
  export declare const useConnectionStatus: (outstanding?: number) => UseConnectionStatus;
201
276
 
277
+ export declare interface UseCrdtDoc {
278
+ /**
279
+ * The live document — pass it straight to `useCrdtEditor({ doc })`.
280
+ *
281
+ * `null` until the mount effect has run. That is not a papercut to design
282
+ * away: constructing it during render is what made the editor crash a
283
+ * prerender and leak an instance under StrictMode. Render the editor in a
284
+ * child component so its `useCrdtEditor` call is not conditional:
285
+ * `{doc.doc !== null ? <Editor doc={doc.doc} /> : null}`.
286
+ *
287
+ * Its encoded state is one call away (`doc.encodeState()`), which is why
288
+ * this hook exposes no separate `state` field — there would be two answers
289
+ * to one question.
290
+ */
291
+ readonly doc: CrdtDocHandle | null;
292
+ /**
293
+ * `true` once an authoritative state has been folded in. Distinguishes an
294
+ * EMPTY document from one that has not arrived yet — the same distinction
295
+ * `remote: null` makes on the way in, surfaced so a UI can show a skeleton
296
+ * instead of an empty editor a user might start typing into.
297
+ */
298
+ readonly loaded: boolean;
299
+ /** Writes not yet acknowledged. `0` means every local edit has landed. */
300
+ readonly outstanding: number;
301
+ /** `outstanding === 0`. */
302
+ readonly synced: boolean;
303
+ /** Report reachability. Going online drains the queue. */
304
+ readonly setOnline: (online: boolean) => void;
305
+ }
306
+
307
+ /**
308
+ * A collaborative rich-text document bound to one `crdtDoc()` cell.
309
+ *
310
+ * ```tsx
311
+ * const row = useSubscription('app', 'documents.byId', { id })
312
+ * const save = useMutation('app', 'documents.setBody')
313
+ * const shared = useCrdtDoc({
314
+ * cell: { table: 'documents', id, column: 'body' },
315
+ * remote: row.data?.body ?? null,
316
+ * push: (w) => save.mutate({ id: w.id, update: w.update }),
317
+ * })
318
+ *
319
+ * // In a CHILD component, so `useCrdtEditor` is never called conditionally:
320
+ * const editor = useCrdtEditor({ doc })
321
+ * return <EditorContent editor={editor} />
322
+ * ```
323
+ */
324
+ export declare const useCrdtDoc: (options: UseCrdtDocOptions) => UseCrdtDoc;
325
+
326
+ export declare interface UseCrdtDocOptions {
327
+ /** The cell: table + row id + the `crdtDoc()` column. */
328
+ readonly cell: CrdtDocKey;
329
+ /**
330
+ * The authoritative state as the app currently sees it — the column's value
331
+ * from the reactive query streaming this row.
332
+ *
333
+ * `null`/`undefined` means "not loaded yet" and is folded as nothing, NOT as
334
+ * an empty document: treating a loading row as empty would let the first
335
+ * keystroke race the load and push a state that erases the stored document.
336
+ */
337
+ readonly remote?: CrdtState | null | undefined;
338
+ /**
339
+ * Deliver a local edit to the server. Bind it to the mutation that writes the
340
+ * `crdtDoc()` column; the server folds it into the stored state
341
+ * authoritatively, so this never has to read-modify-write.
342
+ *
343
+ * What it receives is an INCREMENTAL Yjs update, not the full document — a
344
+ * one-character edit in a 100 KB document is a few dozen bytes.
345
+ *
346
+ * REJECT to trigger the queue's retry — a resolved promise is an ack.
347
+ */
348
+ readonly push: (write: CrdtPush) => Promise<unknown>;
349
+ /**
350
+ * Durable local storage. Omit for an ephemeral (in-memory) client.
351
+ *
352
+ * Must be a STABLE reference — it is a construction option, so a new object
353
+ * each render rebuilds the sync client AND the document, dropping the queue.
354
+ * Build it once (`await createIndexedDbPersistence(…)`) and hold it in state
355
+ * or a module constant. Same for `backend`, `online` and `maxAttempts`.
356
+ */
357
+ readonly adapter?: PersistenceAdapter;
358
+ /** CRDT backend for the local optimistic merge. Defaults to Yjs. */
359
+ readonly backend?: CrdtBackend;
360
+ /** Seed the connection belief (default `true`). */
361
+ readonly online?: boolean;
362
+ /** Stop re-driving a write after this many failed attempts. Default 5. */
363
+ readonly maxAttempts?: number;
364
+ }
365
+
202
366
  export declare interface UseCrdtText {
203
367
  /** The merged text — local edits and every peer's, converged. */
204
368
  readonly text: string;