@ai-matrx/office 0.5.3 → 0.6.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.
@@ -4,9 +4,28 @@
4
4
  import * as Y from "yjs";
5
5
  import { Awareness } from "y-protocols/awareness";
6
6
  import { electCollabHost, remoteOriginOf, REMOTE_PARAM_TAG } from "@ai-matrx/data/realtime/collab";
7
+
8
+ // src/univer/collab/covered.ts
9
+ var COLLAB_COVERED_KEY = "__matrxCollabCovered";
10
+ function stampCovered(snapshot, covered) {
11
+ return { ...snapshot, [COLLAB_COVERED_KEY]: [...covered] };
12
+ }
13
+ function takeCovered(snapshot) {
14
+ if (!snapshot || typeof snapshot !== "object" || !(COLLAB_COVERED_KEY in snapshot)) {
15
+ return { snapshot, covered: /* @__PURE__ */ new Set() };
16
+ }
17
+ const { [COLLAB_COVERED_KEY]: list, ...rest } = snapshot;
18
+ return {
19
+ snapshot: rest,
20
+ covered: new Set(Array.isArray(list) ? list.filter((e) => typeof e === "string") : [])
21
+ };
22
+ }
23
+
24
+ // src/univer/collab/WorkbookCollabSession.ts
7
25
  var MUTATIONS_ARRAY_KEY = "mutations";
8
26
  var CONTROL_MAP_KEY = "control";
9
27
  var REPLACED_KEY = "replaced";
28
+ var CHECKPOINT_KEY = "checkpoint";
10
29
  var WorkbookCollabSession = class {
11
30
  options;
12
31
  doc = null;
@@ -20,6 +39,15 @@ var WorkbookCollabSession = class {
20
39
  muted = 0;
21
40
  started = false;
22
41
  disposed = false;
42
+ /** Room entries this seat holds in its content: applied from a peer, or made here. */
43
+ applied = /* @__PURE__ */ new Set();
44
+ /** Entries without an id (a peer on an older build) are applied once, by identity. */
45
+ appliedRefs = /* @__PURE__ */ new WeakSet();
46
+ seq = 0;
47
+ /** False until this seat has seen the room's content once (the join-time check runs then). */
48
+ joined = false;
49
+ /** The base is being reloaded: entries wait in the log until it is shown. */
50
+ reloadingBase = false;
23
51
  constructor(options) {
24
52
  this.options = options;
25
53
  }
@@ -50,12 +78,8 @@ var WorkbookCollabSession = class {
50
78
  const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
51
79
  this.yArrayObserver = (event) => {
52
80
  if (event.transaction.local) return;
53
- for (const change of event.changes.delta) {
54
- if (!change.insert || !Array.isArray(change.insert)) continue;
55
- for (const op of change.insert) {
56
- this.applyRemoteMutation(op);
57
- }
58
- }
81
+ if (!event.changes.delta.some((change) => Array.isArray(change.insert) && change.insert.length > 0)) return;
82
+ this.drain();
59
83
  };
60
84
  yArray.observe(this.yArrayObserver);
61
85
  if (this.options.onAwarenessChange) {
@@ -70,6 +94,7 @@ var WorkbookCollabSession = class {
70
94
  );
71
95
  await this.provider.connect();
72
96
  await this.provider.ready();
97
+ this.drain();
73
98
  if (this.disposed || !this.doc) return;
74
99
  const control = this.doc.getMap(CONTROL_MAP_KEY);
75
100
  const seen = control.get(REPLACED_KEY)?.nonce ?? null;
@@ -112,28 +137,44 @@ var WorkbookCollabSession = class {
112
137
  this.doc = null;
113
138
  }
114
139
  }
115
- /**
116
- * How many mutations the shared log holds right now. Read it BEFORE
117
- * `workbook.save()`: that many entries are guaranteed to be inside the
118
- * snapshot that save produces.
119
- */
140
+ /** How many mutations the shared log holds right now. */
120
141
  logLength() {
121
142
  return this.doc?.getArray(MUTATIONS_ARRAY_KEY).length ?? 0;
122
143
  }
123
144
  /**
124
- * Drop the first `upTo` mutations from the shared log once a snapshot that
125
- * contains them has been SAVED. Without this a joiner loads the saved
126
- * workbook (which already has every mutation baked in) and then catches up
127
- * the whole log on top of it, replaying edits into a workbook that already
128
- * holds them — Univer refuses with "Trying to overwrite value at key"
129
- * (feedback 88184e53). Only the host calls this; peers receive it as a
130
- * delete, which the inbound observer ignores (it acts on inserts only).
145
+ * The room entries this seat's content holds RIGHT NOW. Read it in the same synchronous step as the
146
+ * content you are about to save, and save it with that content (`stampCovered`); after the save lands,
147
+ * pass it to `checkpoint`.
148
+ */
149
+ coveredNow() {
150
+ if (!this.doc) return [];
151
+ const covered = this.options.base?.covered();
152
+ const out = [];
153
+ for (const op of this.doc.getArray(MUTATIONS_ARRAY_KEY).toArray()) {
154
+ if (op.eid && (this.applied.has(op.eid) || covered?.has(op.eid))) out.push(op.eid);
155
+ }
156
+ return out;
157
+ }
158
+ /**
159
+ * The snapshot `snapshotId` was SAVED holding `covered` (from `coveredNow`): those entries leave the
160
+ * shared log, and the room records the snapshot as its newest checkpoint. Removal is by entry id, never
161
+ * by position — an entry that arrived while the save was in flight is not in the snapshot and stays.
162
+ * Only the elected saver calls this; peers receive it as a delete (never replayed).
131
163
  */
132
- compactLog(upTo) {
133
- if (!this.doc || upTo <= 0) return;
164
+ checkpoint(snapshotId, covered) {
165
+ if (!this.doc) return;
134
166
  const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
135
- const n = Math.min(upTo, yArray.length);
136
- if (n > 0) this.doc.transact(() => yArray.delete(0, n));
167
+ const control = this.doc.getMap(CONTROL_MAP_KEY);
168
+ const drop = new Set(covered);
169
+ const signal = { snapshotId, by: this.options.clientId };
170
+ this.doc.transact(() => {
171
+ const entries = yArray.toArray();
172
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
173
+ const eid = entries[i]?.eid;
174
+ if (eid && drop.has(eid)) yArray.delete(i, 1);
175
+ }
176
+ control.set(CHECKPOINT_KEY, signal);
177
+ });
137
178
  }
138
179
  /**
139
180
  * This client replaced the whole content and SAVED it as `snapshotId`.
@@ -153,6 +194,7 @@ var WorkbookCollabSession = class {
153
194
  this.doc.transact(() => {
154
195
  if (yArray.length > 0) yArray.delete(0, yArray.length);
155
196
  control.set(REPLACED_KEY, signal);
197
+ control.set(CHECKPOINT_KEY, { snapshotId, by: this.options.clientId });
156
198
  });
157
199
  }
158
200
  /**
@@ -214,7 +256,49 @@ var WorkbookCollabSession = class {
214
256
  return;
215
257
  }
216
258
  const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
217
- yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);
259
+ const eid = `${this.options.clientId}:${++this.seq}`;
260
+ this.applied.add(eid);
261
+ this.joined = true;
262
+ yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid, eid }]);
263
+ }
264
+ /**
265
+ * Apply every log entry this seat's content does not hold yet, in the log's order. The first time the
266
+ * room shows content, the join-time check runs: a seat that booted from a snapshot OLDER than the
267
+ * room's newest checkpoint reloads first (the entries that checkpoint trimmed exist only in it).
268
+ */
269
+ drain() {
270
+ if (!this.doc || this.disposed || this.reloadingBase) return;
271
+ const yArray = this.doc.getArray(MUTATIONS_ARRAY_KEY);
272
+ const entries = yArray.toArray();
273
+ const base = this.options.base;
274
+ if (!this.joined) {
275
+ const control = this.doc.getMap(CONTROL_MAP_KEY);
276
+ const checkpoint = control.get(CHECKPOINT_KEY);
277
+ if (entries.length === 0 && !checkpoint) return;
278
+ this.joined = true;
279
+ if (base && checkpoint?.snapshotId && checkpoint.snapshotId !== base.snapshotId()) {
280
+ this.reloadingBase = true;
281
+ void base.reload().catch((error) => {
282
+ console.warn("[collab] could not load the newest saved version on joining", error);
283
+ this.options.onApplyFailed?.("collab.join", error);
284
+ }).finally(() => {
285
+ this.reloadingBase = false;
286
+ this.drain();
287
+ });
288
+ return;
289
+ }
290
+ }
291
+ const covered = base?.covered();
292
+ for (const op of entries) {
293
+ if (op.eid) {
294
+ if (this.applied.has(op.eid) || covered?.has(op.eid)) continue;
295
+ this.applied.add(op.eid);
296
+ } else {
297
+ if (this.appliedRefs.has(op)) continue;
298
+ this.appliedRefs.add(op);
299
+ }
300
+ this.applyRemoteMutation(op);
301
+ }
218
302
  }
219
303
  applyRemoteMutation(op) {
220
304
  const ownUnit = this.options.unitId?.() ?? null;
@@ -267,7 +351,10 @@ function pickColor(uid) {
267
351
  return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? "#ec4899";
268
352
  }
269
353
  export {
354
+ COLLAB_COVERED_KEY,
270
355
  WorkbookCollabSession,
271
- isCellEditorScratch
356
+ isCellEditorScratch,
357
+ stampCovered,
358
+ takeCovered
272
359
  };
273
360
  //# sourceMappingURL=univer-collab.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/univer/collab/WorkbookCollabSession.ts"],"sourcesContent":["/**\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 * TRUE when the room is bound to a Univer DOCUMENT unit (the Docs editor): there the `doc.mutation.*`\n * stream IS the content, so it must travel. Leave unset for a workbook, where `doc.mutation.*` is the\n * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).\n */\n forwardDocumentMutations?: boolean;\n /**\n * The Univer unit id this seat mounted. Each seat can mount its own unit, so a peer's mutation\n * (addressed to the SENDER's unit) is re-addressed to this one before it is applied.\n */\n unitId?: () => string | null;\n /** A peer's mutation could not be applied here: the seat says so and re-syncs; never only a console line. */\n onApplyFailed?: (mutationId: string, error: unknown) => 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 (!this.options.forwardDocumentMutations && 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 ownUnit = this.options.unitId?.() ?? null;\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n ...(ownUnit && typeof (op.params as { unitId?: unknown }).unitId === \"string\" ? { unitId: ownUnit } : {}),\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 this.options.onApplyFailed?.(op.id, err);\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/univer/collab/WorkbookCollabSession.ts","../src/univer/collab/covered.ts"],"sourcesContent":["/**\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\nexport { COLLAB_COVERED_KEY, stampCovered, takeCovered } from \"./covered\";\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 * The entry's identity in the room (`<clientId>:<seq>`). A saved snapshot names the entries it holds\n * (`stampCovered`), so a seat that boots from that snapshot never replays them a second time.\n */\n eid?: string;\n};\n\n/**\n * What a seat booted from: the saved snapshot's row id and the room entries that snapshot already holds.\n * The session never replays a covered entry, and a seat that booted from an OLDER snapshot than the\n * room's newest checkpoint first reloads (the entries that checkpoint trimmed are only in that snapshot).\n */\nexport type CollabBase = {\n snapshotId: () => string | null;\n covered: () => ReadonlySet<string>;\n /** Show the newest saved snapshot (and make it the base). */\n reload: () => Promise<void>;\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 * TRUE when the room is bound to a Univer DOCUMENT unit (the Docs editor): there the `doc.mutation.*`\n * stream IS the content, so it must travel. Leave unset for a workbook, where `doc.mutation.*` is the\n * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).\n */\n forwardDocumentMutations?: boolean;\n /**\n * The Univer unit id this seat mounted. Each seat can mount its own unit, so a peer's mutation\n * (addressed to the SENDER's unit) is re-addressed to this one before it is applied.\n */\n unitId?: () => string | null;\n /** A peer's mutation could not be applied here: the seat says so and re-syncs; never only a console line. */\n onApplyFailed?: (mutationId: string, error: unknown) => void;\n /**\n * The saved snapshot this seat booted from. Without it a reloading seat replays every entry still in\n * the room's log on top of a snapshot that already holds them: the text shows twice, and the next save\n * stores it twice (2026-10-16).\n */\n base?: CollabBase;\n};\n\n/** What `checkpoint` writes into the room's control map: the newest saved snapshot whose entries left the log. */\ntype CheckpointSignal = { snapshotId: string; by: string };\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\";\nconst CHECKPOINT_KEY = \"checkpoint\";\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 /** Room entries this seat holds in its content: applied from a peer, or made here. */\n private readonly applied = new Set<string>();\n /** Entries without an id (a peer on an older build) are applied once, by identity. */\n private readonly appliedRefs = new WeakSet<object>();\n private seq = 0;\n /** False until this seat has seen the room's content once (the join-time check runs then). */\n private joined = false;\n /** The base is being reloaded: entries wait in the log until it is shown. */\n private reloadingBase = 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 if (!event.changes.delta.some((change) => Array.isArray(change.insert) && change.insert.length > 0)) return;\n this.drain();\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 this.drain();\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 /** How many mutations the shared log holds right now. */\n logLength(): number {\n return this.doc?.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).length ?? 0;\n }\n\n /**\n * The room entries this seat's content holds RIGHT NOW. Read it in the same synchronous step as the\n * content you are about to save, and save it with that content (`stampCovered`); after the save lands,\n * pass it to `checkpoint`.\n */\n coveredNow(): string[] {\n if (!this.doc) return [];\n const covered = this.options.base?.covered();\n const out: string[] = [];\n for (const op of this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).toArray()) {\n if (op.eid && (this.applied.has(op.eid) || covered?.has(op.eid))) out.push(op.eid);\n }\n return out;\n }\n\n /**\n * The snapshot `snapshotId` was SAVED holding `covered` (from `coveredNow`): those entries leave the\n * shared log, and the room records the snapshot as its newest checkpoint. Removal is by entry id, never\n * by position — an entry that arrived while the save was in flight is not in the snapshot and stays.\n * Only the elected saver calls this; peers receive it as a delete (never replayed).\n */\n checkpoint(snapshotId: string, covered: readonly 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 drop = new Set(covered);\n const signal: CheckpointSignal = { snapshotId, by: this.options.clientId };\n this.doc.transact(() => {\n const entries = yArray.toArray();\n for (let i = entries.length - 1; i >= 0; i -= 1) {\n const eid = entries[i]?.eid;\n if (eid && drop.has(eid)) yArray.delete(i, 1);\n }\n control.set(CHECKPOINT_KEY, signal);\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 control.set(CHECKPOINT_KEY, { snapshotId, by: this.options.clientId } satisfies CheckpointSignal);\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 (!this.options.forwardDocumentMutations && 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 const eid = `${this.options.clientId}:${++this.seq}`;\n this.applied.add(eid);\n // An edit made here is made on what this seat shows: the join-time check is over.\n this.joined = true;\n yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid, eid }]);\n }\n\n /**\n * Apply every log entry this seat's content does not hold yet, in the log's order. The first time the\n * room shows content, the join-time check runs: a seat that booted from a snapshot OLDER than the\n * room's newest checkpoint reloads first (the entries that checkpoint trimmed exist only in it).\n */\n private drain(): void {\n if (!this.doc || this.disposed || this.reloadingBase) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const entries = yArray.toArray();\n const base = this.options.base;\n if (!this.joined) {\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const checkpoint = control.get(CHECKPOINT_KEY) as CheckpointSignal | undefined;\n if (entries.length === 0 && !checkpoint) return;\n this.joined = true;\n if (base && checkpoint?.snapshotId && checkpoint.snapshotId !== base.snapshotId()) {\n this.reloadingBase = true;\n void base\n .reload()\n .catch((error: unknown) => {\n console.warn(\"[collab] could not load the newest saved version on joining\", error);\n this.options.onApplyFailed?.(\"collab.join\", error);\n })\n .finally(() => {\n this.reloadingBase = false;\n this.drain();\n });\n return;\n }\n }\n const covered = base?.covered();\n for (const op of entries) {\n if (op.eid) {\n if (this.applied.has(op.eid) || covered?.has(op.eid)) continue;\n this.applied.add(op.eid);\n } else {\n if (this.appliedRefs.has(op)) continue;\n this.appliedRefs.add(op);\n }\n this.applyRemoteMutation(op);\n }\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 ownUnit = this.options.unitId?.() ?? null;\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n ...(ownUnit && typeof (op.params as { unitId?: unknown }).unitId === \"string\" ? { unitId: ownUnit } : {}),\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 this.options.onApplyFailed?.(op.id, err);\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 * A saved snapshot names the collab-room entries it already holds, so a seat that boots from it never\n * replays them (2026-10-16: a reload showed the text twice). Pure — no Yjs — so the editors' model can\n * stamp and read it without loading the room.\n */\n\n/** The key a saved snapshot carries its covered room entries under (`stampCovered` / `takeCovered`). */\nexport const COLLAB_COVERED_KEY = \"__matrxCollabCovered\";\n\n/** The snapshot to SAVE: `snapshot` plus the room entries it holds (`WorkbookCollabSession.coveredNow`). */\nexport function stampCovered<T extends object>(snapshot: T, covered: readonly string[]): T {\n return { ...snapshot, [COLLAB_COVERED_KEY]: [...covered] };\n}\n\n/** A LOADED snapshot split into the content to mount and the room entries it already holds. */\nexport function takeCovered<T>(snapshot: T): { snapshot: T; covered: Set<string> } {\n if (!snapshot || typeof snapshot !== \"object\" || !(COLLAB_COVERED_KEY in (snapshot as object))) {\n return { snapshot, covered: new Set() };\n }\n const { [COLLAB_COVERED_KEY]: list, ...rest } = snapshot as Record<string, unknown>;\n return {\n snapshot: rest as T,\n covered: new Set(Array.isArray(list) ? list.filter((e): e is string => typeof e === \"string\") : []),\n };\n}\n"],"mappings":"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","names":[]}
package/dist/univer.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/univer/index.ts
22
22
  var univer_exports = {};
23
23
  __export(univer_exports, {
24
+ COLLAB_COVERED_KEY: () => COLLAB_COVERED_KEY,
24
25
  DOCUMENT_HISTORY_KIND: () => DOCUMENT_HISTORY_KIND,
25
26
  MATRX_IMAGE_PREFIX: () => MATRX_IMAGE_PREFIX,
26
27
  MISSING_UNIVER_ICONS: () => MISSING_UNIVER_ICONS,
@@ -47,7 +48,9 @@ __export(univer_exports, {
47
48
  routeUniverNotificationsToApp: () => routeUniverNotificationsToApp,
48
49
  showSavedSnapshot: () => showSavedSnapshot,
49
50
  showUniverNotification: () => showUniverNotification,
51
+ stampCovered: () => stampCovered,
50
52
  swapContentGuarded: () => swapContentGuarded,
53
+ takeCovered: () => takeCovered,
51
54
  univerOwnerId: () => univerOwnerId,
52
55
  uploadImportedImage: () => uploadImportedImage,
53
56
  useUniverDarkModeSync: () => useUniverDarkModeSync,
@@ -330,4 +333,20 @@ function useUniverDarkModeSync(apiRef, ready) {
330
333
  apiRef.current.toggleDarkMode(mode === "dark");
331
334
  }, [apiRef, ready, mode]);
332
335
  }
336
+
337
+ // src/univer/collab/covered.ts
338
+ var COLLAB_COVERED_KEY = "__matrxCollabCovered";
339
+ function stampCovered(snapshot, covered) {
340
+ return { ...snapshot, [COLLAB_COVERED_KEY]: [...covered] };
341
+ }
342
+ function takeCovered(snapshot) {
343
+ if (!snapshot || typeof snapshot !== "object" || !(COLLAB_COVERED_KEY in snapshot)) {
344
+ return { snapshot, covered: /* @__PURE__ */ new Set() };
345
+ }
346
+ const { [COLLAB_COVERED_KEY]: list, ...rest } = snapshot;
347
+ return {
348
+ snapshot: rest,
349
+ covered: new Set(Array.isArray(list) ? list.filter((e) => typeof e === "string") : [])
350
+ };
351
+ }
333
352
  //# sourceMappingURL=univer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/univer/index.ts","../src/univer/ports.ts","../src/univer/history.ts","../src/univer/univerImageStore.ts","../src/univer/disposeUniverInstance.ts","../src/univer/identifyUniverUser.ts","../src/univer/isSnapshotMutation.ts","../src/univer/matrxImagePorts.ts","../src/univer/openEditorContent.ts","../src/univer/registerMissingUniverIcons.ts","../src/univer/registerUniverFacadeDependencies.ts","../src/univer/routeUniverNotificationsToApp.ts","../src/univer/univer-theme-token-color.ts","../src/univer/useUniverDarkModeSync.ts"],"sourcesContent":["/**\n * The Univer glue shared by `/sheets` and `/docs` (was matrx-frontend `lib/univer`). A transitional\n * subpath: matrx-frontend's two editors import it until they move into their sections, then this\n * entry goes internal again (BUILD-BOOK §1.3, Amendments W1-G2a1). Importing it loads `@univerjs/*`;\n * the pure history keys are their own entry (`./univer/history`) for code that must not.\n */\nexport * from \"./ports\";\nexport * from \"./history\";\nexport * from \"./disposeUniverInstance\";\nexport * from \"./identifyUniverUser\";\nexport * from \"./isSnapshotMutation\";\nexport * from \"./matrxImagePorts\";\nexport * from \"./openEditorContent\";\nexport * from \"./registerMissingUniverIcons\";\nexport * from \"./registerUniverFacadeDependencies\";\nexport * from \"./routeUniverNotificationsToApp\";\nexport * from \"./univer-theme-token-color\";\nexport * from \"./univerImageStore\";\nexport * from \"./useUniverDarkModeSync\";\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","/**\n * A document's or a workbook's snapshot history as a canvas tab — the keys and toggle inputs the\n * editors use, and the reader the two history views use. One tab per document (`document-history`,\n * keyed by the document id) and per workbook (`workbook-history`, keyed by the workbook id).\n *\n * Pure: no React, no canvas package, no Univer. The two `defineCanvasKind` registrations stay with\n * the host app (matrx-frontend `features/canvas/host`) and lazy-load the views, so this module never\n * imports a view (the cycle the old `lib/univer/historyKinds.ts` closed).\n */\n\nexport const DOCUMENT_HISTORY_KIND = \"document-history\";\nexport const WORKBOOK_HISTORY_KIND = \"workbook-history\";\n\nexport interface HistoryTabData {\n id: string;\n editable: boolean;\n}\n\n/** Structurally the host's `ToolToggleInput` (the canvas toggle the editor's History button presses). */\nexport interface HistoryToggleInput {\n kind: string;\n key: string;\n title: string;\n data: { id: string; editable: boolean };\n followTitle: true;\n}\n\n/** A tab's data as a history subject, or null when it names no subject. */\nexport function readHistoryTab(data: unknown): HistoryTabData | null {\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) return null;\n const record = data as Record<string, unknown>;\n const id = typeof record.id === \"string\" && record.id ? record.id : null;\n if (!id) return null;\n return { id, editable: record.editable !== false };\n}\n\n/** \"<what> · <subject>\", or just \"<what>\" while the subject has no name. */\nfunction subjectTitle(what: string, subject: string | null | undefined): string {\n const name = subject?.trim();\n return name ? `${what} · ${name}` : what;\n}\n\n// The tab names its subject — two open history tabs never share a title, and\n// the editor that owns the toggle keeps it in step with a rename.\nexport function documentHistoryToggleInput(documentId: string, editable: boolean, documentName?: string | null): HistoryToggleInput {\n return {\n kind: DOCUMENT_HISTORY_KIND,\n key: documentId,\n title: subjectTitle(\"Document history\", documentName),\n data: { id: documentId, editable },\n followTitle: true,\n };\n}\n\nexport function workbookHistoryToggleInput(workbookId: string, editable: boolean, workbookName?: string | null): HistoryToggleInput {\n return {\n kind: WORKBOOK_HISTORY_KIND,\n key: workbookId,\n title: subjectTitle(\"Workbook history\", workbookName),\n data: { id: workbookId, editable },\n followTitle: true,\n };\n}\n","/**\n * univerImageStore — images on a sheet or in a document live in OUR file\n * store; the Univer snapshot holds only a reference (`matrx-file:<fileId>`).\n *\n * Why: Univer's stock image service inlines every picture as base64 into the\n * snapshot, so one 2 MB photo made every autosave, history row and collab\n * mutation ~2 MB. Here `saveImage` uploads the file and hands Univer a URL-type\n * source; Univer's URL-image downloader turns the reference back into bytes\n * (`getBlob` → object URL) when it paints. Images already inline (BASE64\n * sources) are untouched and keep rendering.\n */\n\nimport {\n IImageIoService,\n IURLImageService,\n ImageSourceType,\n ImageUploadStatusType,\n generateRandomId,\n} from \"@univerjs/core\";\n\nexport const MATRX_IMAGE_PREFIX = \"matrx-file:\";\n\nexport const isMatrxImageSource = (source: string): boolean =>\n source.startsWith(MATRX_IMAGE_PREFIX);\n\nexport const matrxImageFileId = (source: string): string =>\n source.slice(MATRX_IMAGE_PREFIX.length);\n\nexport type ImageStorePorts = {\n upload(file: File): Promise<{ fileId: string }>;\n getBlob(fileId: string): Promise<Blob>;\n /** Called when an upload fails, so the person is told (never silent). */\n onUploadError(error: unknown): void;\n};\n\nconst ALLOWED = [\"image/png\", \"image/jpeg\", \"image/jpg\", \"image/gif\", \"image/bmp\", \"image/webp\"];\n\ntype ImageIo = {\n saveImage(file: File): Promise<unknown>;\n};\ntype UrlImages = {\n registerURLImageDownloader(d: (url: string) => Promise<string>): { dispose(): void };\n};\ntype InjectorLike = { get(id: unknown): unknown };\n\n/** Pure core: the two replacements, given ports. */\nexport function createMatrxImageStore(ports: ImageStorePorts) {\n const blobUrls = new Map<string, Promise<string>>();\n return {\n /** Revoke every object URL this store made (the editor is going away). */\n dispose() {\n for (const pending of blobUrls.values()) void pending.then((url) => URL.revokeObjectURL(url), () => undefined);\n blobUrls.clear();\n },\n async saveImage(file: File) {\n if (!ALLOWED.includes(file.type)) throw new Error(\"ERROR_IMAGE_TYPE\");\n let fileId: string;\n try {\n ({ fileId } = await ports.upload(file));\n } catch (error) {\n ports.onUploadError(error);\n throw error;\n }\n const source = MATRX_IMAGE_PREFIX + fileId;\n // Univer measures the picture from base64Cache before it inserts (an empty one throws, nothing is\n // inserted). A blob URL of the file just chosen serves that and paints it; it is never saved.\n const localUrl = URL.createObjectURL(file);\n blobUrls.set(fileId, Promise.resolve(localUrl));\n return {\n imageId: generateRandomId(6),\n imageSourceType: ImageSourceType.URL,\n source,\n base64Cache: localUrl,\n status: ImageUploadStatusType.SUCCUSS,\n };\n },\n async download(url: string): Promise<string> {\n if (!isMatrxImageSource(url)) return url;\n const fileId = matrxImageFileId(url);\n let pending = blobUrls.get(fileId);\n if (!pending) {\n pending = ports.getBlob(fileId).then((blob) => URL.createObjectURL(blob));\n blobUrls.set(fileId, pending);\n pending.catch(() => blobUrls.delete(fileId));\n }\n return pending;\n },\n };\n}\n\nconst stores = new WeakMap<object, { dispose(): void }>();\n\n/** Release the object URLs of the store wired into this Univer instance (call as the instance is disposed). */\nexport function releaseMatrxImageStore(injector: object | null | undefined): void {\n if (!injector) return;\n stores.get(injector)?.dispose();\n stores.delete(injector);\n}\n\n/** Wire the store into one Univer instance's image services. */\nexport function registerMatrxImageStore(injector: InjectorLike, ports: ImageStorePorts): void {\n const store = createMatrxImageStore(ports);\n stores.set(injector, store);\n const io = injector.get(IImageIoService) as ImageIo;\n io.saveImage = store.saveImage as ImageIo[\"saveImage\"];\n const urls = injector.get(IURLImageService) as UrlImages;\n urls.registerURLImageDownloader(store.download);\n}\n","import { releaseMatrxImageStore } from \"./univerImageStore\";\n\n/** Univer owns React roots; synchronous dispose during React commit causes races. */\nexport function disposeUniverInstance(\n instance: { dispose(): void } | null | undefined,\n): void {\n const target = instance;\n if (!target) return;\n const injector = (target as { __getInjector?: () => object }).__getInjector?.();\n queueMicrotask(() => {\n releaseMatrxImageStore(injector);\n try {\n target.dispose();\n } catch {\n // Instance may already be torn down by a concurrent unmount.\n }\n });\n}\n","import { UserManagerService } from \"@univerjs/core\";\nimport { getUniverHost } from \"./ports\";\n\ntype InjectorLike = { get(id: unknown): unknown };\n\n/**\n * Tell Univer who is typing, so a comment thread is attributed to the\n * signed-in person instead of Univer's placeholder owner.\n *\n * The userID MUST keep the `Owner_` prefix: Univer's built-in local\n * permission service grants unit roles by user-id prefix (`isDevRole`), so a\n * bare uuid gets no role and every edit is refused as \"range is protected\".\n * Our own access (canActOn) decides editability; Univer must always see an\n * owner. Guarded by identify-univer-user-keeps-owner-role.test.ts.\n */\nexport async function identifyUniverUser(injector: InjectorLike): Promise<void> {\n const user = await getUniverHost().getSignedInUser();\n if (!user?.id) return;\n const meta = (user.metadata ?? {}) as Record<string, unknown>;\n const name = String(meta.full_name ?? meta.name ?? user.email ?? \"You\");\n const users = injector.get(UserManagerService) as {\n setCurrentUser(u: { userID: string; name: string; avatar: string }): void;\n };\n users.setCurrentUser({ userID: univerOwnerId(user.id), name, avatar: String(meta.avatar_url ?? \"\") });\n}\n\n/** Univer's local permission service reads the role from this prefix. */\nexport function univerOwnerId(userId: string): string {\n return `Owner_${userId}`;\n}\n","/**\n * isSnapshotMutation — the autosave dirty-filter for the Univer editors.\n *\n * D97: `onCommandExecuted` fires for EVERY command — including viewport\n * scrolls and selection moves — so an unfiltered listener marked the doc\n * dirty on mere scrolling and wrote a new snapshot row (version history +\n * collab broadcast) 2.5s later. Only CommandType.MUTATION changes the data\n * that a snapshot persists (Univer's own docs: OPERATION = \"not saved to\n * snapshot, such as modifying scroll position\"). We therefore accept ONLY\n * mutations, and additionally deny-list any mutation whose id marks it as\n * scroll / selection / viewport bookkeeping, belt-and-braces.\n */\nimport type { ICommandInfo } from \"@univerjs/core\";\n\n/**\n * Univer's `CommandType.MUTATION`. A literal, so this filter (and the document\n * model that uses it outside the editor chunk) never pulls Univer's runtime —\n * which touches `window` at import — into a server-rendered chunk.\n */\nconst MUTATION = 2;\n\n/** Non-content command ids that must never mark the document dirty. */\nconst NON_CONTENT_ID_PATTERN =\n /(scroll|selection|set-selections|viewport|zoom|hover|cursor|focus|activate)/i;\n\nexport function isSnapshotMutation(command: ICommandInfo): boolean {\n if (command.type !== MUTATION) return false;\n if (NON_CONTENT_ID_PATTERN.test(command.id)) return false;\n return true;\n}\n\n/**\n * Mutations a SPREADSHEET fires on its own while it opens and recalculates —\n * they change nothing the person typed, so they must never mark the workbook\n * dirty. Observed on a plain open of a workbook with formulas (2026-10-10):\n * `formula.mutation.set-trigger-formula-calculation-start`,\n * `...set-formula-calculation-start`, `...set-formula-calculation-notification`\n * and a `doc.mutation.rich-text-editing` from the internal cell-editor document.\n * Each open then wrote a new snapshot (history bloat, and a false \"both changed\"\n * against Google). A real edit always arrives as a `sheet.mutation.*`.\n */\nconst WORKBOOK_SELF_MUTATION_PATTERN =\n /^(formula\\.mutation\\.[a-z-]*(calculation|trigger)[a-z-]*|doc\\.mutation\\.)/;\n\nexport function isWorkbookSnapshotMutation(command: ICommandInfo): boolean {\n if (!isSnapshotMutation(command)) return false;\n return !WORKBOOK_SELF_MUTATION_PATTERN.test(command.id);\n}\n","import { getUniverHost } from \"./ports\";\nimport { isMatrxImageSource, matrxImageFileId, type ImageStorePorts } from \"./univerImageStore\";\n\n/** The real file-store ports (the host's): upload, read back as a blob, tell the person on failure. */\nexport const matrxImagePorts: ImageStorePorts = {\n async upload(file) {\n return getUniverHost().files.upload(file);\n },\n getBlob(fileId) {\n return getUniverHost().files.getBlob(fileId);\n },\n onUploadError(error) {\n getUniverHost().notifyUploadError(error);\n },\n};\n\n/** Export: a snapshot picture source (`matrx-file:<id>` or a URL) as bytes + type, or null if unreadable. */\nexport async function resolveExportImage(\n source: string,\n): Promise<{ bytes: Uint8Array; extension: \"png\" | \"jpeg\" | \"gif\" } | null> {\n let blob: Blob;\n if (isMatrxImageSource(source)) {\n blob = await getUniverHost().files.getBlob(matrxImageFileId(source));\n } else {\n const res = await fetch(source);\n if (!res.ok) return null;\n blob = await res.blob();\n }\n const extension = blob.type.includes(\"gif\") ? \"gif\" : blob.type.includes(\"png\") ? \"png\" : \"jpeg\";\n return { bytes: new Uint8Array(await blob.arrayBuffer()), extension };\n}\n\n/** Import: store a picture from a file, return its file id. */\nexport async function uploadImportedImage(bytes: Uint8Array, extension: string): Promise<string> {\n const mime = extension === \"jpg\" ? \"image/jpeg\" : `image/${extension}`;\n const { fileId } = await getUniverHost().files.upload(new File([bytes as BlobPart], `image.${extension}`, { type: mime }));\n return fileId;\n}\n","/**\n * THE ONE way to replace the whole content of an open Univer editor\n * (workbook or document) with a snapshot that is already SAVED: a history\n * restore, \"Update from Google\" / \"Use Google's\", and a collaborator's\n * replacement all arrive here.\n *\n * Callers save first, then call `showSavedSnapshot`. The open editor for that\n * record (registered below) swaps its content in place, drops its pending\n * autosave (it would write the old content back over the new one), and — in\n * a collab room — tells every peer to reload the same snapshot. A caller\n * never touches Univer units itself: disposing the current unit by hand left\n * Univer's selection observers pointing at nothing (`getUnitId` of null).\n *\n * Pure registry (no Univer import), so it is testable without booting Univer.\n */\n\nexport type EditorRecordKind = \"workbook\" | \"document\";\n\n/** A snapshot row as stored: its id and the Univer body. */\nexport type SavedEditorSnapshot = { id: string; snapshot: unknown };\n\nexport type EditorContentReplacer = (row: SavedEditorSnapshot) => void | Promise<void>;\n\nconst replacers = new Map<string, EditorContentReplacer>();\n\nconst keyOf = (kind: EditorRecordKind, recordId: string) => `${kind}:${recordId}`;\n\n/**\n * The open editor of a record lends its replacer while it is mounted. The\n * newest registration wins; the returned release only removes its own.\n */\nexport function registerOpenEditorContent(\n kind: EditorRecordKind,\n recordId: string,\n replace: EditorContentReplacer,\n): () => void {\n const key = keyOf(kind, recordId);\n replacers.set(key, replace);\n return () => {\n if (replacers.get(key) === replace) replacers.delete(key);\n };\n}\n\n/**\n * Show a saved snapshot in the record's open editor (and its collab peers).\n * Returns false when no editor of the record is open in this tab — the saved\n * row then simply is the content the next open loads.\n */\nexport async function showSavedSnapshot(\n kind: EditorRecordKind,\n recordId: string,\n row: SavedEditorSnapshot,\n): Promise<boolean> {\n const replace = replacers.get(keyOf(kind, recordId));\n if (!replace) return false;\n await replace(row);\n return true;\n}\n\n/**\n * THE CONTENT-SWAP GUARD. A swap is dispose-then-create; if the create throws, the unit is gone and\n * the grid would stay blank. `swap(snapshot)` is the editor's own dispose+create (it disposes\n * whatever unit is open, if any). On a throw this puts the content that was open before straight\n * back (synchronously — never a blank grid) and says so; the editor then reloads the latest SAVED\n * version and still raises the original error, so the person is told.\n */\nexport function swapContentGuarded(args: {\n swap: (snapshot: unknown) => void;\n next: unknown;\n /** What was open before (`workbook.save()` taken before the swap), or null when nothing was. */\n previous: unknown | null;\n}): { ok: true } | { ok: false; error: unknown; previousRestored: boolean } {\n try {\n args.swap(args.next);\n return { ok: true };\n } catch (error) {\n let previousRestored = false;\n if (args.previous) {\n try {\n args.swap(args.previous);\n previousRestored = true;\n } catch {\n /* the latest saved version is tried next, by the caller */\n }\n }\n return { ok: false, error, previousRestored };\n }\n}\n","\"use client\";\n\n/**\n * Univer's drawing UI names `AddImageIcon` on its Insert-image menu items but\n * never registers it (upstream gap), so the icon manager logs \"Icon AddImageIcon\n * does not exist\" on every editor load. Register it ourselves, right after\n * `createUniver`, so the toolbar draws a real icon and the console stays clean.\n */\n\nimport { IconManager } from \"@univerjs/ui\";\nimport { AddImageIcon } from \"@univerjs/icons\";\n\ntype InjectorLike = { get(id: unknown): unknown };\n\n/** Icons Univer's own plugins reference but do not register. */\nexport const MISSING_UNIVER_ICONS = { AddImageIcon } as const;\n\nexport function registerMissingUniverIcons(injector: InjectorLike): void {\n const icons = injector.get(IconManager) as {\n register(icons: Record<string, unknown>): unknown;\n };\n icons.register({ ...MISSING_UNIVER_ICONS });\n}\n","type UniverDependency = new (...args: never[]) => unknown;\n\ntype UniverInjector = {\n has(dependency: UniverDependency): boolean;\n add(dependency: [UniverDependency]): void;\n};\n\n/**\n * Register services required by process-global Univer Facade observers.\n *\n * Facade modules can outlive the editor type that first imported them, so a\n * later Univer instance may receive their lifecycle callbacks without loading\n * that type's plugins. Registration is intentionally narrow: callers name only\n * the observer dependencies that are safe for their instance.\n */\nexport function registerUniverFacadeDependencies(\n injector: UniverInjector,\n dependencies: readonly UniverDependency[],\n): void {\n for (const dependency of dependencies) {\n if (!injector.has(dependency)) injector.add([dependency]);\n }\n}\n","\"use client\";\n\n/**\n * Univer's notifications go through the APP's toast path — one toaster per page.\n *\n * Univer's own `DesktopNotificationService` mounts a second sonner `<Toaster>`\n * into its workbench the moment it is created. sonner's toast store is global,\n * so with the app's Toaster also mounted every toast (ours and Univer's) showed\n * twice on a workbook or document page. Replacing the service before anything\n * resolves it means Univer never mounts that Toaster, and what Univer has to\n * say (clipboard permission, paste limits) reaches the person through\n * the host's toast port like every other message (tracked, Error Inspector, Alchemy).\n *\n * Call right after `createUniver`, before the first unit is created.\n */\n\nimport { toDisposable, type IDisposable } from \"@univerjs/core\";\nimport { INotificationService } from \"@univerjs/ui\";\n\nimport { getUniverHost } from \"./ports\";\n\ntype UniverNotification = {\n title: unknown;\n content?: string;\n type?: \"success\" | \"info\" | \"warning\" | \"error\" | \"message\" | \"loading\";\n duration?: number;\n};\n\nexport interface NotificationReplaceableInjector {\n replace?: (dependency: [unknown, unknown]) => void;\n}\n\n/** Show one Univer notification as an app toast; dismissing the disposable removes it. */\nexport function showUniverNotification(params: UniverNotification): IDisposable {\n const title = typeof params.title === \"string\" ? params.title : String(params.title ?? \"\");\n const options = {\n ...(params.content ? { description: params.content } : {}),\n ...(params.duration !== undefined ? { duration: params.duration } : {}),\n };\n const kind = params.type ?? \"message\";\n const { toast } = getUniverHost();\n const show = kind === \"loading\" ? toast.message : toast[kind];\n const id = show(title, options) as string | number;\n return toDisposable(() => toast.dismiss(id));\n}\n\nexport function routeUniverNotificationsToApp(\n injector: NotificationReplaceableInjector | null | undefined,\n): { applied: boolean; reason?: string } {\n if (!injector || typeof injector.replace !== \"function\") {\n return { applied: false, reason: \"injector has no replace()\" };\n }\n try {\n injector.replace([INotificationService, { useValue: { show: showUniverNotification } }]);\n return { applied: true };\n } catch (err) {\n return { applied: false, reason: err instanceof Error ? err.message : String(err) };\n }\n}\n","/**\n * Resolve a colour Univer hands its canvas — WITHOUT the dark-mode inversion.\n *\n * Univer 1.0 writes many of its own fills as THEME TOKENS, not colours: the\n * page is `\"gray.0\"`, the desk `\"gray.100\"`, the page outline `\"gray.200\"`,\n * default ink `\"gray.900\"` (engine-render 1.0.2 `DocBackground`, docs-ui\n * `resolveDocRenderBackground`). Its `CanvasColorService` turns a token into a\n * colour before anything reaches `ctx.fillStyle`; its `DumbCanvasColorService`\n * does NOT — it returns `\"gray.0\"` as-is. A canvas IGNORES a `fillStyle` it\n * cannot parse and keeps the previous one, which starts as `#000000`, so with\n * the dumb service every page came out solid black and the canvas element's\n * CSS background (`style.backgroundColor = \"gray.100\"`, also ignored) stayed\n * transparent — in every browser, headed or headless, light or dark.\n *\n * Pure (no Univer import) so the jest test and `check:univer-doc-theme` drive\n * the same function the editor installs.\n */\n\n/** `palette.shade` — the shape Univer's theme tokens take (`gray.0`, `primary.600`). */\nconst THEME_TOKEN = /^[a-zA-Z]+\\.[a-zA-Z0-9]+$/;\n\n/**\n * @param color What Univer is about to paint: a theme token or a real colour.\n * @param lookup The live theme (`ThemeService.getColorFromTheme`).\n * @returns the token's colour from the current theme; anything else unchanged.\n */\nexport function resolveUniverCanvasColor(\n color: string,\n lookup: (token: string) => unknown,\n): string {\n if (!THEME_TOKEN.test(color)) return color;\n const resolved = lookup(color);\n return typeof resolved === \"string\" && resolved.length > 0 ? resolved : color;\n}\n","/**\n * useUniverDarkModeSync — keep a Univer instance's dark mode in lockstep with\n * the app's painted theme (`<html class=\"dark\">` via `useThemeMode`).\n *\n * Univer owns its own theming (see https://docs.univer.ai/guides/docs/ui/themes):\n * you boot it with `theme` + `darkMode`, then flip dark mode at runtime via the\n * Facade API `univerAPI.toggleDarkMode(isDark)`. This is the ONLY supported way\n * to theme Univer — do NOT try to force it with a `colorScheme` CSS override on\n * a wrapper (that just fights Univer's own popups/portals and breaks dark mode).\n *\n * Shared by every Univer surface (DocumentEditor, WorkbookEditor) so the\n * theme→Univer bridge lives in exactly one place.\n */\n\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { FUniver } from \"@univerjs/presets\";\n\nimport { getUniverHost } from \"./ports\";\n\n/**\n * @param apiRef Ref holding the Univer Facade API (set once the instance boots).\n * @param ready True once the instance is booted and the API is usable.\n */\nexport function useUniverDarkModeSync(\n apiRef: React.RefObject<FUniver | null>,\n ready: boolean,\n): void {\n const mode = getUniverHost().useThemeMode();\n\n useEffect(() => {\n if (!ready || !apiRef.current) return;\n apiRef.current.toggleDarkMode(mode === \"dark\");\n }, [apiRef, ready, mode]);\n}\n"],"mappings":"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","names":["import_core","import_core","import_ui"]}
1
+ {"version":3,"sources":["../src/univer/index.ts","../src/univer/ports.ts","../src/univer/history.ts","../src/univer/univerImageStore.ts","../src/univer/disposeUniverInstance.ts","../src/univer/identifyUniverUser.ts","../src/univer/isSnapshotMutation.ts","../src/univer/matrxImagePorts.ts","../src/univer/openEditorContent.ts","../src/univer/registerMissingUniverIcons.ts","../src/univer/registerUniverFacadeDependencies.ts","../src/univer/routeUniverNotificationsToApp.ts","../src/univer/univer-theme-token-color.ts","../src/univer/useUniverDarkModeSync.ts","../src/univer/collab/covered.ts"],"sourcesContent":["/**\n * The Univer glue shared by `/sheets` and `/docs` (was matrx-frontend `lib/univer`). A transitional\n * subpath: matrx-frontend's two editors import it until they move into their sections, then this\n * entry goes internal again (BUILD-BOOK §1.3, Amendments W1-G2a1). Importing it loads `@univerjs/*`;\n * the pure history keys are their own entry (`./univer/history`) for code that must not.\n */\nexport * from \"./ports\";\nexport * from \"./history\";\nexport * from \"./disposeUniverInstance\";\nexport * from \"./identifyUniverUser\";\nexport * from \"./isSnapshotMutation\";\nexport * from \"./matrxImagePorts\";\nexport * from \"./openEditorContent\";\nexport * from \"./registerMissingUniverIcons\";\nexport * from \"./registerUniverFacadeDependencies\";\nexport * from \"./routeUniverNotificationsToApp\";\nexport * from \"./univer-theme-token-color\";\nexport * from \"./univerImageStore\";\nexport * from \"./useUniverDarkModeSync\";\nexport * from \"./collab/covered\";\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","/**\n * A document's or a workbook's snapshot history as a canvas tab — the keys and toggle inputs the\n * editors use, and the reader the two history views use. One tab per document (`document-history`,\n * keyed by the document id) and per workbook (`workbook-history`, keyed by the workbook id).\n *\n * Pure: no React, no canvas package, no Univer. The two `defineCanvasKind` registrations stay with\n * the host app (matrx-frontend `features/canvas/host`) and lazy-load the views, so this module never\n * imports a view (the cycle the old `lib/univer/historyKinds.ts` closed).\n */\n\nexport const DOCUMENT_HISTORY_KIND = \"document-history\";\nexport const WORKBOOK_HISTORY_KIND = \"workbook-history\";\n\nexport interface HistoryTabData {\n id: string;\n editable: boolean;\n}\n\n/** Structurally the host's `ToolToggleInput` (the canvas toggle the editor's History button presses). */\nexport interface HistoryToggleInput {\n kind: string;\n key: string;\n title: string;\n data: { id: string; editable: boolean };\n followTitle: true;\n}\n\n/** A tab's data as a history subject, or null when it names no subject. */\nexport function readHistoryTab(data: unknown): HistoryTabData | null {\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) return null;\n const record = data as Record<string, unknown>;\n const id = typeof record.id === \"string\" && record.id ? record.id : null;\n if (!id) return null;\n return { id, editable: record.editable !== false };\n}\n\n/** \"<what> · <subject>\", or just \"<what>\" while the subject has no name. */\nfunction subjectTitle(what: string, subject: string | null | undefined): string {\n const name = subject?.trim();\n return name ? `${what} · ${name}` : what;\n}\n\n// The tab names its subject — two open history tabs never share a title, and\n// the editor that owns the toggle keeps it in step with a rename.\nexport function documentHistoryToggleInput(documentId: string, editable: boolean, documentName?: string | null): HistoryToggleInput {\n return {\n kind: DOCUMENT_HISTORY_KIND,\n key: documentId,\n title: subjectTitle(\"Document history\", documentName),\n data: { id: documentId, editable },\n followTitle: true,\n };\n}\n\nexport function workbookHistoryToggleInput(workbookId: string, editable: boolean, workbookName?: string | null): HistoryToggleInput {\n return {\n kind: WORKBOOK_HISTORY_KIND,\n key: workbookId,\n title: subjectTitle(\"Workbook history\", workbookName),\n data: { id: workbookId, editable },\n followTitle: true,\n };\n}\n","/**\n * univerImageStore — images on a sheet or in a document live in OUR file\n * store; the Univer snapshot holds only a reference (`matrx-file:<fileId>`).\n *\n * Why: Univer's stock image service inlines every picture as base64 into the\n * snapshot, so one 2 MB photo made every autosave, history row and collab\n * mutation ~2 MB. Here `saveImage` uploads the file and hands Univer a URL-type\n * source; Univer's URL-image downloader turns the reference back into bytes\n * (`getBlob` → object URL) when it paints. Images already inline (BASE64\n * sources) are untouched and keep rendering.\n */\n\nimport {\n IImageIoService,\n IURLImageService,\n ImageSourceType,\n ImageUploadStatusType,\n generateRandomId,\n} from \"@univerjs/core\";\n\nexport const MATRX_IMAGE_PREFIX = \"matrx-file:\";\n\nexport const isMatrxImageSource = (source: string): boolean =>\n source.startsWith(MATRX_IMAGE_PREFIX);\n\nexport const matrxImageFileId = (source: string): string =>\n source.slice(MATRX_IMAGE_PREFIX.length);\n\nexport type ImageStorePorts = {\n upload(file: File): Promise<{ fileId: string }>;\n getBlob(fileId: string): Promise<Blob>;\n /** Called when an upload fails, so the person is told (never silent). */\n onUploadError(error: unknown): void;\n};\n\nconst ALLOWED = [\"image/png\", \"image/jpeg\", \"image/jpg\", \"image/gif\", \"image/bmp\", \"image/webp\"];\n\ntype ImageIo = {\n saveImage(file: File): Promise<unknown>;\n};\ntype UrlImages = {\n registerURLImageDownloader(d: (url: string) => Promise<string>): { dispose(): void };\n};\ntype InjectorLike = { get(id: unknown): unknown };\n\n/** Pure core: the two replacements, given ports. */\nexport function createMatrxImageStore(ports: ImageStorePorts) {\n const blobUrls = new Map<string, Promise<string>>();\n return {\n /** Revoke every object URL this store made (the editor is going away). */\n dispose() {\n for (const pending of blobUrls.values()) void pending.then((url) => URL.revokeObjectURL(url), () => undefined);\n blobUrls.clear();\n },\n async saveImage(file: File) {\n if (!ALLOWED.includes(file.type)) throw new Error(\"ERROR_IMAGE_TYPE\");\n let fileId: string;\n try {\n ({ fileId } = await ports.upload(file));\n } catch (error) {\n ports.onUploadError(error);\n throw error;\n }\n const source = MATRX_IMAGE_PREFIX + fileId;\n // Univer measures the picture from base64Cache before it inserts (an empty one throws, nothing is\n // inserted). A blob URL of the file just chosen serves that and paints it; it is never saved.\n const localUrl = URL.createObjectURL(file);\n blobUrls.set(fileId, Promise.resolve(localUrl));\n return {\n imageId: generateRandomId(6),\n imageSourceType: ImageSourceType.URL,\n source,\n base64Cache: localUrl,\n status: ImageUploadStatusType.SUCCUSS,\n };\n },\n async download(url: string): Promise<string> {\n if (!isMatrxImageSource(url)) return url;\n const fileId = matrxImageFileId(url);\n let pending = blobUrls.get(fileId);\n if (!pending) {\n pending = ports.getBlob(fileId).then((blob) => URL.createObjectURL(blob));\n blobUrls.set(fileId, pending);\n pending.catch(() => blobUrls.delete(fileId));\n }\n return pending;\n },\n };\n}\n\nconst stores = new WeakMap<object, { dispose(): void }>();\n\n/** Release the object URLs of the store wired into this Univer instance (call as the instance is disposed). */\nexport function releaseMatrxImageStore(injector: object | null | undefined): void {\n if (!injector) return;\n stores.get(injector)?.dispose();\n stores.delete(injector);\n}\n\n/** Wire the store into one Univer instance's image services. */\nexport function registerMatrxImageStore(injector: InjectorLike, ports: ImageStorePorts): void {\n const store = createMatrxImageStore(ports);\n stores.set(injector, store);\n const io = injector.get(IImageIoService) as ImageIo;\n io.saveImage = store.saveImage as ImageIo[\"saveImage\"];\n const urls = injector.get(IURLImageService) as UrlImages;\n urls.registerURLImageDownloader(store.download);\n}\n","import { releaseMatrxImageStore } from \"./univerImageStore\";\n\n/** Univer owns React roots; synchronous dispose during React commit causes races. */\nexport function disposeUniverInstance(\n instance: { dispose(): void } | null | undefined,\n): void {\n const target = instance;\n if (!target) return;\n const injector = (target as { __getInjector?: () => object }).__getInjector?.();\n queueMicrotask(() => {\n releaseMatrxImageStore(injector);\n try {\n target.dispose();\n } catch {\n // Instance may already be torn down by a concurrent unmount.\n }\n });\n}\n","import { UserManagerService } from \"@univerjs/core\";\nimport { getUniverHost } from \"./ports\";\n\ntype InjectorLike = { get(id: unknown): unknown };\n\n/**\n * Tell Univer who is typing, so a comment thread is attributed to the\n * signed-in person instead of Univer's placeholder owner.\n *\n * The userID MUST keep the `Owner_` prefix: Univer's built-in local\n * permission service grants unit roles by user-id prefix (`isDevRole`), so a\n * bare uuid gets no role and every edit is refused as \"range is protected\".\n * Our own access (canActOn) decides editability; Univer must always see an\n * owner. Guarded by identify-univer-user-keeps-owner-role.test.ts.\n */\nexport async function identifyUniverUser(injector: InjectorLike): Promise<void> {\n const user = await getUniverHost().getSignedInUser();\n if (!user?.id) return;\n const meta = (user.metadata ?? {}) as Record<string, unknown>;\n const name = String(meta.full_name ?? meta.name ?? user.email ?? \"You\");\n const users = injector.get(UserManagerService) as {\n setCurrentUser(u: { userID: string; name: string; avatar: string }): void;\n };\n users.setCurrentUser({ userID: univerOwnerId(user.id), name, avatar: String(meta.avatar_url ?? \"\") });\n}\n\n/** Univer's local permission service reads the role from this prefix. */\nexport function univerOwnerId(userId: string): string {\n return `Owner_${userId}`;\n}\n","/**\n * isSnapshotMutation — the autosave dirty-filter for the Univer editors.\n *\n * D97: `onCommandExecuted` fires for EVERY command — including viewport\n * scrolls and selection moves — so an unfiltered listener marked the doc\n * dirty on mere scrolling and wrote a new snapshot row (version history +\n * collab broadcast) 2.5s later. Only CommandType.MUTATION changes the data\n * that a snapshot persists (Univer's own docs: OPERATION = \"not saved to\n * snapshot, such as modifying scroll position\"). We therefore accept ONLY\n * mutations, and additionally deny-list any mutation whose id marks it as\n * scroll / selection / viewport bookkeeping, belt-and-braces.\n */\nimport type { ICommandInfo } from \"@univerjs/core\";\n\n/**\n * Univer's `CommandType.MUTATION`. A literal, so this filter (and the document\n * model that uses it outside the editor chunk) never pulls Univer's runtime —\n * which touches `window` at import — into a server-rendered chunk.\n */\nconst MUTATION = 2;\n\n/** Non-content command ids that must never mark the document dirty. */\nconst NON_CONTENT_ID_PATTERN =\n /(scroll|selection|set-selections|viewport|zoom|hover|cursor|focus|activate)/i;\n\nexport function isSnapshotMutation(command: ICommandInfo): boolean {\n if (command.type !== MUTATION) return false;\n if (NON_CONTENT_ID_PATTERN.test(command.id)) return false;\n return true;\n}\n\n/**\n * Mutations a SPREADSHEET fires on its own while it opens and recalculates —\n * they change nothing the person typed, so they must never mark the workbook\n * dirty. Observed on a plain open of a workbook with formulas (2026-10-10):\n * `formula.mutation.set-trigger-formula-calculation-start`,\n * `...set-formula-calculation-start`, `...set-formula-calculation-notification`\n * and a `doc.mutation.rich-text-editing` from the internal cell-editor document.\n * Each open then wrote a new snapshot (history bloat, and a false \"both changed\"\n * against Google). A real edit always arrives as a `sheet.mutation.*`.\n */\nconst WORKBOOK_SELF_MUTATION_PATTERN =\n /^(formula\\.mutation\\.[a-z-]*(calculation|trigger)[a-z-]*|doc\\.mutation\\.)/;\n\nexport function isWorkbookSnapshotMutation(command: ICommandInfo): boolean {\n if (!isSnapshotMutation(command)) return false;\n return !WORKBOOK_SELF_MUTATION_PATTERN.test(command.id);\n}\n","import { getUniverHost } from \"./ports\";\nimport { isMatrxImageSource, matrxImageFileId, type ImageStorePorts } from \"./univerImageStore\";\n\n/** The real file-store ports (the host's): upload, read back as a blob, tell the person on failure. */\nexport const matrxImagePorts: ImageStorePorts = {\n async upload(file) {\n return getUniverHost().files.upload(file);\n },\n getBlob(fileId) {\n return getUniverHost().files.getBlob(fileId);\n },\n onUploadError(error) {\n getUniverHost().notifyUploadError(error);\n },\n};\n\n/** Export: a snapshot picture source (`matrx-file:<id>` or a URL) as bytes + type, or null if unreadable. */\nexport async function resolveExportImage(\n source: string,\n): Promise<{ bytes: Uint8Array; extension: \"png\" | \"jpeg\" | \"gif\" } | null> {\n let blob: Blob;\n if (isMatrxImageSource(source)) {\n blob = await getUniverHost().files.getBlob(matrxImageFileId(source));\n } else {\n const res = await fetch(source);\n if (!res.ok) return null;\n blob = await res.blob();\n }\n const extension = blob.type.includes(\"gif\") ? \"gif\" : blob.type.includes(\"png\") ? \"png\" : \"jpeg\";\n return { bytes: new Uint8Array(await blob.arrayBuffer()), extension };\n}\n\n/** Import: store a picture from a file, return its file id. */\nexport async function uploadImportedImage(bytes: Uint8Array, extension: string): Promise<string> {\n const mime = extension === \"jpg\" ? \"image/jpeg\" : `image/${extension}`;\n const { fileId } = await getUniverHost().files.upload(new File([bytes as BlobPart], `image.${extension}`, { type: mime }));\n return fileId;\n}\n","/**\n * THE ONE way to replace the whole content of an open Univer editor\n * (workbook or document) with a snapshot that is already SAVED: a history\n * restore, \"Update from Google\" / \"Use Google's\", and a collaborator's\n * replacement all arrive here.\n *\n * Callers save first, then call `showSavedSnapshot`. The open editor for that\n * record (registered below) swaps its content in place, drops its pending\n * autosave (it would write the old content back over the new one), and — in\n * a collab room — tells every peer to reload the same snapshot. A caller\n * never touches Univer units itself: disposing the current unit by hand left\n * Univer's selection observers pointing at nothing (`getUnitId` of null).\n *\n * Pure registry (no Univer import), so it is testable without booting Univer.\n */\n\nexport type EditorRecordKind = \"workbook\" | \"document\";\n\n/** A snapshot row as stored: its id and the Univer body. */\nexport type SavedEditorSnapshot = { id: string; snapshot: unknown };\n\nexport type EditorContentReplacer = (row: SavedEditorSnapshot) => void | Promise<void>;\n\nconst replacers = new Map<string, EditorContentReplacer>();\n\nconst keyOf = (kind: EditorRecordKind, recordId: string) => `${kind}:${recordId}`;\n\n/**\n * The open editor of a record lends its replacer while it is mounted. The\n * newest registration wins; the returned release only removes its own.\n */\nexport function registerOpenEditorContent(\n kind: EditorRecordKind,\n recordId: string,\n replace: EditorContentReplacer,\n): () => void {\n const key = keyOf(kind, recordId);\n replacers.set(key, replace);\n return () => {\n if (replacers.get(key) === replace) replacers.delete(key);\n };\n}\n\n/**\n * Show a saved snapshot in the record's open editor (and its collab peers).\n * Returns false when no editor of the record is open in this tab — the saved\n * row then simply is the content the next open loads.\n */\nexport async function showSavedSnapshot(\n kind: EditorRecordKind,\n recordId: string,\n row: SavedEditorSnapshot,\n): Promise<boolean> {\n const replace = replacers.get(keyOf(kind, recordId));\n if (!replace) return false;\n await replace(row);\n return true;\n}\n\n/**\n * THE CONTENT-SWAP GUARD. A swap is dispose-then-create; if the create throws, the unit is gone and\n * the grid would stay blank. `swap(snapshot)` is the editor's own dispose+create (it disposes\n * whatever unit is open, if any). On a throw this puts the content that was open before straight\n * back (synchronously — never a blank grid) and says so; the editor then reloads the latest SAVED\n * version and still raises the original error, so the person is told.\n */\nexport function swapContentGuarded(args: {\n swap: (snapshot: unknown) => void;\n next: unknown;\n /** What was open before (`workbook.save()` taken before the swap), or null when nothing was. */\n previous: unknown | null;\n}): { ok: true } | { ok: false; error: unknown; previousRestored: boolean } {\n try {\n args.swap(args.next);\n return { ok: true };\n } catch (error) {\n let previousRestored = false;\n if (args.previous) {\n try {\n args.swap(args.previous);\n previousRestored = true;\n } catch {\n /* the latest saved version is tried next, by the caller */\n }\n }\n return { ok: false, error, previousRestored };\n }\n}\n","\"use client\";\n\n/**\n * Univer's drawing UI names `AddImageIcon` on its Insert-image menu items but\n * never registers it (upstream gap), so the icon manager logs \"Icon AddImageIcon\n * does not exist\" on every editor load. Register it ourselves, right after\n * `createUniver`, so the toolbar draws a real icon and the console stays clean.\n */\n\nimport { IconManager } from \"@univerjs/ui\";\nimport { AddImageIcon } from \"@univerjs/icons\";\n\ntype InjectorLike = { get(id: unknown): unknown };\n\n/** Icons Univer's own plugins reference but do not register. */\nexport const MISSING_UNIVER_ICONS = { AddImageIcon } as const;\n\nexport function registerMissingUniverIcons(injector: InjectorLike): void {\n const icons = injector.get(IconManager) as {\n register(icons: Record<string, unknown>): unknown;\n };\n icons.register({ ...MISSING_UNIVER_ICONS });\n}\n","type UniverDependency = new (...args: never[]) => unknown;\n\ntype UniverInjector = {\n has(dependency: UniverDependency): boolean;\n add(dependency: [UniverDependency]): void;\n};\n\n/**\n * Register services required by process-global Univer Facade observers.\n *\n * Facade modules can outlive the editor type that first imported them, so a\n * later Univer instance may receive their lifecycle callbacks without loading\n * that type's plugins. Registration is intentionally narrow: callers name only\n * the observer dependencies that are safe for their instance.\n */\nexport function registerUniverFacadeDependencies(\n injector: UniverInjector,\n dependencies: readonly UniverDependency[],\n): void {\n for (const dependency of dependencies) {\n if (!injector.has(dependency)) injector.add([dependency]);\n }\n}\n","\"use client\";\n\n/**\n * Univer's notifications go through the APP's toast path — one toaster per page.\n *\n * Univer's own `DesktopNotificationService` mounts a second sonner `<Toaster>`\n * into its workbench the moment it is created. sonner's toast store is global,\n * so with the app's Toaster also mounted every toast (ours and Univer's) showed\n * twice on a workbook or document page. Replacing the service before anything\n * resolves it means Univer never mounts that Toaster, and what Univer has to\n * say (clipboard permission, paste limits) reaches the person through\n * the host's toast port like every other message (tracked, Error Inspector, Alchemy).\n *\n * Call right after `createUniver`, before the first unit is created.\n */\n\nimport { toDisposable, type IDisposable } from \"@univerjs/core\";\nimport { INotificationService } from \"@univerjs/ui\";\n\nimport { getUniverHost } from \"./ports\";\n\ntype UniverNotification = {\n title: unknown;\n content?: string;\n type?: \"success\" | \"info\" | \"warning\" | \"error\" | \"message\" | \"loading\";\n duration?: number;\n};\n\nexport interface NotificationReplaceableInjector {\n replace?: (dependency: [unknown, unknown]) => void;\n}\n\n/** Show one Univer notification as an app toast; dismissing the disposable removes it. */\nexport function showUniverNotification(params: UniverNotification): IDisposable {\n const title = typeof params.title === \"string\" ? params.title : String(params.title ?? \"\");\n const options = {\n ...(params.content ? { description: params.content } : {}),\n ...(params.duration !== undefined ? { duration: params.duration } : {}),\n };\n const kind = params.type ?? \"message\";\n const { toast } = getUniverHost();\n const show = kind === \"loading\" ? toast.message : toast[kind];\n const id = show(title, options) as string | number;\n return toDisposable(() => toast.dismiss(id));\n}\n\nexport function routeUniverNotificationsToApp(\n injector: NotificationReplaceableInjector | null | undefined,\n): { applied: boolean; reason?: string } {\n if (!injector || typeof injector.replace !== \"function\") {\n return { applied: false, reason: \"injector has no replace()\" };\n }\n try {\n injector.replace([INotificationService, { useValue: { show: showUniverNotification } }]);\n return { applied: true };\n } catch (err) {\n return { applied: false, reason: err instanceof Error ? err.message : String(err) };\n }\n}\n","/**\n * Resolve a colour Univer hands its canvas — WITHOUT the dark-mode inversion.\n *\n * Univer 1.0 writes many of its own fills as THEME TOKENS, not colours: the\n * page is `\"gray.0\"`, the desk `\"gray.100\"`, the page outline `\"gray.200\"`,\n * default ink `\"gray.900\"` (engine-render 1.0.2 `DocBackground`, docs-ui\n * `resolveDocRenderBackground`). Its `CanvasColorService` turns a token into a\n * colour before anything reaches `ctx.fillStyle`; its `DumbCanvasColorService`\n * does NOT — it returns `\"gray.0\"` as-is. A canvas IGNORES a `fillStyle` it\n * cannot parse and keeps the previous one, which starts as `#000000`, so with\n * the dumb service every page came out solid black and the canvas element's\n * CSS background (`style.backgroundColor = \"gray.100\"`, also ignored) stayed\n * transparent — in every browser, headed or headless, light or dark.\n *\n * Pure (no Univer import) so the jest test and `check:univer-doc-theme` drive\n * the same function the editor installs.\n */\n\n/** `palette.shade` — the shape Univer's theme tokens take (`gray.0`, `primary.600`). */\nconst THEME_TOKEN = /^[a-zA-Z]+\\.[a-zA-Z0-9]+$/;\n\n/**\n * @param color What Univer is about to paint: a theme token or a real colour.\n * @param lookup The live theme (`ThemeService.getColorFromTheme`).\n * @returns the token's colour from the current theme; anything else unchanged.\n */\nexport function resolveUniverCanvasColor(\n color: string,\n lookup: (token: string) => unknown,\n): string {\n if (!THEME_TOKEN.test(color)) return color;\n const resolved = lookup(color);\n return typeof resolved === \"string\" && resolved.length > 0 ? resolved : color;\n}\n","/**\n * useUniverDarkModeSync — keep a Univer instance's dark mode in lockstep with\n * the app's painted theme (`<html class=\"dark\">` via `useThemeMode`).\n *\n * Univer owns its own theming (see https://docs.univer.ai/guides/docs/ui/themes):\n * you boot it with `theme` + `darkMode`, then flip dark mode at runtime via the\n * Facade API `univerAPI.toggleDarkMode(isDark)`. This is the ONLY supported way\n * to theme Univer — do NOT try to force it with a `colorScheme` CSS override on\n * a wrapper (that just fights Univer's own popups/portals and breaks dark mode).\n *\n * Shared by every Univer surface (DocumentEditor, WorkbookEditor) so the\n * theme→Univer bridge lives in exactly one place.\n */\n\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { FUniver } from \"@univerjs/presets\";\n\nimport { getUniverHost } from \"./ports\";\n\n/**\n * @param apiRef Ref holding the Univer Facade API (set once the instance boots).\n * @param ready True once the instance is booted and the API is usable.\n */\nexport function useUniverDarkModeSync(\n apiRef: React.RefObject<FUniver | null>,\n ready: boolean,\n): void {\n const mode = getUniverHost().useThemeMode();\n\n useEffect(() => {\n if (!ready || !apiRef.current) return;\n apiRef.current.toggleDarkMode(mode === \"dark\");\n }, [apiRef, ready, mode]);\n}\n","/**\n * A saved snapshot names the collab-room entries it already holds, so a seat that boots from it never\n * replays them (2026-10-16: a reload showed the text twice). Pure — no Yjs — so the editors' model can\n * stamp and read it without loading the room.\n */\n\n/** The key a saved snapshot carries its covered room entries under (`stampCovered` / `takeCovered`). */\nexport const COLLAB_COVERED_KEY = \"__matrxCollabCovered\";\n\n/** The snapshot to SAVE: `snapshot` plus the room entries it holds (`WorkbookCollabSession.coveredNow`). */\nexport function stampCovered<T extends object>(snapshot: T, covered: readonly string[]): T {\n return { ...snapshot, [COLLAB_COVERED_KEY]: [...covered] };\n}\n\n/** A LOADED snapshot split into the content to mount and the room entries it already holds. */\nexport function takeCovered<T>(snapshot: T): { snapshot: T; covered: Set<string> } {\n if (!snapshot || typeof snapshot !== \"object\" || !(COLLAB_COVERED_KEY in (snapshot as object))) {\n return { snapshot, covered: new Set() };\n }\n const { [COLLAB_COVERED_KEY]: list, ...rest } = snapshot as Record<string, unknown>;\n return {\n snapshot: rest as T,\n covered: new Set(Array.isArray(list) ? list.filter((e): e is string => typeof e === \"string\") : []),\n };\n}\n"],"mappings":"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","names":["import_core","import_core","import_ui"]}
package/dist/univer.d.cts CHANGED
@@ -328,4 +328,19 @@ declare function resolveUniverCanvasColor(color: string, lookup: (token: string)
328
328
  */
329
329
  declare function useUniverDarkModeSync(apiRef: React.RefObject<FUniver | null>, ready: boolean): void;
330
330
 
331
- export { DOCUMENT_HISTORY_KIND, type EditorContentReplacer, type EditorRecordKind, type HistoryTabData, type HistoryToggleInput, type ImageStorePorts, MATRX_IMAGE_PREFIX, MISSING_UNIVER_ICONS, type NotificationReplaceableInjector, type SavedEditorSnapshot, type UniverHostPorts, type UniverToastId, WORKBOOK_HISTORY_KIND, configureUniverHost, createMatrxImageStore, disposeUniverInstance, documentHistoryToggleInput, getUniverHost, identifyUniverUser, isMatrxImageSource, isSnapshotMutation, isWorkbookSnapshotMutation, matrxImageFileId, matrxImagePorts, readHistoryTab, registerMatrxImageStore, registerMissingUniverIcons, registerOpenEditorContent, registerUniverFacadeDependencies, releaseMatrxImageStore, resolveExportImage, resolveUniverCanvasColor, routeUniverNotificationsToApp, showSavedSnapshot, showUniverNotification, swapContentGuarded, univerOwnerId, uploadImportedImage, useUniverDarkModeSync, workbookHistoryToggleInput };
331
+ /**
332
+ * A saved snapshot names the collab-room entries it already holds, so a seat that boots from it never
333
+ * replays them (2026-10-16: a reload showed the text twice). Pure — no Yjs — so the editors' model can
334
+ * stamp and read it without loading the room.
335
+ */
336
+ /** The key a saved snapshot carries its covered room entries under (`stampCovered` / `takeCovered`). */
337
+ declare const COLLAB_COVERED_KEY = "__matrxCollabCovered";
338
+ /** The snapshot to SAVE: `snapshot` plus the room entries it holds (`WorkbookCollabSession.coveredNow`). */
339
+ declare function stampCovered<T extends object>(snapshot: T, covered: readonly string[]): T;
340
+ /** A LOADED snapshot split into the content to mount and the room entries it already holds. */
341
+ declare function takeCovered<T>(snapshot: T): {
342
+ snapshot: T;
343
+ covered: Set<string>;
344
+ };
345
+
346
+ export { COLLAB_COVERED_KEY, DOCUMENT_HISTORY_KIND, type EditorContentReplacer, type EditorRecordKind, type HistoryTabData, type HistoryToggleInput, type ImageStorePorts, MATRX_IMAGE_PREFIX, MISSING_UNIVER_ICONS, type NotificationReplaceableInjector, type SavedEditorSnapshot, type UniverHostPorts, type UniverToastId, WORKBOOK_HISTORY_KIND, configureUniverHost, createMatrxImageStore, disposeUniverInstance, documentHistoryToggleInput, getUniverHost, identifyUniverUser, isMatrxImageSource, isSnapshotMutation, isWorkbookSnapshotMutation, matrxImageFileId, matrxImagePorts, readHistoryTab, registerMatrxImageStore, registerMissingUniverIcons, registerOpenEditorContent, registerUniverFacadeDependencies, releaseMatrxImageStore, resolveExportImage, resolveUniverCanvasColor, routeUniverNotificationsToApp, showSavedSnapshot, showUniverNotification, stampCovered, swapContentGuarded, takeCovered, univerOwnerId, uploadImportedImage, useUniverDarkModeSync, workbookHistoryToggleInput };
package/dist/univer.d.ts CHANGED
@@ -328,4 +328,19 @@ declare function resolveUniverCanvasColor(color: string, lookup: (token: string)
328
328
  */
329
329
  declare function useUniverDarkModeSync(apiRef: React.RefObject<FUniver | null>, ready: boolean): void;
330
330
 
331
- export { DOCUMENT_HISTORY_KIND, type EditorContentReplacer, type EditorRecordKind, type HistoryTabData, type HistoryToggleInput, type ImageStorePorts, MATRX_IMAGE_PREFIX, MISSING_UNIVER_ICONS, type NotificationReplaceableInjector, type SavedEditorSnapshot, type UniverHostPorts, type UniverToastId, WORKBOOK_HISTORY_KIND, configureUniverHost, createMatrxImageStore, disposeUniverInstance, documentHistoryToggleInput, getUniverHost, identifyUniverUser, isMatrxImageSource, isSnapshotMutation, isWorkbookSnapshotMutation, matrxImageFileId, matrxImagePorts, readHistoryTab, registerMatrxImageStore, registerMissingUniverIcons, registerOpenEditorContent, registerUniverFacadeDependencies, releaseMatrxImageStore, resolveExportImage, resolveUniverCanvasColor, routeUniverNotificationsToApp, showSavedSnapshot, showUniverNotification, swapContentGuarded, univerOwnerId, uploadImportedImage, useUniverDarkModeSync, workbookHistoryToggleInput };
331
+ /**
332
+ * A saved snapshot names the collab-room entries it already holds, so a seat that boots from it never
333
+ * replays them (2026-10-16: a reload showed the text twice). Pure — no Yjs — so the editors' model can
334
+ * stamp and read it without loading the room.
335
+ */
336
+ /** The key a saved snapshot carries its covered room entries under (`stampCovered` / `takeCovered`). */
337
+ declare const COLLAB_COVERED_KEY = "__matrxCollabCovered";
338
+ /** The snapshot to SAVE: `snapshot` plus the room entries it holds (`WorkbookCollabSession.coveredNow`). */
339
+ declare function stampCovered<T extends object>(snapshot: T, covered: readonly string[]): T;
340
+ /** A LOADED snapshot split into the content to mount and the room entries it already holds. */
341
+ declare function takeCovered<T>(snapshot: T): {
342
+ snapshot: T;
343
+ covered: Set<string>;
344
+ };
345
+
346
+ export { COLLAB_COVERED_KEY, DOCUMENT_HISTORY_KIND, type EditorContentReplacer, type EditorRecordKind, type HistoryTabData, type HistoryToggleInput, type ImageStorePorts, MATRX_IMAGE_PREFIX, MISSING_UNIVER_ICONS, type NotificationReplaceableInjector, type SavedEditorSnapshot, type UniverHostPorts, type UniverToastId, WORKBOOK_HISTORY_KIND, configureUniverHost, createMatrxImageStore, disposeUniverInstance, documentHistoryToggleInput, getUniverHost, identifyUniverUser, isMatrxImageSource, isSnapshotMutation, isWorkbookSnapshotMutation, matrxImageFileId, matrxImagePorts, readHistoryTab, registerMatrxImageStore, registerMissingUniverIcons, registerOpenEditorContent, registerUniverFacadeDependencies, releaseMatrxImageStore, resolveExportImage, resolveUniverCanvasColor, routeUniverNotificationsToApp, showSavedSnapshot, showUniverNotification, stampCovered, swapContentGuarded, takeCovered, univerOwnerId, uploadImportedImage, useUniverDarkModeSync, workbookHistoryToggleInput };