@ai-matrx/office 0.1.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ W1-G2a2. The collab layer shared by Sheets and Docs moves in from matrx-frontend.
6
+
7
+ - New `./univer/collab` entry (client): `WorkbookCollabSession` (the Univer <-> Yjs bridge, with `CommandServiceLike`, `CollabProviderLike`, `CollabMutationInfo`) and `useWorkbookRealtime` (snapshot-insert subscription). `autosave-writer` was already gone (W1-B moved the election into `@ai-matrx/data/realtime/collab`).
8
+ - New required host port `readNewestWorkbookSnapshot(workbookId)` on `configureUniverHost` (the realtime catch-up read; the package owns no database client).
9
+ - Depends on `@ai-matrx/data`, `yjs`, `y-protocols`.
10
+
11
+ ### Consumer action
12
+
13
+ Add `readNewestWorkbookSnapshot` to your `configureUniverHost(...)` call. Replace `@/features/workbooks/collab/WorkbookCollabSession` and `@/features/workbooks/hooks/useWorkbookRealtime` imports with `@ai-matrx/office/univer/collab`.
14
+
15
+ ## 0.2.0
16
+
17
+ W1-G2a1. The Univer glue shared by Sheets and Docs moves in from matrx-frontend `lib/univer`.
18
+
19
+ - New `./univer` entry (client; loads `@univerjs/*`): `disposeUniverInstance`, `identifyUniverUser`, `isSnapshotMutation` / `isWorkbookSnapshotMutation`, `matrxImagePorts` + `resolveExportImage` + `uploadImportedImage`, `openEditorContent` (`registerOpenEditorContent`, `showSavedSnapshot`, `swapContentGuarded`), `registerMissingUniverIcons`, `registerUniverFacadeDependencies`, `routeUniverNotificationsToApp`, `resolveUniverCanvasColor`, `univerImageStore`, `useUniverDarkModeSync`. A transitional entry: it goes internal again when the two editors move into their sections.
20
+ - New `./univer/history` entry (pure): the `document-history` / `workbook-history` tab keys, `readHistoryTab`, `documentHistoryToggleInput`, `workbookHistoryToggleInput`. The two canvas-kind registrations stay in the host app (they lazy-load the views, so no import cycle).
21
+ - New host ports: `configureUniverHost({ toast, getSignedInUser, files, notifyUploadError, useThemeMode })`. Univer glue throws naming the port if an editor mounts before the host configured them.
22
+
23
+ ### Consumer action
24
+
25
+ Call `configureUniverHost(...)` once at start-up, before a workbook or document editor mounts. Replace `@/lib/univer/*` imports with `@ai-matrx/office/univer`, and `@/lib/univer/historyKinds` with `@ai-matrx/office/univer/history`.
26
+
3
27
  ## 0.1.0
4
28
 
5
29
  First release (W1-G1a). `/core`: editor menu model, ribbon model, Google parity rows, document page style, `PageSetup`, product hosts, Office row types. `/core/page-setup-apply`: Univer page-setup commands.
@@ -0,0 +1,378 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/univer/collab/index.ts
32
+ var collab_exports = {};
33
+ __export(collab_exports, {
34
+ WorkbookCollabSession: () => WorkbookCollabSession,
35
+ isCellEditorScratch: () => isCellEditorScratch,
36
+ useWorkbookRealtime: () => useWorkbookRealtime
37
+ });
38
+ module.exports = __toCommonJS(collab_exports);
39
+
40
+ // src/univer/collab/WorkbookCollabSession.ts
41
+ var Y = __toESM(require("yjs"), 1);
42
+ var import_awareness = require("y-protocols/awareness");
43
+ var import_collab = require("@ai-matrx/data/realtime/collab");
44
+ var MUTATIONS_ARRAY_KEY = "mutations";
45
+ var CONTROL_MAP_KEY = "control";
46
+ var REPLACED_KEY = "replaced";
47
+ var WorkbookCollabSession = class {
48
+ options;
49
+ doc = null;
50
+ awareness = null;
51
+ provider = null;
52
+ commandDisposer = null;
53
+ yArrayObserver = null;
54
+ awarenessObserver = null;
55
+ controlObserver = null;
56
+ /** >0 while this client swaps its own content: its mutations are not edits. */
57
+ muted = 0;
58
+ started = false;
59
+ disposed = false;
60
+ constructor(options) {
61
+ this.options = options;
62
+ }
63
+ async start() {
64
+ if (this.started || this.disposed) return;
65
+ this.started = true;
66
+ this.doc = new Y.Doc();
67
+ this.awareness = new import_awareness.Awareness(this.doc);
68
+ this.awareness.setLocalState({
69
+ uid: this.options.uid,
70
+ clientId: this.options.clientId,
71
+ name: this.options.uid.slice(0, 8),
72
+ color: pickColor(this.options.uid),
73
+ sheetId: null,
74
+ row: null,
75
+ col: null,
76
+ ts: Date.now()
77
+ });
78
+ this.provider = this.options.makeProvider({
79
+ workbookId: this.options.workbookId,
80
+ clientId: this.options.clientId,
81
+ doc: this.doc,
82
+ awareness: this.awareness,
83
+ onLeaderChange: () => {
84
+ if (this.awareness) this.options.onAwarenessChange?.(this.awareness);
85
+ }
86
+ });
87
+ const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
88
+ this.yArrayObserver = (event) => {
89
+ if (event.transaction.local) return;
90
+ for (const change of event.changes.delta) {
91
+ if (!change.insert || !Array.isArray(change.insert)) continue;
92
+ for (const op of change.insert) {
93
+ this.applyRemoteMutation(op);
94
+ }
95
+ }
96
+ };
97
+ yArray.observe(this.yArrayObserver);
98
+ if (this.options.onAwarenessChange) {
99
+ const cb = this.options.onAwarenessChange;
100
+ this.awarenessObserver = () => {
101
+ if (this.awareness) cb(this.awareness);
102
+ };
103
+ this.awareness.on("change", this.awarenessObserver);
104
+ }
105
+ this.commandDisposer = this.options.commandService.onMutationExecutedForCollab(
106
+ (info) => this.handleLocalMutation(info)
107
+ );
108
+ await this.provider.connect();
109
+ await this.provider.ready();
110
+ if (this.disposed || !this.doc) return;
111
+ const control = this.doc.getMap(CONTROL_MAP_KEY);
112
+ const seen = control.get(REPLACED_KEY)?.nonce ?? null;
113
+ this.controlObserver = (event) => {
114
+ if (event.transaction.local || !event.keysChanged.has(REPLACED_KEY)) return;
115
+ const signal = control.get(REPLACED_KEY);
116
+ if (!signal?.snapshotId || signal.nonce === seen) return;
117
+ this.options.onReplaced?.(signal.snapshotId);
118
+ };
119
+ control.observe(this.controlObserver);
120
+ }
121
+ stop() {
122
+ if (this.disposed) return;
123
+ this.disposed = true;
124
+ this.commandDisposer?.dispose();
125
+ this.commandDisposer = null;
126
+ if (this.doc && this.yArrayObserver) {
127
+ const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
128
+ yArray.unobserve(
129
+ this.yArrayObserver
130
+ );
131
+ this.yArrayObserver = null;
132
+ }
133
+ if (this.doc && this.controlObserver) {
134
+ this.doc.getMap(CONTROL_MAP_KEY).unobserve(this.controlObserver);
135
+ this.controlObserver = null;
136
+ }
137
+ if (this.awareness && this.awarenessObserver) {
138
+ this.awareness.off("change", this.awarenessObserver);
139
+ this.awarenessObserver = null;
140
+ }
141
+ this.provider?.disconnect();
142
+ this.provider = null;
143
+ if (this.awareness) {
144
+ this.awareness.destroy();
145
+ this.awareness = null;
146
+ }
147
+ if (this.doc) {
148
+ this.doc.destroy();
149
+ this.doc = null;
150
+ }
151
+ }
152
+ /**
153
+ * How many mutations the shared log holds right now. Read it BEFORE
154
+ * `workbook.save()`: that many entries are guaranteed to be inside the
155
+ * snapshot that save produces.
156
+ */
157
+ logLength() {
158
+ return this.doc?.getArray(MUTATIONS_ARRAY_KEY).length ?? 0;
159
+ }
160
+ /**
161
+ * Drop the first `upTo` mutations from the shared log once a snapshot that
162
+ * contains them has been SAVED. Without this a joiner loads the saved
163
+ * workbook (which already has every mutation baked in) and then catches up
164
+ * the whole log on top of it, replaying edits into a workbook that already
165
+ * holds them — Univer refuses with "Trying to overwrite value at key"
166
+ * (feedback 88184e53). Only the host calls this; peers receive it as a
167
+ * delete, which the inbound observer ignores (it acts on inserts only).
168
+ */
169
+ compactLog(upTo) {
170
+ if (!this.doc || upTo <= 0) return;
171
+ const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
172
+ const n = Math.min(upTo, yArray.length);
173
+ if (n > 0) this.doc.transact(() => yArray.delete(0, n));
174
+ }
175
+ /**
176
+ * This client replaced the whole content and SAVED it as `snapshotId`.
177
+ * One transaction empties the shared mutation log (every entry targets the
178
+ * content that is gone — a joiner must never replay them onto the new
179
+ * snapshot) and tells every peer to reload the newest snapshot.
180
+ */
181
+ announceReplaced(snapshotId) {
182
+ if (!this.doc) return;
183
+ const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
184
+ const control = this.doc.getMap(CONTROL_MAP_KEY);
185
+ const signal = {
186
+ snapshotId,
187
+ by: this.options.clientId,
188
+ nonce: `${this.options.clientId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`
189
+ };
190
+ this.doc.transact(() => {
191
+ if (yArray.length > 0) yArray.delete(0, yArray.length);
192
+ control.set(REPLACED_KEY, signal);
193
+ });
194
+ }
195
+ /**
196
+ * Run `swap` (this client tearing its content down and mounting the new
197
+ * one) without broadcasting what Univer emits while it does: those are not
198
+ * edits, and peers reload the same snapshot themselves.
199
+ */
200
+ muteWhile(swap) {
201
+ this.muted += 1;
202
+ try {
203
+ return swap();
204
+ } finally {
205
+ this.muted -= 1;
206
+ }
207
+ }
208
+ /**
209
+ * Update the local awareness cursor. Call this from Univer's selection
210
+ * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site
211
+ * (~50ms is plenty); the provider broadcasts on its own debounced cycle.
212
+ */
213
+ setCursor(input) {
214
+ if (!this.awareness) return;
215
+ const current = this.awareness.getLocalState() ?? {};
216
+ this.awareness.setLocalState({
217
+ ...current,
218
+ sheetId: input.sheetId,
219
+ row: input.row,
220
+ col: input.col,
221
+ ts: Date.now()
222
+ });
223
+ }
224
+ /**
225
+ * Returns the deterministic host: the connected peer with the lowest uid
226
+ * (lexicographic). Host is the only client that writes canonical snapshots.
227
+ * No election protocol; recompute whenever awareness changes.
228
+ */
229
+ electHost(opts = {}) {
230
+ if (!this.awareness) return { isHost: false, hostUid: null };
231
+ const states = Array.from(this.awareness.getStates().values());
232
+ const leader = this.provider?.isTabLeader?.() ?? true;
233
+ return (0, import_collab.electCollabHost)(states, this.options.uid, this.options.clientId, leader, opts.tabTieBreak ?? false);
234
+ }
235
+ // ─── internals ────────────────────────────────────────────────────────
236
+ handleLocalMutation(info) {
237
+ if (this.muted > 0) return;
238
+ if (isCellEditorScratch(info)) return;
239
+ if (info.params && typeof info.params === "object" && !Array.isArray(info.params) && (0, import_collab.remoteOriginOf)(info.params) !== null) {
240
+ return;
241
+ }
242
+ if (!this.doc) return;
243
+ let safeParams;
244
+ try {
245
+ safeParams = info.params === void 0 || info.params === null ? null : JSON.parse(JSON.stringify(info.params));
246
+ } catch (err) {
247
+ console.warn(
248
+ `[collab] skipping non-serializable mutation "${info.id}" \u2014 peers will not receive it`,
249
+ err
250
+ );
251
+ return;
252
+ }
253
+ const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
254
+ yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);
255
+ }
256
+ applyRemoteMutation(op) {
257
+ const taggedParams = op.params && typeof op.params === "object" && !Array.isArray(op.params) ? {
258
+ ...op.params,
259
+ [import_collab.REMOTE_PARAM_TAG]: op.uid ?? true
260
+ } : op.params;
261
+ try {
262
+ this.options.commandService.syncExecuteCommand(op.id, taggedParams, {
263
+ onlyLocal: true,
264
+ fromCollab: true
265
+ });
266
+ } catch (err) {
267
+ console.warn(
268
+ `[collab] could not apply remote mutation "${op.id}" \u2014 skipped`,
269
+ err
270
+ );
271
+ }
272
+ }
273
+ };
274
+ function isCellEditorScratch(info) {
275
+ return typeof info.id === "string" && info.id.startsWith("doc.mutation.");
276
+ }
277
+ var CURSOR_PALETTE = [
278
+ "#3b82f6",
279
+ // blue
280
+ "#10b981",
281
+ // emerald
282
+ "#f59e0b",
283
+ // amber
284
+ "#ef4444",
285
+ // red
286
+ "#8b5cf6",
287
+ // violet
288
+ "#06b6d4",
289
+ // cyan
290
+ "#ec4899",
291
+ // pink
292
+ "#84cc16"
293
+ // lime
294
+ ];
295
+ function pickColor(uid) {
296
+ let h = 2166136261;
297
+ for (let i = 0; i < uid.length; i++) {
298
+ h ^= uid.charCodeAt(i);
299
+ h = Math.imul(h, 16777619);
300
+ }
301
+ return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? "#ec4899";
302
+ }
303
+
304
+ // src/univer/collab/useWorkbookRealtime.ts
305
+ var import_react = require("react");
306
+ var import_realtime = require("@ai-matrx/data/realtime");
307
+ var import_react2 = require("@ai-matrx/data/realtime/react");
308
+
309
+ // src/univer/ports.ts
310
+ var SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.office.univer-host");
311
+ var slot = () => {
312
+ const g = globalThis;
313
+ return g[SLOT] ??= {};
314
+ };
315
+ function getUniverHost() {
316
+ const ports = slot().ports;
317
+ if (!ports) throw new Error("@ai-matrx/office/univer: configureUniverHost(ports) was not called before an editor mounted.");
318
+ return ports;
319
+ }
320
+
321
+ // src/univer/collab/useWorkbookRealtime.ts
322
+ var workbookSnapshotsChannel = (0, import_realtime.defineChannelNamespace)({
323
+ namespace: "udt-workbook-snapshots",
324
+ parts: ["workbookId"],
325
+ description: "workbench.udt_workbook_snapshots inserts for one workbook"
326
+ });
327
+ function useWorkbookRealtime(workbookId, onSnapshot, options) {
328
+ const enabled = options?.enabled ?? true;
329
+ const emit = (0, import_react.useCallback)(
330
+ (row) => {
331
+ if (!row?.id) return;
332
+ onSnapshot({
333
+ snapshotId: row.id,
334
+ // component-created-by-ok: append-only snapshot with NO updated_by column and NO parent-rewrite trigger — created_by is the author the client wrote at insert (the only person field this table has)
335
+ createdBy: row.created_by ?? null,
336
+ createdAt: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
337
+ });
338
+ },
339
+ [onSnapshot]
340
+ );
341
+ (0, import_react2.useChannel)(
342
+ enabled && workbookId ? {
343
+ topic: workbookSnapshotsChannel.topic({ workbookId }),
344
+ postgresChanges: [
345
+ {
346
+ event: "INSERT",
347
+ schema: "workbench",
348
+ table: "udt_workbook_snapshots",
349
+ filter: `workbook_id=eq.${workbookId}`,
350
+ rowId: (row) => typeof row.id === "string" ? row.id : void 0,
351
+ onChange: ({ row }) => emit(row)
352
+ }
353
+ ],
354
+ // Realtime has no replay. Re-read the newest snapshot so a reconnect,
355
+ // tab wake or network restore cannot leave this client checkpointing
356
+ // on top of a base that moved while it was away.
357
+ onBackfill: async () => {
358
+ let data;
359
+ let error;
360
+ try {
361
+ ({ data, error } = await getUniverHost().readNewestWorkbookSnapshot(workbookId));
362
+ } catch (e) {
363
+ data = null;
364
+ error = { message: e instanceof Error ? e.message : String(e) };
365
+ }
366
+ if (error) {
367
+ console.warn(
368
+ "[udt-workbook RT] catch-up read failed \u2014 this workbook may be working from a stale snapshot base; reload before saving.",
369
+ error.message
370
+ );
371
+ return;
372
+ }
373
+ emit(data);
374
+ }
375
+ } : null
376
+ );
377
+ }
378
+ //# sourceMappingURL=univer-collab.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/univer/collab/index.ts","../src/univer/collab/WorkbookCollabSession.ts","../src/univer/collab/useWorkbookRealtime.ts","../src/univer/ports.ts"],"sourcesContent":["/**\n * The collab layer shared by `/sheets` and `/docs` (was matrx-frontend `features/workbooks/collab` and\n * `hooks/useWorkbookRealtime`): the Univer ↔ Yjs session and the snapshot-insert subscription. A\n * transitional subpath, like `./univer`: it goes internal when the editors move into their sections.\n */\nexport * from \"./WorkbookCollabSession\";\nexport * from \"./useWorkbookRealtime\";\n","/**\n * WorkbookCollabSession — the Univer ↔ Yjs bridge.\n *\n * Subscribes to Univer's public `commandService.onMutationExecutedForCollab`\n * hook for local mutations and pushes them onto a `Y.Array<MutationOp>` shared\n * via `SupabaseYjsProvider`. Inbound Yjs updates produce `IMutationInfo` ops\n * that are applied through `commandService.syncExecuteCommand` with the\n * `fromCollab` execution option flagged so the outbound listener short-\n * circuits and we don't echo our own apply.\n *\n * See `FEATURE.md` in this directory for the architecture, the \"do NOT use\n * the Pro preset\" verdict, and the host-election rule. This class is the\n * runtime side of that plan.\n *\n * Lifecycle:\n * - constructor: wire references, no side effects\n * - start(): create Y.Doc + Awareness, instantiate provider, subscribe to\n * command stream, subscribe to Yjs Array observer\n * - stop(): detach all listeners, dispose provider\n *\n * The session is one-shot per workbook open. Reload = stop() + new instance.\n */\n\"use client\";\n\nimport * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\n\nimport { electCollabHost, remoteOriginOf, REMOTE_PARAM_TAG } from \"@ai-matrx/data/realtime/collab\";\n\n/**\n * Structural transport contract. `SupabaseYjsProvider` satisfies it in\n * production; the verification harness injects an in-memory loopback\n * transport through the same seam. Keeping this structural (not the concrete\n * class) is what makes the session testable without network.\n */\nexport type CollabProviderLike = {\n connect(): Promise<void>;\n disconnect(): void;\n ready(): Promise<void>;\n /**\n * True when this is the only holder of its room in this tab allowed to act as\n * host (autosave). Same-tab holders share one client id and one uid, so\n * awareness cannot tell them apart. Absent = a lone holder = leader.\n */\n isTabLeader?(): boolean;\n};\n\n// Univer types are loose at the public hook boundary; we narrow with a\n// structural interface so we don't have to import the full Univer surface.\ntype MutationParams = Record<string, unknown> | unknown[] | null;\n\nexport type CollabMutationInfo = {\n id: string;\n type: number; // CommandType.MUTATION = 2 in Univer's enum\n params: MutationParams;\n /** Who made it (the person's uid), so a peer can tell its own other tab's edit from a stranger's. */\n uid?: string;\n};\n\ntype ExecuteOptions = {\n /** Per-Univer convention: when true, the command runs locally only and is\n * NOT broadcast to other plugins. We set this on inbound-apply so the\n * subsequent onMutationExecutedForCollab does NOT re-fire on our peer. */\n onlyLocal?: boolean;\n fromCollab?: boolean;\n};\n\nexport type CommandServiceLike = {\n onMutationExecutedForCollab(\n listener: (info: CollabMutationInfo, options?: ExecuteOptions) => void,\n ): {\n dispose: () => void;\n };\n syncExecuteCommand(\n id: string,\n params: MutationParams,\n options?: ExecuteOptions,\n ): unknown;\n};\n\nexport type WorkbookCollabSessionOptions = {\n workbookId: string;\n uid: string;\n clientId: string;\n commandService: CommandServiceLike;\n /** Factory so this file does not statically depend on the provider. */\n makeProvider: (args: {\n workbookId: string;\n clientId: string;\n doc: Y.Doc;\n awareness: Awareness;\n /** Wire to the provider so a host handover re-runs the election. */\n onLeaderChange: () => void;\n }) => CollabProviderLike;\n /** Called when the awareness set changes (peers join / leave / move cursor). */\n onAwarenessChange?: (awareness: Awareness) => void;\n /**\n * A peer replaced the whole content (history restore, Google update): the\n * newest saved snapshot is the content now. Reload it in place; never\n * re-announce. See `announceReplaced`.\n */\n onReplaced?: (snapshotId: string) => void;\n};\n\n/** What `announceReplaced` writes into the room's control map. */\ntype ReplacedSignal = { snapshotId: string; by: string; nonce: string };\n\nconst MUTATIONS_ARRAY_KEY = \"mutations\";\nconst CONTROL_MAP_KEY = \"control\";\nconst REPLACED_KEY = \"replaced\";\n\nexport class WorkbookCollabSession {\n private readonly options: WorkbookCollabSessionOptions;\n private doc: Y.Doc | null = null;\n private awareness: Awareness | null = null;\n private provider: CollabProviderLike | null = null;\n private commandDisposer: { dispose: () => void } | null = null;\n private yArrayObserver:\n ((event: Y.YArrayEvent<CollabMutationInfo>) => void) | null = null;\n private awarenessObserver: (() => void) | null = null;\n private controlObserver: ((event: Y.YMapEvent<unknown>) => void) | null = null;\n /** >0 while this client swaps its own content: its mutations are not edits. */\n private muted = 0;\n private started = false;\n private disposed = false;\n\n constructor(options: WorkbookCollabSessionOptions) {\n this.options = options;\n }\n\n async start(): Promise<void> {\n if (this.started || this.disposed) return;\n this.started = true;\n\n this.doc = new Y.Doc();\n this.awareness = new Awareness(this.doc);\n\n // Seed local awareness with the user identity. Cursor (sheetId/row/col)\n // is set later via setCursor().\n this.awareness.setLocalState({\n uid: this.options.uid,\n clientId: this.options.clientId,\n name: this.options.uid.slice(0, 8),\n color: pickColor(this.options.uid),\n sheetId: null,\n row: null,\n col: null,\n ts: Date.now(),\n });\n\n this.provider = this.options.makeProvider({\n workbookId: this.options.workbookId,\n clientId: this.options.clientId,\n doc: this.doc,\n awareness: this.awareness,\n onLeaderChange: () => {\n if (this.awareness) this.options.onAwarenessChange?.(this.awareness);\n },\n });\n\n // Subscribe to inbound Yjs array changes BEFORE connecting the provider,\n // so the initial-state replay (if any) hits our observer.\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n this.yArrayObserver = (event) => {\n // Local transactions are OUR OWN pushes from handleLocalMutation —\n // Univer already executed those mutations. Re-applying them here would\n // double-apply every local edit on the originator. Only act on\n // transactions that arrived via Y.applyUpdate (origin 'remote').\n if (event.transaction.local) return;\n for (const change of event.changes.delta) {\n if (!change.insert || !Array.isArray(change.insert)) continue;\n for (const op of change.insert as CollabMutationInfo[]) {\n this.applyRemoteMutation(op);\n }\n }\n };\n yArray.observe(this.yArrayObserver as Parameters<typeof yArray.observe>[0]);\n\n // Awareness change passthrough.\n if (this.options.onAwarenessChange) {\n const cb = this.options.onAwarenessChange;\n this.awarenessObserver = () => {\n if (this.awareness) cb(this.awareness);\n };\n this.awareness.on(\"change\", this.awarenessObserver);\n }\n\n // Subscribe to Univer's collab-grain mutation stream.\n this.commandDisposer =\n this.options.commandService.onMutationExecutedForCollab((info) =>\n this.handleLocalMutation(info),\n );\n\n await this.provider.connect();\n await this.provider.ready();\n\n // Whole-content replacement from a peer. Bound AFTER the initial sync: a\n // joiner already loaded the newest snapshot, so the signal it finds in\n // the room's state is history, not news (the editor also skips a\n // snapshot it already shows).\n if (this.disposed || !this.doc) return;\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const seen = (control.get(REPLACED_KEY) as ReplacedSignal | undefined)?.nonce ?? null;\n this.controlObserver = (event) => {\n if (event.transaction.local || !event.keysChanged.has(REPLACED_KEY)) return;\n const signal = control.get(REPLACED_KEY) as ReplacedSignal | undefined;\n if (!signal?.snapshotId || signal.nonce === seen) return;\n this.options.onReplaced?.(signal.snapshotId);\n };\n control.observe(this.controlObserver);\n }\n\n stop(): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.commandDisposer?.dispose();\n this.commandDisposer = null;\n\n if (this.doc && this.yArrayObserver) {\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.unobserve(\n this.yArrayObserver as Parameters<typeof yArray.unobserve>[0],\n );\n this.yArrayObserver = null;\n }\n\n if (this.doc && this.controlObserver) {\n this.doc.getMap<unknown>(CONTROL_MAP_KEY).unobserve(this.controlObserver);\n this.controlObserver = null;\n }\n\n if (this.awareness && this.awarenessObserver) {\n this.awareness.off(\"change\", this.awarenessObserver);\n this.awarenessObserver = null;\n }\n\n this.provider?.disconnect();\n this.provider = null;\n\n if (this.awareness) {\n this.awareness.destroy();\n this.awareness = null;\n }\n\n if (this.doc) {\n this.doc.destroy();\n this.doc = null;\n }\n }\n\n /**\n * How many mutations the shared log holds right now. Read it BEFORE\n * `workbook.save()`: that many entries are guaranteed to be inside the\n * snapshot that save produces.\n */\n logLength(): number {\n return this.doc?.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).length ?? 0;\n }\n\n /**\n * Drop the first `upTo` mutations from the shared log once a snapshot that\n * contains them has been SAVED. Without this a joiner loads the saved\n * workbook (which already has every mutation baked in) and then catches up\n * the whole log on top of it, replaying edits into a workbook that already\n * holds them — Univer refuses with \"Trying to overwrite value at key\"\n * (feedback 88184e53). Only the host calls this; peers receive it as a\n * delete, which the inbound observer ignores (it acts on inserts only).\n */\n compactLog(upTo: number): void {\n if (!this.doc || upTo <= 0) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const n = Math.min(upTo, yArray.length);\n if (n > 0) this.doc.transact(() => yArray.delete(0, n));\n }\n\n /**\n * This client replaced the whole content and SAVED it as `snapshotId`.\n * One transaction empties the shared mutation log (every entry targets the\n * content that is gone — a joiner must never replay them onto the new\n * snapshot) and tells every peer to reload the newest snapshot.\n */\n announceReplaced(snapshotId: string): void {\n if (!this.doc) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const signal: ReplacedSignal = {\n snapshotId,\n by: this.options.clientId,\n nonce: `${this.options.clientId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,\n };\n this.doc.transact(() => {\n if (yArray.length > 0) yArray.delete(0, yArray.length);\n control.set(REPLACED_KEY, signal);\n });\n }\n\n /**\n * Run `swap` (this client tearing its content down and mounting the new\n * one) without broadcasting what Univer emits while it does: those are not\n * edits, and peers reload the same snapshot themselves.\n */\n muteWhile<T>(swap: () => T): T {\n this.muted += 1;\n try {\n return swap();\n } finally {\n this.muted -= 1;\n }\n }\n\n /**\n * Update the local awareness cursor. Call this from Univer's selection\n * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site\n * (~50ms is plenty); the provider broadcasts on its own debounced cycle.\n */\n setCursor(input: {\n sheetId: string | null;\n row: number | null;\n col: number | null;\n }): void {\n if (!this.awareness) return;\n const current = this.awareness.getLocalState() ?? {};\n this.awareness.setLocalState({\n ...current,\n sheetId: input.sheetId,\n row: input.row,\n col: input.col,\n ts: Date.now(),\n });\n }\n\n /**\n * Returns the deterministic host: the connected peer with the lowest uid\n * (lexicographic). Host is the only client that writes canonical snapshots.\n * No election protocol; recompute whenever awareness changes.\n */\n electHost(opts: { tabTieBreak?: boolean } = {}): { isHost: boolean; hostUid: string | null } {\n if (!this.awareness) return { isHost: false, hostUid: null };\n const states = Array.from(this.awareness.getStates().values()) as Array<\n Record<string, unknown>\n >;\n // Lowest uid wins ACROSS tabs; inside this tab only the room's tab leader\n // may be host, because every same-tab holder carries the same uid.\n const leader = this.provider?.isTabLeader?.() ?? true;\n return electCollabHost(states, this.options.uid, this.options.clientId, leader, opts.tabTieBreak ?? false);\n }\n\n // ─── internals ────────────────────────────────────────────────────────\n\n private handleLocalMutation(info: CollabMutationInfo): void {\n if (this.muted > 0) return;\n if (isCellEditorScratch(info)) return;\n // Inbound-applied mutations re-fire this listener because Univer's\n // syncExecuteCommand legitimately processes them through the full\n // pipeline. Skip by sniffing the sentinel we stamped on apply.\n if (\n info.params &&\n typeof info.params === \"object\" &&\n !Array.isArray(info.params) &&\n remoteOriginOf(info.params) !== null\n ) {\n return;\n }\n if (!this.doc) return;\n\n // Normalize params through a JSON round-trip BEFORE pushing into Yjs.\n // Yjs (and the Broadcast transport) only carry JSON-encodable values;\n // Univer types its mutation params as \"serializable\" but JS objects can\n // smuggle Date / Map / class instances through that promise. The\n // round-trip makes the failure mode deterministic: a non-encodable\n // mutation is skipped with a warning instead of corrupting the shared\n // doc or desyncing peers mid-session. What we push here is byte-for-byte\n // what remote peers will apply, so local-apply == remote-apply.\n let safeParams: MutationParams;\n try {\n safeParams =\n info.params === undefined || info.params === null\n ? null\n : (JSON.parse(JSON.stringify(info.params)) as MutationParams);\n } catch (err) {\n console.warn(\n `[collab] skipping non-serializable mutation \"${info.id}\" — peers will not receive it`,\n err,\n );\n return;\n }\n\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);\n }\n\n private applyRemoteMutation(op: CollabMutationInfo): void {\n // Wrap params with the sentinel so handleLocalMutation knows not to\n // echo the apply back into Yjs.\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n [REMOTE_PARAM_TAG]: op.uid ?? true,\n }\n : op.params;\n\n try {\n this.options.commandService.syncExecuteCommand(op.id, taggedParams, {\n onlyLocal: true,\n fromCollab: true,\n });\n } catch (err) {\n // One unappliable op (already baked into the loaded snapshot, or a\n // stale target) must not throw into the realtime channel handler and\n // abort the rest of the catch-up. Said, never swallowed.\n console.warn(\n `[collab] could not apply remote mutation \"${op.id}\" — skipped`,\n err,\n );\n }\n }\n}\n\n// ─── helpers ────────────────────────────────────────────────────────────\n\n/**\n * A workbook holds no Univer DOCUMENT units of its own: the `doc.mutation.*` stream a sheet emits is\n * the in-cell editor's scratch document (typing into a cell). The finished text reaches the sheet as\n * `sheet.mutation.set-range-values`, which does travel. Sending the scratch edits made a joiner replay\n * them against an editor unit it does not have (\"Trying to overwrite value at key\").\n */\nexport function isCellEditorScratch(info: Pick<CollabMutationInfo, \"id\">): boolean {\n return typeof info.id === \"string\" && info.id.startsWith(\"doc.mutation.\");\n}\n\nconst CURSOR_PALETTE = [\n \"#3b82f6\", // blue\n \"#10b981\", // emerald\n \"#f59e0b\", // amber\n \"#ef4444\", // red\n \"#8b5cf6\", // violet\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n];\n\nfunction pickColor(uid: string): string {\n // Deterministic FNV-1a-ish hash so the same user is always the same color\n // across sessions and across other users' screens.\n let h = 2166136261;\n for (let i = 0; i < uid.length; i++) {\n h ^= uid.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? \"#ec4899\";\n}\n","/**\n * useWorkbookRealtime — subscribe to udt_workbook_snapshots inserts for one\n * workbook. Fires whenever a NEW snapshot is committed (any client, including\n * us). The caller passes a callback that receives the new snapshot id and\n * created_by, so it can decide whether to reload (e.g. ignore our own writes\n * via the created_by comparison).\n *\n * NOT a CRDT layer. This is \"last write wins on the snapshot row,\" matching\n * the v1 storage model in `udt_v2_workbook_snapshots.sql`. Real concurrent\n * collab (per-keystroke CRDT updates) is a follow-up phase. *\n * REALTIME: `@ai-matrx/data/realtime` owns the channel (`useChannel`) — dedup, the\n * decoupled ordered handler queue, jittered reconnect, tab sleep, diagnostics.\n * What was here was a raw `.channel(...).subscribe()` with manual teardown and\n * NO catch-up read, which on this surface is the worst version of the bug: a\n * snapshot committed by a collaborator while the tab slept was never seen, so\n * the next local save checkpointed on top of a base that had already moved.\n * `onBackfill` re-reads the newest snapshot and hands it to the same callback,\n * so a recovery is indistinguishable from having received the event.\n *\n * The catch-up read is the host port `readNewestWorkbookSnapshot` (the package owns no database client).\n *\n * Echo suppression is deliberately NOT relied on here: these are INSERTs of\n * new rows this client never registers on the write ledger, and the caller's\n * `created_by` comparison is the contract — every insert is delivered.\n */\n\"use client\";\n\nimport { useCallback } from \"react\";\n\nimport { defineChannelNamespace } from \"@ai-matrx/data/realtime\";\nimport { useChannel } from \"@ai-matrx/data/realtime/react\";\nimport { getUniverHost } from \"../ports\";\n\nexport type WorkbookRealtimeEvent = {\n snapshotId: string;\n createdBy: string | null;\n createdAt: string;\n};\n\n/** One place names this channel. A second, different declaration throws. */\nconst workbookSnapshotsChannel = defineChannelNamespace({\n namespace: \"udt-workbook-snapshots\",\n parts: [\"workbookId\"],\n description: \"workbench.udt_workbook_snapshots inserts for one workbook\",\n});\n\nexport type SnapshotRow = {\n id?: string;\n created_by?: string | null;\n created_at?: string;\n};\n\nexport function useWorkbookRealtime(\n workbookId: string | null | undefined,\n onSnapshot: (event: WorkbookRealtimeEvent) => void,\n options?: { enabled?: boolean },\n) {\n const enabled = options?.enabled ?? true;\n\n const emit = useCallback(\n (row: SnapshotRow | null) => {\n if (!row?.id) return;\n onSnapshot({\n snapshotId: row.id,\n // component-created-by-ok: append-only snapshot with NO updated_by column and NO parent-rewrite trigger — created_by is the author the client wrote at insert (the only person field this table has)\n createdBy: row.created_by ?? null,\n createdAt: row.created_at ?? new Date().toISOString(),\n });\n },\n [onSnapshot],\n );\n\n useChannel(\n enabled && workbookId\n ? {\n topic: workbookSnapshotsChannel.topic({ workbookId }),\n postgresChanges: [\n {\n event: \"INSERT\",\n schema: \"workbench\",\n table: \"udt_workbook_snapshots\",\n filter: `workbook_id=eq.${workbookId}`,\n rowId: (row) => (typeof row.id === \"string\" ? row.id : undefined),\n onChange: ({ row }) => emit(row as SnapshotRow | null),\n },\n ],\n // Realtime has no replay. Re-read the newest snapshot so a reconnect,\n // tab wake or network restore cannot leave this client checkpointing\n // on top of a base that moved while it was away.\n onBackfill: async () => {\n let data: SnapshotRow | null;\n let error: { message: string } | null;\n try {\n ({ data, error } = await getUniverHost().readNewestWorkbookSnapshot(workbookId));\n } catch (e) {\n data = null;\n error = { message: e instanceof Error ? e.message : String(e) };\n }\n if (error) {\n // Not silent: without this read the caller is working from a\n // base it cannot know is stale.\n console.warn(\n \"[udt-workbook RT] catch-up read failed — this workbook may be \" +\n \"working from a stale snapshot base; reload before saving.\",\n error.message,\n );\n return;\n }\n emit(data);\n },\n }\n : null,\n );\n}\n","/**\n * The host ports of the Univer glue.\n *\n * The glue belongs to the package, but four things belong to the app that mounts an editor: the\n * toast path, the signed-in person, the file store, the snapshot catch-up read and the painted theme. The host states them\n * once at start-up with `configureUniverHost`; the glue reads them here. State sits on a\n * `globalThis` slot so a dual-bundled copy of the package still sees one host (dual-bundle rule).\n * An unconfigured port THROWS naming the port (nothing fails silently).\n */\n\nexport type UniverToastId = string | number;\ntype ToastShow = (title: string, options?: { description?: string; duration?: number }) => UniverToastId;\n\nexport interface UniverHostPorts {\n /** The app's toast path (one toaster per page). */\n toast: {\n success: ToastShow;\n info: ToastShow;\n warning: ToastShow;\n error: ToastShow;\n message: ToastShow;\n dismiss(id: UniverToastId): void;\n };\n /** Who is typing, or null when signed out. `metadata` is the auth user's metadata bag. */\n getSignedInUser(): Promise<{ id: string; email?: string | null; metadata?: Record<string, unknown> | null } | null>;\n /** The file store behind pictures on a sheet or in a document. */\n files: {\n upload(file: File): Promise<{ fileId: string }>;\n getBlob(fileId: string): Promise<Blob>;\n };\n /** The newest snapshot row of a workbook (the realtime catch-up read), or `data: null` when it has none. */\n readNewestWorkbookSnapshot(\n workbookId: string,\n ): Promise<{ data: { id?: string; created_by?: string | null; created_at?: string } | null; error: { message: string } | null }>;\n /** Tell the person a picture did not upload. */\n notifyUploadError(error: unknown): void;\n /** The painted colour mode, as a React hook (re-renders on a switch). */\n useThemeMode(): \"light\" | \"dark\";\n}\n\nconst SLOT = Symbol.for(\"ai-matrx.office.univer-host\");\ntype Slot = { ports?: UniverHostPorts };\nconst slot = (): Slot => {\n const g = globalThis as Record<symbol, Slot | undefined>;\n return (g[SLOT] ??= {});\n};\n\nexport function configureUniverHost(ports: UniverHostPorts): void {\n slot().ports = ports;\n}\n\nexport function getUniverHost(): UniverHostPorts {\n const ports = slot().ports;\n if (!ports) throw new Error(\"@ai-matrx/office/univer: configureUniverHost(ports) was not called before an editor mounted.\");\n return ports;\n}\n"],"mappings":"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","names":["import_react"]}
@@ -0,0 +1,156 @@
1
+ import * as Y from 'yjs';
2
+ import { Awareness } from 'y-protocols/awareness';
3
+
4
+ /**
5
+ * Structural transport contract. `SupabaseYjsProvider` satisfies it in
6
+ * production; the verification harness injects an in-memory loopback
7
+ * transport through the same seam. Keeping this structural (not the concrete
8
+ * class) is what makes the session testable without network.
9
+ */
10
+ type CollabProviderLike = {
11
+ connect(): Promise<void>;
12
+ disconnect(): void;
13
+ ready(): Promise<void>;
14
+ /**
15
+ * True when this is the only holder of its room in this tab allowed to act as
16
+ * host (autosave). Same-tab holders share one client id and one uid, so
17
+ * awareness cannot tell them apart. Absent = a lone holder = leader.
18
+ */
19
+ isTabLeader?(): boolean;
20
+ };
21
+ type MutationParams = Record<string, unknown> | unknown[] | null;
22
+ type CollabMutationInfo = {
23
+ id: string;
24
+ type: number;
25
+ params: MutationParams;
26
+ /** Who made it (the person's uid), so a peer can tell its own other tab's edit from a stranger's. */
27
+ uid?: string;
28
+ };
29
+ type ExecuteOptions = {
30
+ /** Per-Univer convention: when true, the command runs locally only and is
31
+ * NOT broadcast to other plugins. We set this on inbound-apply so the
32
+ * subsequent onMutationExecutedForCollab does NOT re-fire on our peer. */
33
+ onlyLocal?: boolean;
34
+ fromCollab?: boolean;
35
+ };
36
+ type CommandServiceLike = {
37
+ onMutationExecutedForCollab(listener: (info: CollabMutationInfo, options?: ExecuteOptions) => void): {
38
+ dispose: () => void;
39
+ };
40
+ syncExecuteCommand(id: string, params: MutationParams, options?: ExecuteOptions): unknown;
41
+ };
42
+ type WorkbookCollabSessionOptions = {
43
+ workbookId: string;
44
+ uid: string;
45
+ clientId: string;
46
+ commandService: CommandServiceLike;
47
+ /** Factory so this file does not statically depend on the provider. */
48
+ makeProvider: (args: {
49
+ workbookId: string;
50
+ clientId: string;
51
+ doc: Y.Doc;
52
+ awareness: Awareness;
53
+ /** Wire to the provider so a host handover re-runs the election. */
54
+ onLeaderChange: () => void;
55
+ }) => CollabProviderLike;
56
+ /** Called when the awareness set changes (peers join / leave / move cursor). */
57
+ onAwarenessChange?: (awareness: Awareness) => void;
58
+ /**
59
+ * A peer replaced the whole content (history restore, Google update): the
60
+ * newest saved snapshot is the content now. Reload it in place; never
61
+ * re-announce. See `announceReplaced`.
62
+ */
63
+ onReplaced?: (snapshotId: string) => void;
64
+ };
65
+ declare class WorkbookCollabSession {
66
+ private readonly options;
67
+ private doc;
68
+ private awareness;
69
+ private provider;
70
+ private commandDisposer;
71
+ private yArrayObserver;
72
+ private awarenessObserver;
73
+ private controlObserver;
74
+ /** >0 while this client swaps its own content: its mutations are not edits. */
75
+ private muted;
76
+ private started;
77
+ private disposed;
78
+ constructor(options: WorkbookCollabSessionOptions);
79
+ start(): Promise<void>;
80
+ stop(): void;
81
+ /**
82
+ * How many mutations the shared log holds right now. Read it BEFORE
83
+ * `workbook.save()`: that many entries are guaranteed to be inside the
84
+ * snapshot that save produces.
85
+ */
86
+ logLength(): number;
87
+ /**
88
+ * Drop the first `upTo` mutations from the shared log once a snapshot that
89
+ * contains them has been SAVED. Without this a joiner loads the saved
90
+ * workbook (which already has every mutation baked in) and then catches up
91
+ * the whole log on top of it, replaying edits into a workbook that already
92
+ * holds them — Univer refuses with "Trying to overwrite value at key"
93
+ * (feedback 88184e53). Only the host calls this; peers receive it as a
94
+ * delete, which the inbound observer ignores (it acts on inserts only).
95
+ */
96
+ compactLog(upTo: number): void;
97
+ /**
98
+ * This client replaced the whole content and SAVED it as `snapshotId`.
99
+ * One transaction empties the shared mutation log (every entry targets the
100
+ * content that is gone — a joiner must never replay them onto the new
101
+ * snapshot) and tells every peer to reload the newest snapshot.
102
+ */
103
+ announceReplaced(snapshotId: string): void;
104
+ /**
105
+ * Run `swap` (this client tearing its content down and mounting the new
106
+ * one) without broadcasting what Univer emits while it does: those are not
107
+ * edits, and peers reload the same snapshot themselves.
108
+ */
109
+ muteWhile<T>(swap: () => T): T;
110
+ /**
111
+ * Update the local awareness cursor. Call this from Univer's selection
112
+ * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site
113
+ * (~50ms is plenty); the provider broadcasts on its own debounced cycle.
114
+ */
115
+ setCursor(input: {
116
+ sheetId: string | null;
117
+ row: number | null;
118
+ col: number | null;
119
+ }): void;
120
+ /**
121
+ * Returns the deterministic host: the connected peer with the lowest uid
122
+ * (lexicographic). Host is the only client that writes canonical snapshots.
123
+ * No election protocol; recompute whenever awareness changes.
124
+ */
125
+ electHost(opts?: {
126
+ tabTieBreak?: boolean;
127
+ }): {
128
+ isHost: boolean;
129
+ hostUid: string | null;
130
+ };
131
+ private handleLocalMutation;
132
+ private applyRemoteMutation;
133
+ }
134
+ /**
135
+ * A workbook holds no Univer DOCUMENT units of its own: the `doc.mutation.*` stream a sheet emits is
136
+ * the in-cell editor's scratch document (typing into a cell). The finished text reaches the sheet as
137
+ * `sheet.mutation.set-range-values`, which does travel. Sending the scratch edits made a joiner replay
138
+ * them against an editor unit it does not have ("Trying to overwrite value at key").
139
+ */
140
+ declare function isCellEditorScratch(info: Pick<CollabMutationInfo, "id">): boolean;
141
+
142
+ type WorkbookRealtimeEvent = {
143
+ snapshotId: string;
144
+ createdBy: string | null;
145
+ createdAt: string;
146
+ };
147
+ type SnapshotRow = {
148
+ id?: string;
149
+ created_by?: string | null;
150
+ created_at?: string;
151
+ };
152
+ declare function useWorkbookRealtime(workbookId: string | null | undefined, onSnapshot: (event: WorkbookRealtimeEvent) => void, options?: {
153
+ enabled?: boolean;
154
+ }): void;
155
+
156
+ export { type CollabMutationInfo, type CollabProviderLike, type CommandServiceLike, type SnapshotRow, WorkbookCollabSession, type WorkbookCollabSessionOptions, type WorkbookRealtimeEvent, isCellEditorScratch, useWorkbookRealtime };