@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.
- package/dist/api-BI2VLYQ6.d.cts +62 -0
- package/dist/api-BI2VLYQ6.d.ts +62 -0
- package/dist/chrome.cjs +2569 -0
- package/dist/chrome.cjs.map +1 -0
- package/dist/chrome.d.cts +96 -0
- package/dist/chrome.d.ts +96 -0
- package/dist/chrome.js +2556 -0
- package/dist/chrome.js.map +1 -0
- package/dist/collab.cjs +770 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.cts +248 -0
- package/dist/collab.d.ts +248 -0
- package/dist/collab.js +737 -0
- package/dist/collab.js.map +1 -0
- package/dist/embed/embed-runtime.js +128 -128
- package/dist/embed.cjs +166 -0
- package/dist/embed.cjs.map +1 -1
- package/dist/embed.d.cts +78 -3
- package/dist/embed.d.ts +78 -3
- package/dist/embed.js +166 -0
- package/dist/embed.js.map +1 -1
- package/dist/index.cjs +262 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +271 -168
- package/dist/index.js.map +1 -1
- package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
- package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
- package/dist/sheets.cjs +102 -16
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.d.cts +49 -63
- package/dist/sheets.d.ts +49 -63
- package/dist/sheets.js +111 -19
- package/dist/sheets.js.map +1 -1
- package/package.json +28 -3
- package/src/chrome/AutoSumPicker.tsx +176 -0
- package/src/chrome/BordersPicker.tsx +171 -0
- package/src/chrome/ChromeBottom.tsx +18 -0
- package/src/chrome/ChromeTop.tsx +21 -0
- package/src/chrome/ColorPicker.tsx +220 -0
- package/src/chrome/FindReplace.tsx +370 -0
- package/src/chrome/FormulaBar.tsx +378 -0
- package/src/chrome/Icon.tsx +43 -0
- package/src/chrome/MenuBar.tsx +336 -0
- package/src/chrome/NameBox.tsx +347 -0
- package/src/chrome/SheetTabs.tsx +346 -0
- package/src/chrome/StatusBar.tsx +232 -0
- package/src/chrome/Toolbar.tsx +401 -0
- package/src/chrome/fonts.ts +42 -0
- package/src/chrome/index.ts +24 -0
- package/src/collab/attachCollab.ts +151 -0
- package/src/collab/bridge-helpers.ts +97 -0
- package/src/collab/bridge.ts +885 -0
- package/src/collab/bridge.unit.test.ts +160 -0
- package/src/collab/index.ts +38 -0
- package/src/collab/replay-retry.ts +137 -0
- package/src/collab/replay-retry.unit.test.ts +223 -0
- package/src/collab/ws-url.ts +20 -0
- package/src/collab/ws-url.unit.test.ts +35 -0
- package/src/embed/EmbedHostTransport.ts +16 -1
- package/src/embed/EmbedTransport.ts +16 -0
- package/src/embed/EmbedTransport.unit.test.ts +88 -2
- package/src/embed/index.ts +7 -0
- package/src/embed/protocol.ts +34 -0
- package/src/embed-runtime/index.tsx +20 -0
- package/src/sheets/CasualSheets.tsx +204 -33
package/dist/collab.cjs
ADDED
|
@@ -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
|