@casualoffice/sheets 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/api-BI2VLYQ6.d.cts +62 -0
  2. package/dist/api-BI2VLYQ6.d.ts +62 -0
  3. package/dist/chrome.cjs +2569 -0
  4. package/dist/chrome.cjs.map +1 -0
  5. package/dist/chrome.d.cts +96 -0
  6. package/dist/chrome.d.ts +96 -0
  7. package/dist/chrome.js +2556 -0
  8. package/dist/chrome.js.map +1 -0
  9. package/dist/collab.cjs +770 -0
  10. package/dist/collab.cjs.map +1 -0
  11. package/dist/collab.d.cts +248 -0
  12. package/dist/collab.d.ts +248 -0
  13. package/dist/collab.js +737 -0
  14. package/dist/collab.js.map +1 -0
  15. package/dist/embed/embed-runtime.js +128 -128
  16. package/dist/embed.cjs +166 -0
  17. package/dist/embed.cjs.map +1 -1
  18. package/dist/embed.d.cts +78 -3
  19. package/dist/embed.d.ts +78 -3
  20. package/dist/embed.js +166 -0
  21. package/dist/embed.js.map +1 -1
  22. package/dist/index.cjs +262 -165
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +4 -3
  25. package/dist/index.d.ts +4 -3
  26. package/dist/index.js +271 -168
  27. package/dist/index.js.map +1 -1
  28. package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
  29. package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
  30. package/dist/sheets.cjs +102 -16
  31. package/dist/sheets.cjs.map +1 -1
  32. package/dist/sheets.d.cts +49 -63
  33. package/dist/sheets.d.ts +49 -63
  34. package/dist/sheets.js +111 -19
  35. package/dist/sheets.js.map +1 -1
  36. package/package.json +28 -3
  37. package/src/chrome/AutoSumPicker.tsx +176 -0
  38. package/src/chrome/BordersPicker.tsx +171 -0
  39. package/src/chrome/ChromeBottom.tsx +18 -0
  40. package/src/chrome/ChromeTop.tsx +21 -0
  41. package/src/chrome/ColorPicker.tsx +220 -0
  42. package/src/chrome/FindReplace.tsx +370 -0
  43. package/src/chrome/FormulaBar.tsx +378 -0
  44. package/src/chrome/Icon.tsx +43 -0
  45. package/src/chrome/MenuBar.tsx +336 -0
  46. package/src/chrome/NameBox.tsx +347 -0
  47. package/src/chrome/SheetTabs.tsx +346 -0
  48. package/src/chrome/StatusBar.tsx +232 -0
  49. package/src/chrome/Toolbar.tsx +401 -0
  50. package/src/chrome/fonts.ts +42 -0
  51. package/src/chrome/index.ts +24 -0
  52. package/src/collab/attachCollab.ts +151 -0
  53. package/src/collab/bridge-helpers.ts +97 -0
  54. package/src/collab/bridge.ts +885 -0
  55. package/src/collab/bridge.unit.test.ts +160 -0
  56. package/src/collab/index.ts +38 -0
  57. package/src/collab/replay-retry.ts +137 -0
  58. package/src/collab/replay-retry.unit.test.ts +223 -0
  59. package/src/collab/ws-url.ts +20 -0
  60. package/src/collab/ws-url.unit.test.ts +35 -0
  61. package/src/embed/EmbedHostTransport.ts +16 -1
  62. package/src/embed/EmbedTransport.ts +16 -0
  63. package/src/embed/EmbedTransport.unit.test.ts +88 -2
  64. package/src/embed/index.ts +7 -0
  65. package/src/embed/protocol.ts +34 -0
  66. package/src/embed-runtime/index.tsx +20 -0
  67. package/src/sheets/CasualSheets.tsx +204 -33
@@ -0,0 +1,770 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/collab/index.ts
31
+ var collab_exports = {};
32
+ __export(collab_exports, {
33
+ REVERTABLE_MUTATIONS: () => REVERTABLE_MUTATIONS,
34
+ SYNCED_MUTATIONS: () => SYNCED_MUTATIONS,
35
+ attachCollab: () => attachCollab,
36
+ classifyReplayError: () => classifyReplayError,
37
+ deepRewriteUnitId: () => deepRewriteUnitId,
38
+ rewriteJson1OpPathUnitId: () => rewriteJson1OpPathUnitId,
39
+ startBridge: () => startBridge
40
+ });
41
+ module.exports = __toCommonJS(collab_exports);
42
+
43
+ // src/collab/attachCollab.ts
44
+ var Y = __toESM(require("yjs"), 1);
45
+ var import_provider = require("@hocuspocus/provider");
46
+
47
+ // src/collab/bridge.ts
48
+ var import_core = require("@univerjs/core");
49
+ var import_sheets = require("@univerjs/sheets");
50
+
51
+ // src/collab/bridge-helpers.ts
52
+ function rewriteJson1OpPathUnitId(op, oldUnitId, newUnitId) {
53
+ if (oldUnitId === newUnitId) return op;
54
+ if (!Array.isArray(op)) return op;
55
+ const looksLikeOpList = op.length > 0 && Array.isArray(op[0]);
56
+ if (looksLikeOpList) {
57
+ let changed = false;
58
+ const next = op.map((entry) => {
59
+ const r = rewriteJson1OpPathUnitId(entry, oldUnitId, newUnitId);
60
+ if (r !== entry) changed = true;
61
+ return r;
62
+ });
63
+ return changed ? next : op;
64
+ }
65
+ if (op[0] === oldUnitId) {
66
+ const next = [...op];
67
+ next[0] = newUnitId;
68
+ return next;
69
+ }
70
+ return op;
71
+ }
72
+ function deepRewriteUnitId(value, localUnitId) {
73
+ if (Array.isArray(value)) {
74
+ let changed = false;
75
+ const next = value.map((item) => {
76
+ const r = deepRewriteUnitId(item, localUnitId);
77
+ if (r !== item) changed = true;
78
+ return r;
79
+ });
80
+ return changed ? next : value;
81
+ }
82
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
83
+ const obj = value;
84
+ let changed = false;
85
+ const out = {};
86
+ for (const key of Object.keys(obj)) {
87
+ const v = obj[key];
88
+ if (key === "unitId" && typeof v === "string" && v !== localUnitId) {
89
+ out[key] = localUnitId;
90
+ changed = true;
91
+ } else {
92
+ const r = deepRewriteUnitId(v, localUnitId);
93
+ if (r !== v) changed = true;
94
+ out[key] = r;
95
+ }
96
+ }
97
+ return changed ? out : value;
98
+ }
99
+ return value;
100
+ }
101
+
102
+ // src/univer/lazy-plugins.ts
103
+ var LOADERS = {
104
+ cf: async () => {
105
+ const [base, ui] = await Promise.all([
106
+ import("@univerjs/sheets-conditional-formatting"),
107
+ import("@univerjs/sheets-conditional-formatting-ui")
108
+ ]);
109
+ return [
110
+ [base.UniverSheetsConditionalFormattingPlugin],
111
+ [ui.UniverSheetsConditionalFormattingUIPlugin]
112
+ ];
113
+ },
114
+ dv: async () => {
115
+ const [base, ui] = await Promise.all([
116
+ import("@univerjs/sheets-data-validation"),
117
+ import("@univerjs/sheets-data-validation-ui")
118
+ ]);
119
+ return [
120
+ [base.UniverSheetsDataValidationPlugin],
121
+ [ui.UniverSheetsDataValidationUIPlugin]
122
+ ];
123
+ },
124
+ hyperlink: async () => {
125
+ const [base, ui] = await Promise.all([
126
+ import("@univerjs/sheets-hyper-link"),
127
+ import("@univerjs/sheets-hyper-link-ui")
128
+ ]);
129
+ return [
130
+ [base.UniverSheetsHyperLinkPlugin],
131
+ [ui.UniverSheetsHyperLinkUIPlugin]
132
+ ];
133
+ },
134
+ note: async () => {
135
+ const [base, ui] = await Promise.all([
136
+ import("@univerjs/sheets-note"),
137
+ import("@univerjs/sheets-note-ui")
138
+ ]);
139
+ return [[base.UniverSheetsNotePlugin], [ui.UniverSheetsNoteUIPlugin]];
140
+ },
141
+ table: async () => {
142
+ const [base, ui] = await Promise.all([
143
+ import("@univerjs/sheets-table"),
144
+ import("@univerjs/sheets-table-ui")
145
+ ]);
146
+ return [[base.UniverSheetsTablePlugin], [ui.UniverSheetsTableUIPlugin]];
147
+ },
148
+ threadComment: async () => {
149
+ const [tc, tcUi, sheetsTc, sheetsTcUi] = await Promise.all([
150
+ import("@univerjs/thread-comment"),
151
+ import("@univerjs/thread-comment-ui"),
152
+ import("@univerjs/sheets-thread-comment"),
153
+ import("@univerjs/sheets-thread-comment-ui")
154
+ ]);
155
+ return [
156
+ [tc.UniverThreadCommentPlugin],
157
+ [tcUi.UniverThreadCommentUIPlugin],
158
+ [sheetsTc.UniverSheetsThreadCommentPlugin],
159
+ [sheetsTcUi.UniverSheetsThreadCommentUIPlugin]
160
+ ];
161
+ },
162
+ drawing: async () => {
163
+ const [d, dUi, sd, sdUi] = await Promise.all([
164
+ import("@univerjs/drawing"),
165
+ import("@univerjs/drawing-ui"),
166
+ import("@univerjs/sheets-drawing"),
167
+ import("@univerjs/sheets-drawing-ui"),
168
+ // Side-effect imports: install FWorksheet.insertImage / getImages /
169
+ // updateImages on the facade prototype. Without these, code that
170
+ // reaches in via the FUniver facade (e2e specs, future shell glue)
171
+ // sees an undefined method even though the plugin is registered.
172
+ import("@univerjs/sheets-drawing/facade"),
173
+ import("@univerjs/sheets-drawing-ui/facade")
174
+ ]);
175
+ return [
176
+ [d.UniverDrawingPlugin],
177
+ [dUi.UniverDrawingUIPlugin],
178
+ [sd.UniverSheetsDrawingPlugin],
179
+ [sdUi.UniverSheetsDrawingUIPlugin]
180
+ ];
181
+ },
182
+ sort: async () => {
183
+ const [base, ui] = await Promise.all([
184
+ import("@univerjs/sheets-sort"),
185
+ import("@univerjs/sheets-sort-ui")
186
+ ]);
187
+ return [[base.UniverSheetsSortPlugin], [ui.UniverSheetsSortUIPlugin]];
188
+ },
189
+ filter: async () => {
190
+ const [base, ui] = await Promise.all([
191
+ import("@univerjs/sheets-filter"),
192
+ import("@univerjs/sheets-filter-ui")
193
+ ]);
194
+ return [[base.UniverSheetsFilterPlugin], [ui.UniverSheetsFilterUIPlugin]];
195
+ },
196
+ findReplace: async () => {
197
+ const [base, sheets] = await Promise.all([
198
+ import("@univerjs/find-replace"),
199
+ import("@univerjs/sheets-find-replace")
200
+ ]);
201
+ return [[base.UniverFindReplacePlugin], [sheets.UniverSheetsFindReplacePlugin]];
202
+ }
203
+ };
204
+ var loaded = /* @__PURE__ */ new Set();
205
+ var inflight = /* @__PURE__ */ new Map();
206
+ var currentUniver = null;
207
+ function ensurePlugin(univer, group) {
208
+ if (loaded.has(group)) return Promise.resolve();
209
+ const existing = inflight.get(group);
210
+ if (existing) return existing;
211
+ const loader = LOADERS[group];
212
+ if (!loader) return Promise.resolve();
213
+ const p = loader().then((plugins) => {
214
+ for (const [PluginCtor, config] of plugins) {
215
+ univer.registerPlugin(PluginCtor, config);
216
+ }
217
+ loaded.add(group);
218
+ inflight.delete(group);
219
+ });
220
+ inflight.set(group, p);
221
+ return p;
222
+ }
223
+ function ensurePluginByName(group) {
224
+ if (loaded.has(group)) return Promise.resolve();
225
+ if (!currentUniver) return Promise.resolve();
226
+ return ensurePlugin(currentUniver, group);
227
+ }
228
+
229
+ // src/collab/replay-retry.ts
230
+ var TRANSIENT_RETRY_DELAYS_MS = [300, 900, 2700];
231
+ var DEAD_LETTER_CAP = 20;
232
+ function classifyReplayError(err) {
233
+ if (err == null) return "permanent";
234
+ const e = err;
235
+ const name = typeof e.name === "string" ? e.name : "";
236
+ const message = typeof e.message === "string" ? e.message : String(err);
237
+ if (name === "ChunkLoadError") return "transient";
238
+ const lower = message.toLowerCase();
239
+ if (lower.includes("loading chunk") && lower.includes("failed")) {
240
+ return "transient";
241
+ }
242
+ if (lower.includes("failed to fetch dynamically imported")) {
243
+ return "transient";
244
+ }
245
+ if (lower.includes("networkerror when attempting to fetch")) {
246
+ return "transient";
247
+ }
248
+ if (lower.includes("network request failed")) return "transient";
249
+ return "permanent";
250
+ }
251
+ async function withRetry(task, delays, shouldRetry = () => true, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
252
+ let lastErr;
253
+ for (let i = 0; i <= delays.length; i += 1) {
254
+ try {
255
+ return await task();
256
+ } catch (err) {
257
+ lastErr = err;
258
+ const attempts = i + 1;
259
+ if (i >= delays.length) break;
260
+ if (!shouldRetry(err, attempts)) break;
261
+ await sleep(delays[i]);
262
+ }
263
+ }
264
+ throw lastErr;
265
+ }
266
+ function pushDeadLetter(buffer, rec, cap = DEAD_LETTER_CAP) {
267
+ const next = buffer.length >= cap ? [...buffer.slice(buffer.length - cap + 1), rec] : [...buffer, rec];
268
+ return next;
269
+ }
270
+
271
+ // src/collab/bridge.ts
272
+ var MUTATION_TO_LAZY_GROUP = {
273
+ "sheet.mutation.add-conditional-rule": "cf",
274
+ "sheet.mutation.set-conditional-rule": "cf",
275
+ "sheet.mutation.delete-conditional-rule": "cf",
276
+ "sheet.mutation.move-conditional-rule": "cf",
277
+ "sheet.mutation.add-table": "table",
278
+ "sheet.mutation.delete-table": "table",
279
+ "sheet.mutation.set-sheet-table": "table",
280
+ "sheet.mutation.set-table-filter": "table",
281
+ "sheet.mutation.set-filter-criteria": "filter",
282
+ "sheet.mutation.set-filter-range": "filter",
283
+ "sheet.mutation.remove-filter": "filter",
284
+ "sheet.mutation.update-note": "note",
285
+ "sheet.mutation.remove-note": "note",
286
+ "sheet.mutation.add-hyper-link": "hyperlink",
287
+ "sheet.mutation.remove-hyper-link": "hyperlink",
288
+ "sheet.mutation.update-hyper-link": "hyperlink",
289
+ "data-validation.mutation.addRule": "dv",
290
+ "data-validation.mutation.removeRule": "dv",
291
+ "data-validation.mutation.updateRule": "dv",
292
+ "sheet.mutation.set-drawing-apply": "drawing"
293
+ };
294
+ var LOG_KEY = "ops";
295
+ var SYNCED_MUTATIONS = /* @__PURE__ */ new Set([
296
+ // Cell-level — values, formulas, styles, rich text.
297
+ "sheet.mutation.set-range-values",
298
+ "sheet.mutation.set-style",
299
+ // Row / column structural.
300
+ "sheet.mutation.insert-row",
301
+ "sheet.mutation.insert-col",
302
+ "sheet.mutation.remove-row",
303
+ "sheet.mutation.remove-col",
304
+ "sheet.mutation.move-rows",
305
+ "sheet.mutation.move-cols",
306
+ "sheet.mutation.set-row-hidden",
307
+ "sheet.mutation.set-row-visible",
308
+ "sheet.mutation.set-col-hidden",
309
+ "sheet.mutation.set-col-visible",
310
+ "sheet.mutation.set-worksheet-row-height",
311
+ "sheet.mutation.set-worksheet-row-is-auto-height",
312
+ "sheet.mutation.set-worksheet-col-width",
313
+ // Merges.
314
+ "sheet.mutation.add-worksheet-merge",
315
+ "sheet.mutation.remove-worksheet-merge",
316
+ // Sheet lifecycle.
317
+ "sheet.mutation.insert-sheet",
318
+ "sheet.mutation.remove-sheet",
319
+ "sheet.mutation.set-worksheet-name",
320
+ "sheet.mutation.set-worksheet-order",
321
+ // Sheet visibility — hide/show. NB: `set-worksheet-activate` is
322
+ // deliberately omitted so each peer keeps their own active sheet
323
+ // independent of which sheet another user is editing.
324
+ "sheet.mutation.set-worksheet-hidden",
325
+ // Freeze.
326
+ "sheet.mutation.set-frozen",
327
+ // Hyperlinks (sheets-hyper-link).
328
+ "sheet.mutation.add-hyper-link",
329
+ "sheet.mutation.remove-hyper-link",
330
+ "sheet.mutation.update-hyper-link",
331
+ // Tab colour — picks up the right-click "Tab color" menu.
332
+ "sheet.mutation.set-tab-color",
333
+ // Move + sort. Without these, a peer's cut-and-paste-cell-block or
334
+ // sort-range action silently doesn't appear on receivers.
335
+ "sheet.mutation.move-range",
336
+ "sheet.mutation.reorder-range",
337
+ // Per-row / per-column metadata (height, custom style, colData).
338
+ // The narrower set-worksheet-row-height / set-worksheet-col-width
339
+ // are already allowlisted; these are the broader resource-style
340
+ // mutations Univer emits for "Format → Row/Column" operations.
341
+ "sheet.mutation.set-row-data",
342
+ "sheet.mutation.set-col-data",
343
+ "sheet.mutation.set-worksheet-default-style",
344
+ // Format-as-table / sheets-table — adds/removes named tables and
345
+ // their config. Picked up so the table chrome appears on both peers.
346
+ "sheet.mutation.add-table",
347
+ "sheet.mutation.delete-table",
348
+ "sheet.mutation.set-sheet-table",
349
+ "sheet.mutation.set-table-filter",
350
+ // Autofilter (sheets-filter).
351
+ "sheet.mutation.set-filter-criteria",
352
+ "sheet.mutation.set-filter-range",
353
+ "sheet.mutation.remove-filter",
354
+ // Notes (sheets-note) — the small cell-corner indicator + popup.
355
+ "sheet.mutation.update-note",
356
+ "sheet.mutation.remove-note",
357
+ // Conditional formatting (sheets-conditional-formatting). Mutations
358
+ // are self-contained — `add` carries the full rule, `set` carries
359
+ // the patched rule, `delete` / `move` carry rule ids. Univer's
360
+ // `ConditionalFormattingRuleModel` consumes them and triggers a
361
+ // canvas re-render so highlighted cells update on peers. Existing
362
+ // rules in a downloaded seed already load via the workbook's
363
+ // resource channel; the mutations cover deltas during the session.
364
+ "sheet.mutation.add-conditional-rule",
365
+ "sheet.mutation.set-conditional-rule",
366
+ "sheet.mutation.delete-conditional-rule",
367
+ "sheet.mutation.move-conditional-rule",
368
+ // Data validation (data-validation core). NB: this package uses the
369
+ // `data-validation.mutation.*` prefix, not `sheet.mutation.*`.
370
+ // Mutation handlers live in @univerjs/data-validation; the
371
+ // sheets-data-validation plugin is the lazy-loaded integration our
372
+ // MUTATION_TO_LAZY_GROUP map keys on.
373
+ "data-validation.mutation.addRule",
374
+ "data-validation.mutation.removeRule",
375
+ "data-validation.mutation.updateRule",
376
+ // Drawings / images (sheets-drawing). Single all-purpose mutation
377
+ // wraps add / remove / update via a JSON-1 op + an enum type. Params
378
+ // can be large (embedded image blobs) — accept the bandwidth hit
379
+ // until we move drawings to a side-channel resource model.
380
+ "sheet.mutation.set-drawing-apply",
381
+ // Workbook / worksheet metadata. Each is rarely changed mid-session
382
+ // but cheap to propagate when it does happen. Without these,
383
+ // renaming the workbook or toggling gridlines silently stays
384
+ // local-only — confusing in a shared room.
385
+ // NOTE: `set-worksheet-right-to-left` is intentionally NOT here —
386
+ // neither the command nor the mutation is registered in
387
+ // @univerjs/sheets@0.22.1 (it's exported but never wired up by any
388
+ // plugin), so nothing in our app can emit it. Add it back if a
389
+ // future Univer bump registers it.
390
+ "sheet.mutation.set-workbook-name",
391
+ "sheet.mutation.set-worksheet-row-count",
392
+ "sheet.mutation.set-worksheet-column-count",
393
+ "sheet.mutation.toggle-gridlines",
394
+ "sheet.mutation.set-gridlines-color"
395
+ ]);
396
+ var REVERTABLE_MUTATIONS = /* @__PURE__ */ new Set([
397
+ "sheet.mutation.set-range-values"
398
+ ]);
399
+ var COMPACT_OPS_THRESHOLD = 200;
400
+ var COMPACT_MIN_INTERVAL_MS = 6e4;
401
+ var COMPACT_CHECK_INTERVAL_MS = 3e4;
402
+ function startBridge(api, doc, opts = {}) {
403
+ const role = opts.role ?? "write";
404
+ const injector = api._injector;
405
+ if (!injector) {
406
+ throw new Error("[collab] FUniver injector not accessible \u2014 Univer too old?");
407
+ }
408
+ const cmdSvc = injector.get(import_core.ICommandService);
409
+ const log = doc.getArray(LOG_KEY);
410
+ const myClientId = String(doc.clientID);
411
+ let splitChunkWarned = false;
412
+ let replayFailures = 0;
413
+ const replayFailureSubscribers = /* @__PURE__ */ new Set();
414
+ let deadLetter = [];
415
+ const deadLetterSubscribers = /* @__PURE__ */ new Set();
416
+ const noteReplayFailure = (rec) => {
417
+ replayFailures += 1;
418
+ deadLetter = pushDeadLetter(deadLetter, rec);
419
+ for (const cb of replayFailureSubscribers) {
420
+ try {
421
+ cb(replayFailures);
422
+ } catch (err) {
423
+ console.warn("[collab] replay-failure subscriber threw", err);
424
+ }
425
+ }
426
+ for (const cb of deadLetterSubscribers) {
427
+ try {
428
+ cb(deadLetter);
429
+ } catch (err) {
430
+ console.warn("[collab] dead-letter subscriber threw", err);
431
+ }
432
+ }
433
+ };
434
+ const pendingUndo = /* @__PURE__ */ new Map();
435
+ const subBeforeDispose = cmdSvc.beforeCommandExecuted((info, options) => {
436
+ if (role === "view") return;
437
+ if (options?.fromCollab) return;
438
+ if (!REVERTABLE_MUTATIONS.has(info.id)) return;
439
+ try {
440
+ const undo = (0, import_sheets.SetRangeValuesUndoMutationFactory)(injector, info.params);
441
+ pendingUndo.set(JSON.stringify(info.params), undo);
442
+ } catch (err) {
443
+ console.warn("[collab] failed to capture undo params for", info.id, err);
444
+ }
445
+ });
446
+ let pending = [];
447
+ let flushScheduled = false;
448
+ const flush = () => {
449
+ flushScheduled = false;
450
+ if (pending.length === 0) return;
451
+ const batch = pending;
452
+ pending = [];
453
+ doc.transact(() => {
454
+ log.push(batch);
455
+ });
456
+ };
457
+ const subDispose = cmdSvc.onMutationExecutedForCollab((info, options) => {
458
+ if (role === "view") return;
459
+ if (options?.fromCollab) return;
460
+ if (!SYNCED_MUTATIONS.has(info.id)) return;
461
+ if (!splitChunkWarned && hasSplitChunkMarker(info.params)) {
462
+ splitChunkWarned = true;
463
+ console.warn(
464
+ '[collab] mutation "%s" carries __splitChunk__ \u2014 Univer reintroduced chunked mutations; bridge needs reassembly logic. See docs/COLLAB-FIXES.md issue 8.',
465
+ info.id
466
+ );
467
+ }
468
+ const key = JSON.stringify(info.params);
469
+ const undoParams = pendingUndo.get(key);
470
+ pendingUndo.delete(key);
471
+ pending.push({
472
+ c: myClientId,
473
+ t: Date.now(),
474
+ id: info.id,
475
+ // Univer mutation params are already JSON-friendly (numbers, strings,
476
+ // plain objects). If something carries a Map / Set / cyclic ref we'll
477
+ // discover it via a runtime error; that's the signal to drop the
478
+ // mutation from SYNCED_MUTATIONS.
479
+ p: info.params,
480
+ ...undoParams !== void 0 ? { u: undoParams } : {}
481
+ });
482
+ if (!flushScheduled) {
483
+ flushScheduled = true;
484
+ queueMicrotask(flush);
485
+ }
486
+ });
487
+ let appliedCount = 0;
488
+ let replayInFlight = null;
489
+ const replayPending = () => {
490
+ if (replayInFlight) return replayInFlight;
491
+ let resolveOuter;
492
+ const p = new Promise((r) => {
493
+ resolveOuter = r;
494
+ });
495
+ replayInFlight = p;
496
+ void (async () => {
497
+ try {
498
+ while (true) {
499
+ const total = log.length;
500
+ if (appliedCount > total) appliedCount = 0;
501
+ if (appliedCount >= total) {
502
+ break;
503
+ }
504
+ const rec = log.get(appliedCount);
505
+ appliedCount += 1;
506
+ if (!rec) continue;
507
+ if (rec.c === myClientId) continue;
508
+ if (rec.kind === "snapshot") {
509
+ if (opts.onSnapshotReceived) {
510
+ try {
511
+ await opts.onSnapshotReceived(rec.wb);
512
+ } catch (err) {
513
+ console.warn("[collab] failed to apply compaction snapshot", err);
514
+ }
515
+ } else {
516
+ console.warn(
517
+ "[collab] received compaction snapshot but no handler \u2014 workbook may diverge"
518
+ );
519
+ }
520
+ continue;
521
+ }
522
+ const params = rewriteUnitId(api, rec.p, rec.id);
523
+ const sheetBefore = rec.id === "sheet.mutation.insert-sheet" ? captureActiveSheetId(api) : null;
524
+ const lazyGroup = MUTATION_TO_LAZY_GROUP[rec.id];
525
+ const attempt = () => (lazyGroup ? ensurePluginByName(lazyGroup) : Promise.resolve()).then(
526
+ () => cmdSvc.executeCommand(rec.id, params, { fromCollab: true })
527
+ );
528
+ void withRetry(
529
+ attempt,
530
+ TRANSIENT_RETRY_DELAYS_MS,
531
+ (err) => classifyReplayError(err) === "transient"
532
+ ).then(() => {
533
+ if (sheetBefore) restoreActiveSheetId(api, sheetBefore);
534
+ }).catch((err) => {
535
+ const cls = classifyReplayError(err);
536
+ const message = err instanceof Error ? err.message : String(err);
537
+ console.warn(
538
+ "[collab] replay failed for",
539
+ rec.id,
540
+ "(class:",
541
+ cls + ",",
542
+ "gave up)",
543
+ err
544
+ );
545
+ const now = Date.now();
546
+ const failure = {
547
+ id: rec.id,
548
+ params: rec.p,
549
+ lastError: message,
550
+ // Permanent = 1 attempt; transient = 1 + N retries
551
+ // configured in TRANSIENT_RETRY_DELAYS_MS.
552
+ attempts: cls === "transient" ? 1 + TRANSIENT_RETRY_DELAYS_MS.length : 1,
553
+ firstFailedAt: now,
554
+ lastFailedAt: now,
555
+ classification: cls
556
+ };
557
+ noteReplayFailure(failure);
558
+ if (sheetBefore) restoreActiveSheetId(api, sheetBefore);
559
+ });
560
+ }
561
+ } finally {
562
+ if (replayInFlight === p) replayInFlight = null;
563
+ resolveOuter();
564
+ }
565
+ })();
566
+ return p;
567
+ };
568
+ const observer = (event) => {
569
+ void event;
570
+ void replayPending();
571
+ };
572
+ log.observe(observer);
573
+ void replayPending();
574
+ let lastCompactedAt = Date.now();
575
+ let intervalHandle = null;
576
+ let idleHandle = null;
577
+ const cic = globalThis.cancelIdleCallback;
578
+ if (role !== "view" && opts.awareness) {
579
+ const awareness = opts.awareness;
580
+ const tryCompact = () => {
581
+ try {
582
+ if (log.length < COMPACT_OPS_THRESHOLD) return;
583
+ if (Date.now() - lastCompactedAt < COMPACT_MIN_INTERVAL_MS) return;
584
+ const keys = Array.from(awareness.getStates().keys());
585
+ if (keys.length === 0) return;
586
+ const designated = Math.min(...keys);
587
+ if (designated !== doc.clientID) return;
588
+ const wb = api.getActiveWorkbook();
589
+ if (!wb) return;
590
+ const snap = wb.save();
591
+ const snapshotRec = {
592
+ kind: "snapshot",
593
+ c: myClientId,
594
+ t: Date.now(),
595
+ wb: snap
596
+ };
597
+ const opsBefore = log.length;
598
+ doc.transact(() => {
599
+ log.delete(0, log.length);
600
+ log.push([snapshotRec]);
601
+ });
602
+ appliedCount = 1;
603
+ lastCompactedAt = Date.now();
604
+ console.info("[collab] op-log compacted: %d ops \u2192 1 snapshot record", opsBefore);
605
+ } catch (err) {
606
+ console.warn("[collab] compaction attempt failed", err);
607
+ }
608
+ };
609
+ const ric = globalThis.requestIdleCallback;
610
+ if (typeof ric === "function") {
611
+ const scheduleNext = () => {
612
+ idleHandle = ric(
613
+ (deadline) => {
614
+ if (deadline.didTimeout || deadline.timeRemaining() > 5) {
615
+ tryCompact();
616
+ }
617
+ scheduleNext();
618
+ },
619
+ { timeout: COMPACT_CHECK_INTERVAL_MS }
620
+ );
621
+ };
622
+ scheduleNext();
623
+ } else {
624
+ intervalHandle = setInterval(tryCompact, COMPACT_CHECK_INTERVAL_MS);
625
+ intervalHandle.unref?.();
626
+ }
627
+ globalThis.__bridgeLogLength = () => log.length;
628
+ globalThis.__bridgeForceCompact = () => {
629
+ lastCompactedAt = 0;
630
+ tryCompact();
631
+ };
632
+ }
633
+ return {
634
+ doc,
635
+ dispose: () => {
636
+ subDispose.dispose();
637
+ subBeforeDispose.dispose();
638
+ if (pending.length > 0) flush();
639
+ log.unobserve(observer);
640
+ pendingUndo.clear();
641
+ replayFailureSubscribers.clear();
642
+ deadLetterSubscribers.clear();
643
+ if (intervalHandle) clearInterval(intervalHandle);
644
+ if (idleHandle !== null && typeof cic === "function") cic(idleHandle);
645
+ },
646
+ getReplayFailures: () => replayFailures,
647
+ subscribeReplayFailures: (cb) => {
648
+ replayFailureSubscribers.add(cb);
649
+ return () => {
650
+ replayFailureSubscribers.delete(cb);
651
+ };
652
+ },
653
+ getReplayDeadLetter: () => deadLetter,
654
+ subscribeReplayDeadLetter: (cb) => {
655
+ deadLetterSubscribers.add(cb);
656
+ return () => {
657
+ deadLetterSubscribers.delete(cb);
658
+ };
659
+ }
660
+ };
661
+ }
662
+ function rewriteUnitId(api, params, mutationId) {
663
+ const wb = api.getActiveWorkbook();
664
+ if (!wb) return params;
665
+ const localUnitId = wb.getId();
666
+ let senderUnitId;
667
+ if (mutationId === "sheet.mutation.set-drawing-apply" && params && typeof params === "object") {
668
+ const u = params.unitId;
669
+ if (typeof u === "string") senderUnitId = u;
670
+ }
671
+ const rewritten = deepRewriteUnitId(params, localUnitId);
672
+ if (!senderUnitId || senderUnitId === localUnitId) return rewritten;
673
+ if (rewritten && typeof rewritten === "object") {
674
+ const r = rewritten;
675
+ const fixedOp = rewriteJson1OpPathUnitId(r.op, senderUnitId, localUnitId);
676
+ if (fixedOp !== r.op) {
677
+ return { ...r, op: fixedOp };
678
+ }
679
+ }
680
+ return rewritten;
681
+ }
682
+ function captureActiveSheetId(api) {
683
+ try {
684
+ const wb = api.getActiveWorkbook();
685
+ if (!wb) return null;
686
+ const sheet = wb.getActiveSheet();
687
+ if (!sheet) return null;
688
+ const id = sheet.getSheetId?.() ?? sheet.getId?.() ?? null;
689
+ return typeof id === "string" ? id : null;
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
694
+ function restoreActiveSheetId(api, sheetId) {
695
+ try {
696
+ const wb = api.getActiveWorkbook();
697
+ if (!wb) return;
698
+ const current = wb.getActiveSheet()?.getSheetId?.();
699
+ if (current === sheetId) return;
700
+ const sheets = wb.getSheets();
701
+ const target = sheets.find((s) => s.getSheetId?.() === sheetId);
702
+ if (!target) return;
703
+ wb.setActiveSheet?.(target);
704
+ } catch (err) {
705
+ console.warn("[collab] failed to restore active sheet after remote insert-sheet", err);
706
+ }
707
+ }
708
+ function hasSplitChunkMarker(params) {
709
+ if (!params || typeof params !== "object") return false;
710
+ return Object.prototype.hasOwnProperty.call(params, "__splitChunk__");
711
+ }
712
+
713
+ // src/collab/ws-url.ts
714
+ function buildWsUrl(server, room, role, password) {
715
+ const sep = server.includes("?") ? "&" : "?";
716
+ return `${server}${sep}room=${encodeURIComponent(room)}${password ? `&p=${encodeURIComponent(password)}` : ""}&role=${encodeURIComponent(role)}`;
717
+ }
718
+
719
+ // src/collab/attachCollab.ts
720
+ function resolveFacade(api) {
721
+ const maybe = api;
722
+ return typeof maybe.getActiveWorkbook === "function" ? api : maybe.univer;
723
+ }
724
+ function attachCollab(api, opts) {
725
+ const facade = resolveFacade(api);
726
+ const role = opts.role ?? "write";
727
+ const doc = new Y.Doc();
728
+ const ws = new import_provider.HocuspocusProviderWebsocket({
729
+ url: buildWsUrl(opts.server, opts.room, role, opts.password),
730
+ messageReconnectTimeout: 1e4
731
+ });
732
+ const provider = new import_provider.HocuspocusProvider({
733
+ websocketProvider: ws,
734
+ name: opts.room,
735
+ document: doc,
736
+ // Truthy token so the handshake completes even when the server has an
737
+ // onAuthenticate hook (used for role enforcement). See option docs above.
738
+ token: opts.token ?? "anon"
739
+ });
740
+ const bridge = startBridge(facade, doc, {
741
+ role,
742
+ awareness: provider.awareness ?? void 0,
743
+ onSnapshotReceived: opts.onSnapshot
744
+ });
745
+ let current = "connecting";
746
+ const onStatus = (ev) => {
747
+ const next = ev.status === "connected" ? "live" : ev.status === "connecting" ? "connecting" : "offline";
748
+ if (next === current) return;
749
+ current = next;
750
+ opts.onStatus?.(next);
751
+ };
752
+ provider.on("status", onStatus);
753
+ let detached = false;
754
+ const detach = () => {
755
+ if (detached) return;
756
+ detached = true;
757
+ provider.off("status", onStatus);
758
+ bridge.dispose();
759
+ provider.destroy();
760
+ doc.destroy();
761
+ };
762
+ return {
763
+ doc,
764
+ provider,
765
+ bridge,
766
+ status: () => current,
767
+ detach
768
+ };
769
+ }
770
+ //# sourceMappingURL=collab.cjs.map