@rowsncolumns/y-spreadsheet 14.0.32
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/LICENSE.md +100 -0
- package/Readme.md +77 -0
- package/dist/cjs/helpers.d.ts +179 -0
- package/dist/cjs/helpers.d.ts.map +1 -0
- package/dist/cjs/helpers.js +1635 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/observables.d.ts +10 -0
- package/dist/cjs/observables.d.ts.map +1 -0
- package/dist/cjs/observables.js +90 -0
- package/dist/cjs/profiling/batch-profile.spec.d.ts +2 -0
- package/dist/cjs/profiling/batch-profile.spec.d.ts.map +1 -0
- package/dist/cjs/profiling/batch-profile.spec.js +175 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/cjs/types.d.ts +62 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/use-yspreadsheet-v2.d.ts +152 -0
- package/dist/cjs/use-yspreadsheet-v2.d.ts.map +1 -0
- package/dist/cjs/use-yspreadsheet-v2.js +980 -0
- package/dist/cjs/use-yspreadsheet.d.ts +39 -0
- package/dist/cjs/use-yspreadsheet.d.ts.map +1 -0
- package/dist/cjs/use-yspreadsheet.js +430 -0
- package/dist/cjs/vitest.config.d.ts +3 -0
- package/dist/cjs/vitest.config.d.ts.map +1 -0
- package/dist/cjs/vitest.config.js +23 -0
- package/dist/esm/helpers.d.ts +179 -0
- package/dist/esm/helpers.d.ts.map +1 -0
- package/dist/esm/helpers.js +1346 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/observables.d.ts +10 -0
- package/dist/esm/observables.d.ts.map +1 -0
- package/dist/esm/observables.js +51 -0
- package/dist/esm/profiling/batch-profile.spec.d.ts +2 -0
- package/dist/esm/profiling/batch-profile.spec.d.ts.map +1 -0
- package/dist/esm/profiling/batch-profile.spec.js +131 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/esm/types.d.ts +62 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/use-yspreadsheet-v2.d.ts +152 -0
- package/dist/esm/use-yspreadsheet-v2.d.ts.map +1 -0
- package/dist/esm/use-yspreadsheet-v2.js +814 -0
- package/dist/esm/use-yspreadsheet.d.ts +39 -0
- package/dist/esm/use-yspreadsheet.d.ts.map +1 -0
- package/dist/esm/use-yspreadsheet.js +304 -0
- package/dist/esm/vitest.config.d.ts +3 -0
- package/dist/esm/vitest.config.d.ts.map +1 -0
- package/dist/esm/vitest.config.js +21 -0
- package/package.json +54 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { createNewSheet, } from "@rowsncolumns/spreadsheet";
|
|
2
|
+
import { getCellUserEnteredValue, getExtendedValueFormula, isAFormula, isNil, sortSheetsByIndex, } from "@rowsncolumns/utils";
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "@rowsncolumns/ui";
|
|
4
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { flushSync } from "react-dom";
|
|
6
|
+
import * as Y from "yjs";
|
|
7
|
+
import { addSheetDataPatches, addSheetDataPatchesV3, applyMapPatches, createCellKeyV3, filterErrorCellPatches, getCellDataFromYSheetData, mergeErrorEffectiveValueIntoCellData, processSheetDataEvents, shouldPersistErrorCellPatch, processSheetDataEventsV3, sheetDataV2ToV3, sheetDataV3ToV2, updateArrayType, validateSheets, } from "./helpers";
|
|
8
|
+
/**
|
|
9
|
+
* Convert Y.js events to SheetDataEvent format for incremental processing
|
|
10
|
+
*/
|
|
11
|
+
const convertToSheetDataEvents = (events) => {
|
|
12
|
+
return events.map((event) => {
|
|
13
|
+
const isYArrayEvent = event instanceof Y.YArrayEvent;
|
|
14
|
+
return {
|
|
15
|
+
path: event.path,
|
|
16
|
+
delta: isYArrayEvent
|
|
17
|
+
? event.changes.delta
|
|
18
|
+
: undefined,
|
|
19
|
+
isYArrayEvent,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Read user entered value directly from Y.js data to avoid race conditions
|
|
25
|
+
* with React state updates. This is used when recalcCells observer fires
|
|
26
|
+
* before sheetData observer has updated React state.
|
|
27
|
+
*/
|
|
28
|
+
const getUserEnteredValueFromYDoc = (ySheetData, sheetId, rowIndex, columnIndex) => {
|
|
29
|
+
const sheet = ySheetData.get(String(sheetId));
|
|
30
|
+
if (!sheet || rowIndex >= sheet.length)
|
|
31
|
+
return undefined;
|
|
32
|
+
const row = sheet.get(rowIndex);
|
|
33
|
+
if (!row)
|
|
34
|
+
return undefined;
|
|
35
|
+
const values = row.get("values");
|
|
36
|
+
if (!values || columnIndex >= values.length)
|
|
37
|
+
return undefined;
|
|
38
|
+
const cell = values.get(columnIndex);
|
|
39
|
+
// Extract value from ExtendedValue, matching SheetCell.getUserEnteredValue() logic
|
|
40
|
+
// ue is the abbreviated form of userEnteredValue
|
|
41
|
+
const userEnteredValue = getCellUserEnteredValue(cell);
|
|
42
|
+
return getExtendedValueFormula(userEnteredValue);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Read user entered value from V3 Y.js data structure
|
|
46
|
+
*/
|
|
47
|
+
const getUserEnteredValueFromYDocV3 = (ySheetDataV3, sheetId, rowIndex, columnIndex) => {
|
|
48
|
+
const cellKey = createCellKeyV3(sheetId, rowIndex, columnIndex);
|
|
49
|
+
const cellDataV3 = ySheetDataV3.get(cellKey);
|
|
50
|
+
return getExtendedValueFormula(getCellUserEnteredValue(cellDataV3?.value));
|
|
51
|
+
};
|
|
52
|
+
import { createDeepObservable, createObservable, createSheetDataObservable, getPatchDeltasFromEvent, } from "./observables";
|
|
53
|
+
// Add a single null entry to the YSheetData
|
|
54
|
+
const origin = "local";
|
|
55
|
+
export const useYSpreadsheetV2 = ({ onChangeSheetData, onChangeSheets, onChangeTables, onChangeCharts, onChangeEmbeds, onChangeConditionalFormats, onChangeDataValidations, onChangeCitations, onAfterBroadcastPatch, recalcCellsMaxEntries, onChangeNamedRanges, onChangeProtectedRanges, onChangePivotTables, enqueueCalculation, enqueueGraphOperation, reevaluateDerivationsForCells, onChangeCellXfs, onChangeSharedStrings, calculateNow, onChangeActiveSheet, clientOnlyFunctionPattern, initialSheets, awarenessScopeId, userId, title, sheetId, activeCell, kind, agentInitiator, selections, doc, provider, useV3 = false, sheetDataVersion = "v2", onMigrationComplete, deleteV2AfterMigration = false, skipInitialSync = false, supportLegacySharedStringsArray = false, }) => {
|
|
56
|
+
const [synced, setSynced] = useState(false);
|
|
57
|
+
const [users, setUsers] = useState([]);
|
|
58
|
+
const [isLeader, setIsLeader] = useState(false);
|
|
59
|
+
const isLeaderRef = useRef(false);
|
|
60
|
+
const resolvedAwarenessScopeId = awarenessScopeId;
|
|
61
|
+
// Listen to awareness
|
|
62
|
+
const awareness = provider?.awareness;
|
|
63
|
+
useIsomorphicLayoutEffect(() => {
|
|
64
|
+
if (!awareness)
|
|
65
|
+
return;
|
|
66
|
+
const updateLeader = () => {
|
|
67
|
+
const entries = Array.from(awareness.getStates().entries());
|
|
68
|
+
const docStates = resolvedAwarenessScopeId
|
|
69
|
+
? entries.filter(([_, state]) => state?.docGuid === resolvedAwarenessScopeId)
|
|
70
|
+
: entries;
|
|
71
|
+
if (!docStates.length) {
|
|
72
|
+
setIsLeader(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const states = docStates.map(([clientId]) => clientId);
|
|
76
|
+
const minId = Math.min(...states);
|
|
77
|
+
// Always include self in the elec
|
|
78
|
+
setIsLeader(doc.clientID === minId);
|
|
79
|
+
};
|
|
80
|
+
awareness.on("change", updateLeader);
|
|
81
|
+
updateLeader(); // Initial check
|
|
82
|
+
return () => awareness.off("change", updateLeader);
|
|
83
|
+
}, [awareness, doc, resolvedAwarenessScopeId]);
|
|
84
|
+
useIsomorphicLayoutEffect(() => {
|
|
85
|
+
isLeaderRef.current = isLeader;
|
|
86
|
+
}, [isLeader]);
|
|
87
|
+
const ySheetDataV2 = useMemo(() => doc.getMap("sheetDataV2"), [doc]);
|
|
88
|
+
// V3: Flat map structure - Map<cellKey, CellDataV3>
|
|
89
|
+
// cellKey format: `${sheetId}!${A1Address}` (e.g., "1!A1", "2!B5")
|
|
90
|
+
const ySheetDataV3 = useMemo(() => doc.getMap("sheetDataV3"), [doc]);
|
|
91
|
+
// Doc metadata - used as fallback for version when prop is undefined
|
|
92
|
+
const yDocMeta = useMemo(() => doc.getMap("docMeta"), [doc]);
|
|
93
|
+
const yDisableRecalc = useMemo(() => doc.getMap("disableRecalc"), [doc]);
|
|
94
|
+
// Recalc cells
|
|
95
|
+
const yRecalcCells = useMemo(() => doc.getArray("recalcCells"), [doc]);
|
|
96
|
+
// Make sure initialSheets is always filled
|
|
97
|
+
const getInitialSheets = useCallback(() => {
|
|
98
|
+
if (initialSheets.length) {
|
|
99
|
+
return initialSheets;
|
|
100
|
+
}
|
|
101
|
+
return [createNewSheet(1, "Sheet1")];
|
|
102
|
+
}, [initialSheets]);
|
|
103
|
+
// cellxfs
|
|
104
|
+
const yCellXfs = useMemo(() => doc.getMap("cellXfs"), [doc]);
|
|
105
|
+
// Shared strings - always use Map for new storage, Array kept for migration
|
|
106
|
+
const ySharedStringsArray = useMemo(() => doc.getArray("sharedStrings"), [doc]);
|
|
107
|
+
const ySharedStringsMap = useMemo(() => doc.getMap("sharedStringsMap"), [doc]);
|
|
108
|
+
// Track if initial sync has changed active sheet
|
|
109
|
+
const isActiveSheetChanged = useRef(false);
|
|
110
|
+
/**
|
|
111
|
+
* Handle shared strings updates from the Y.Map (new format).
|
|
112
|
+
* Merges incoming Map entries into the current shared strings state.
|
|
113
|
+
* Used for documents that store shared strings in the Map format.
|
|
114
|
+
*/
|
|
115
|
+
const handleSharedStringsMapChange = useCallback((value) => {
|
|
116
|
+
onChangeSharedStrings?.((currentMap) => {
|
|
117
|
+
const newMap = new Map(currentMap);
|
|
118
|
+
for (const [key, val] of value) {
|
|
119
|
+
newMap.set(key, val);
|
|
120
|
+
}
|
|
121
|
+
return newMap;
|
|
122
|
+
});
|
|
123
|
+
}, [onChangeSharedStrings]);
|
|
124
|
+
/**
|
|
125
|
+
* Handle shared strings updates from the Y.Array (legacy format).
|
|
126
|
+
* Converts array indices to string keys and merges into current state.
|
|
127
|
+
* Provides backwards compatibility for older documents using array storage.
|
|
128
|
+
*/
|
|
129
|
+
const handleSharedStringsArrayChange = useCallback((value) => {
|
|
130
|
+
onChangeSharedStrings?.((currentMap) => {
|
|
131
|
+
const newMap = new Map(currentMap);
|
|
132
|
+
value.forEach((v, index) => {
|
|
133
|
+
newMap.set(String(index), v);
|
|
134
|
+
});
|
|
135
|
+
return newMap;
|
|
136
|
+
});
|
|
137
|
+
}, [onChangeSharedStrings]);
|
|
138
|
+
const documentMap = useMemo(() => {
|
|
139
|
+
const entries = [
|
|
140
|
+
["sheets", [doc.getArray("sheets"), onChangeSheets]],
|
|
141
|
+
["tables", [doc.getArray("tables"), onChangeTables]],
|
|
142
|
+
[
|
|
143
|
+
"embeds",
|
|
144
|
+
[doc.getArray("embeds"), onChangeEmbeds],
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"charts",
|
|
148
|
+
[doc.getArray("charts"), onChangeCharts],
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
"conditionalFormats",
|
|
152
|
+
[
|
|
153
|
+
doc.getArray("conditionalFormats"),
|
|
154
|
+
onChangeConditionalFormats,
|
|
155
|
+
],
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
"dataValidations",
|
|
159
|
+
[
|
|
160
|
+
doc.getArray("dataValidations"),
|
|
161
|
+
onChangeDataValidations,
|
|
162
|
+
],
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"pivotTables",
|
|
166
|
+
[doc.getArray("pivotTables"), onChangePivotTables],
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"namedRanges",
|
|
170
|
+
[doc.getArray("namedRanges"), onChangeNamedRanges],
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"protectedRanges",
|
|
174
|
+
[
|
|
175
|
+
doc.getArray("protectedRanges"),
|
|
176
|
+
onChangeProtectedRanges,
|
|
177
|
+
],
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
"citations",
|
|
181
|
+
[doc.getArray("citations"), onChangeCitations],
|
|
182
|
+
],
|
|
183
|
+
[
|
|
184
|
+
"sharedStrings",
|
|
185
|
+
[ySharedStringsMap, handleSharedStringsMapChange],
|
|
186
|
+
],
|
|
187
|
+
...(supportLegacySharedStringsArray
|
|
188
|
+
? [
|
|
189
|
+
[
|
|
190
|
+
"sharedStringsArray",
|
|
191
|
+
[ySharedStringsArray, handleSharedStringsArrayChange],
|
|
192
|
+
],
|
|
193
|
+
]
|
|
194
|
+
: []),
|
|
195
|
+
];
|
|
196
|
+
return new Map(entries);
|
|
197
|
+
}, [
|
|
198
|
+
doc,
|
|
199
|
+
onChangeSheets,
|
|
200
|
+
onChangeTables,
|
|
201
|
+
onChangeEmbeds,
|
|
202
|
+
onChangeCharts,
|
|
203
|
+
onChangeConditionalFormats,
|
|
204
|
+
onChangeDataValidations,
|
|
205
|
+
onChangePivotTables,
|
|
206
|
+
onChangeNamedRanges,
|
|
207
|
+
onChangeProtectedRanges,
|
|
208
|
+
onChangeCitations,
|
|
209
|
+
ySharedStringsMap,
|
|
210
|
+
handleSharedStringsMapChange,
|
|
211
|
+
supportLegacySharedStringsArray,
|
|
212
|
+
ySharedStringsArray,
|
|
213
|
+
handleSharedStringsArrayChange,
|
|
214
|
+
]);
|
|
215
|
+
// Get backlog of recalc cells from backend
|
|
216
|
+
const getBacklog = useCallback(() => {
|
|
217
|
+
return yRecalcCells
|
|
218
|
+
.toArray()
|
|
219
|
+
.flatMap((entry) => entry.patches)
|
|
220
|
+
.filter(([, , source]) => source === "backend" || source === "agent");
|
|
221
|
+
}, [yRecalcCells]);
|
|
222
|
+
// Trigger cell recalcs
|
|
223
|
+
const syncRecalcCells = useCallback((recalcCells, origin) => {
|
|
224
|
+
for (const [_, position, source] of recalcCells) {
|
|
225
|
+
// backend will calculate results
|
|
226
|
+
const isAgent = source === "agent"; // Agent already does the calculation
|
|
227
|
+
// Read directly from Y.js to avoid race condition where recalcCells
|
|
228
|
+
// observer fires before sheetData observer has updated React state
|
|
229
|
+
const formulaValue = useV3
|
|
230
|
+
? getUserEnteredValueFromYDocV3(ySheetDataV3, position.sheetId, position.rowIndex, position.columnIndex)
|
|
231
|
+
: getUserEnteredValueFromYDoc(ySheetDataV2, position.sheetId, position.rowIndex, position.columnIndex);
|
|
232
|
+
// Was this from a changeBatch event in the back-end?
|
|
233
|
+
// To support client functions to run only
|
|
234
|
+
const isChangeBatch = position.force && isAgent;
|
|
235
|
+
const isClientOnly = !isNil(formulaValue) &&
|
|
236
|
+
!isNil(clientOnlyFunctionPattern) &&
|
|
237
|
+
isAFormula(formulaValue)
|
|
238
|
+
? clientOnlyFunctionPattern.test(String(formulaValue))
|
|
239
|
+
: false;
|
|
240
|
+
/**
|
|
241
|
+
* Determines whether to force immediate execution of the operation.
|
|
242
|
+
*
|
|
243
|
+
* Force execution is allowed only for leaders and under these conditions:
|
|
244
|
+
* - No source specified (backwards compatibility - treats as force exec)
|
|
245
|
+
* - Source is "backend" (backend-initiated changes)
|
|
246
|
+
* - Client-only mode with change batch (batch operations need immediate execution)
|
|
247
|
+
*
|
|
248
|
+
* Note: Agent sources never force execution since they've pre-calculated results.
|
|
249
|
+
*/
|
|
250
|
+
const forceExecution = isLeaderRef.current &&
|
|
251
|
+
(!source || // backwards compatible: no source means force exec for leader
|
|
252
|
+
source === "backend" ||
|
|
253
|
+
(isClientOnly && isChangeBatch));
|
|
254
|
+
const enqueue = forceExecution
|
|
255
|
+
? enqueueCalculation
|
|
256
|
+
: enqueueGraphOperation;
|
|
257
|
+
/**
|
|
258
|
+
* Selects the appropriate queue function for the operation.
|
|
259
|
+
*
|
|
260
|
+
* - enqueueCalculation: Used when forcing execution (immediate formula evaluation)
|
|
261
|
+
* - enqueueGraphOperation: Used for normal graph operations (no formula evaluation)
|
|
262
|
+
*/
|
|
263
|
+
enqueue?.({
|
|
264
|
+
type: "add",
|
|
265
|
+
position,
|
|
266
|
+
source,
|
|
267
|
+
force: forceExecution,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// The agent/backend pre-computes cell VALUES (hence the graph-only
|
|
271
|
+
// enqueues above), but conditional-format / data-validation RESULTS
|
|
272
|
+
// are derived on the client — re-evaluate them for the changed cells
|
|
273
|
+
// so their paint tracks the new values.
|
|
274
|
+
if (recalcCells.length) {
|
|
275
|
+
reevaluateDerivationsForCells?.(recalcCells.map(([, position]) => ({
|
|
276
|
+
sheetId: position.sheetId,
|
|
277
|
+
rowIndex: position.rowIndex,
|
|
278
|
+
columnIndex: position.columnIndex,
|
|
279
|
+
})));
|
|
280
|
+
}
|
|
281
|
+
}, [
|
|
282
|
+
synced,
|
|
283
|
+
clientOnlyFunctionPattern,
|
|
284
|
+
ySheetDataV2,
|
|
285
|
+
ySheetDataV3,
|
|
286
|
+
useV3,
|
|
287
|
+
reevaluateDerivationsForCells,
|
|
288
|
+
]);
|
|
289
|
+
// Setup initial sync
|
|
290
|
+
const setupInitialSync = useCallback(() => {
|
|
291
|
+
// Migration from sharedStrings (Array) to sharedStringsMap (Map) is disabled.
|
|
292
|
+
// Both formats are now supported simultaneously via handleSharedStringsMapChange
|
|
293
|
+
// and handleSharedStringsArrayChange callbacks.
|
|
294
|
+
const arrayData = ySharedStringsArray.toArray();
|
|
295
|
+
const mapData = ySharedStringsMap.toJSON();
|
|
296
|
+
const hasArrayData = arrayData.length > 0;
|
|
297
|
+
const isMapEmpty = Object.keys(mapData).length === 0;
|
|
298
|
+
// Migrate if array has data and map is empty
|
|
299
|
+
if (!supportLegacySharedStringsArray && hasArrayData && isMapEmpty) {
|
|
300
|
+
doc.transact(() => {
|
|
301
|
+
// Convert array to map with string keys
|
|
302
|
+
arrayData.forEach((value, index) => {
|
|
303
|
+
ySharedStringsMap.set(String(index), value);
|
|
304
|
+
});
|
|
305
|
+
// Optional: Clear the old array to save space
|
|
306
|
+
// Uncomment if you want to remove old data after migration
|
|
307
|
+
// ySharedStringsArray.delete(0, ySharedStringsArray.length);
|
|
308
|
+
}, "migration");
|
|
309
|
+
}
|
|
310
|
+
for (const [mappingKey, [ydoc, callback]] of documentMap.entries()) {
|
|
311
|
+
let value = ydoc.toJSON();
|
|
312
|
+
if (mappingKey === "sheets") {
|
|
313
|
+
const sheets = (Array.isArray(value) && value.length
|
|
314
|
+
? validateSheets(value)
|
|
315
|
+
: getInitialSheets());
|
|
316
|
+
callback?.(sheets);
|
|
317
|
+
// Set active sheet id
|
|
318
|
+
if (sheets.length && !isActiveSheetChanged.current) {
|
|
319
|
+
onChangeActiveSheet?.(sortSheetsByIndex(sheets)[0].sheetId);
|
|
320
|
+
isActiveSheetChanged.current = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (mappingKey === "sharedStrings") {
|
|
324
|
+
// Convert Map object to Map instance for shared strings
|
|
325
|
+
const sharedStringsMap = new Map(Object.entries(value ?? {}));
|
|
326
|
+
callback?.(sharedStringsMap);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
callback?.(value);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Load sheet data based on V2 or V3 mode
|
|
333
|
+
if (useV3) {
|
|
334
|
+
// Check if we need to migrate from V2 to V3
|
|
335
|
+
// Use prop if provided, otherwise fall back to yDocMeta for backwards compatibility
|
|
336
|
+
const resolvedVersion = sheetDataVersion ?? yDocMeta.get("sheetDataVersion");
|
|
337
|
+
if (resolvedVersion !== "v3") {
|
|
338
|
+
const v2Data = ySheetDataV2.toJSON();
|
|
339
|
+
const hasV2Data = Object.keys(v2Data).length > 0;
|
|
340
|
+
if (hasV2Data) {
|
|
341
|
+
// Migrate V2 data to V3
|
|
342
|
+
doc.transact(() => {
|
|
343
|
+
const v3Data = sheetDataV2ToV3(v2Data);
|
|
344
|
+
// Add all V3 entries to the Y.Map
|
|
345
|
+
for (const [key, cellData] of v3Data.entries()) {
|
|
346
|
+
ySheetDataV3.set(key, cellData);
|
|
347
|
+
}
|
|
348
|
+
// Optionally clear V2 data after migration
|
|
349
|
+
if (deleteV2AfterMigration) {
|
|
350
|
+
for (const sheetId of ySheetDataV2.keys()) {
|
|
351
|
+
ySheetDataV2.delete(sheetId);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}, "migrate-v2-to-v3");
|
|
355
|
+
// Notify backend that migration completed
|
|
356
|
+
onMigrationComplete?.("v3");
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// No V2 data, notify backend of version
|
|
360
|
+
onMigrationComplete?.("v3");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// V3: Convert flat map to V2 format for React state
|
|
364
|
+
const v3Data = ySheetDataV3.toJSON();
|
|
365
|
+
const v3Map = new Map(Object.entries(v3Data));
|
|
366
|
+
onChangeSheetData?.(sheetDataV3ToV2(v3Map));
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
onChangeSheetData?.(ySheetDataV2.toJSON());
|
|
370
|
+
}
|
|
371
|
+
// Cellxfs
|
|
372
|
+
const cellXfs = new Map(Object.entries(yCellXfs.toJSON() ?? {}));
|
|
373
|
+
onChangeCellXfs?.(cellXfs);
|
|
374
|
+
}, [
|
|
375
|
+
documentMap,
|
|
376
|
+
onChangeCellXfs,
|
|
377
|
+
onChangeSheetData,
|
|
378
|
+
ySheetDataV2,
|
|
379
|
+
ySheetDataV3,
|
|
380
|
+
yDocMeta,
|
|
381
|
+
yCellXfs,
|
|
382
|
+
getInitialSheets,
|
|
383
|
+
onChangeActiveSheet,
|
|
384
|
+
ySharedStringsArray,
|
|
385
|
+
ySharedStringsMap,
|
|
386
|
+
doc,
|
|
387
|
+
useV3,
|
|
388
|
+
sheetDataVersion,
|
|
389
|
+
onMigrationComplete,
|
|
390
|
+
deleteV2AfterMigration,
|
|
391
|
+
]);
|
|
392
|
+
// Synced
|
|
393
|
+
const onSynced = useCallback(() => {
|
|
394
|
+
if (!synced) {
|
|
395
|
+
if (!skipInitialSync) {
|
|
396
|
+
// Use flushSync to ensure React state is updated before continuing
|
|
397
|
+
// This guarantees sheetData is available when syncRecalcCells runs
|
|
398
|
+
flushSync(() => {
|
|
399
|
+
setupInitialSync();
|
|
400
|
+
});
|
|
401
|
+
// Build dependency graph, then sync backlog cells
|
|
402
|
+
requestAnimationFrame(async () => {
|
|
403
|
+
await calculateNow({
|
|
404
|
+
disableEvaluation: true,
|
|
405
|
+
shouldResetCellDependencyGraph: true,
|
|
406
|
+
});
|
|
407
|
+
// Sync backlog after graph is initialized
|
|
408
|
+
const backlog = getBacklog();
|
|
409
|
+
if (backlog.length) {
|
|
410
|
+
syncRecalcCells(backlog, "backend");
|
|
411
|
+
// Clear processed backlog entries from yRecalcCells
|
|
412
|
+
doc.transact(() => {
|
|
413
|
+
yRecalcCells.delete(0, yRecalcCells.length);
|
|
414
|
+
}, origin);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
setSynced(true);
|
|
419
|
+
}
|
|
420
|
+
}, [
|
|
421
|
+
calculateNow,
|
|
422
|
+
setupInitialSync,
|
|
423
|
+
synced,
|
|
424
|
+
syncRecalcCells,
|
|
425
|
+
getBacklog,
|
|
426
|
+
doc,
|
|
427
|
+
yRecalcCells,
|
|
428
|
+
skipInitialSync,
|
|
429
|
+
]);
|
|
430
|
+
// Sync state
|
|
431
|
+
if (provider?.synced && !synced) {
|
|
432
|
+
onSynced();
|
|
433
|
+
}
|
|
434
|
+
useIsomorphicLayoutEffect(() => {
|
|
435
|
+
provider?.on("synced", onSynced);
|
|
436
|
+
return () => {
|
|
437
|
+
provider?.off("synced", onSynced);
|
|
438
|
+
};
|
|
439
|
+
}, [provider]);
|
|
440
|
+
// Only for sheet Data (V2 mode)
|
|
441
|
+
useIsomorphicLayoutEffect(() => {
|
|
442
|
+
if (useV3)
|
|
443
|
+
return; // Skip V2 observer in V3 mode
|
|
444
|
+
// Subscribe to sheet data
|
|
445
|
+
const subscription = createSheetDataObservable(ySheetDataV2).subscribe((events) => {
|
|
446
|
+
/**
|
|
447
|
+
* Disable recalc is a flag that is set ONLY locally
|
|
448
|
+
* when a calculation completes
|
|
449
|
+
*
|
|
450
|
+
* This is to prevent yjs from observing sheetdata
|
|
451
|
+
* and updating it, which is not necessary
|
|
452
|
+
*/
|
|
453
|
+
if (events.length) {
|
|
454
|
+
const origin = events?.[0]?.transaction?.origin;
|
|
455
|
+
if (origin !== "local") {
|
|
456
|
+
// Convert Y.js events to SheetDataEvent format and apply incremental updates
|
|
457
|
+
const sheetDataEvents = convertToSheetDataEvents(events);
|
|
458
|
+
onChangeSheetData((prevSheetData) => {
|
|
459
|
+
const result = processSheetDataEvents(sheetDataEvents, prevSheetData, ySheetDataV2);
|
|
460
|
+
// Fall back to full serialization only if incremental update fails
|
|
461
|
+
return result ?? ySheetDataV2.toJSON();
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return () => {
|
|
467
|
+
subscription.unsubscribe();
|
|
468
|
+
};
|
|
469
|
+
}, [onChangeSheetData, ySheetDataV2, useV3]);
|
|
470
|
+
// Only for sheet Data (V3 mode)
|
|
471
|
+
useIsomorphicLayoutEffect(() => {
|
|
472
|
+
if (!useV3)
|
|
473
|
+
return; // Skip V3 observer in V2 mode
|
|
474
|
+
// Subscribe to V3 sheet data (flat Map)
|
|
475
|
+
const callback = (event) => {
|
|
476
|
+
if (event.transaction.origin !== "local") {
|
|
477
|
+
// Extract changes synchronously - Yjs requires accessing event.changes
|
|
478
|
+
// within the event handler, not in a deferred callback like setState
|
|
479
|
+
const changedKeys = new Map();
|
|
480
|
+
event.changes.keys.forEach((change, key) => {
|
|
481
|
+
changedKeys.set(key, { action: change.action });
|
|
482
|
+
});
|
|
483
|
+
// Now we can safely use the extracted data in the async setState callback
|
|
484
|
+
onChangeSheetData((prevSheetData) => {
|
|
485
|
+
const result = processSheetDataEventsV3(changedKeys, prevSheetData, ySheetDataV3);
|
|
486
|
+
if (result) {
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
// Fall back to full conversion
|
|
490
|
+
const v3Data = ySheetDataV3.toJSON();
|
|
491
|
+
const v3Map = new Map(Object.entries(v3Data));
|
|
492
|
+
return sheetDataV3ToV2(v3Map);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
ySheetDataV3.observe(callback);
|
|
497
|
+
return () => {
|
|
498
|
+
ySheetDataV3.unobserve(callback);
|
|
499
|
+
};
|
|
500
|
+
}, [onChangeSheetData, ySheetDataV3, useV3]);
|
|
501
|
+
// CellXfs
|
|
502
|
+
useIsomorphicLayoutEffect(() => {
|
|
503
|
+
// Subscribe to sheet data
|
|
504
|
+
const callback = (event) => {
|
|
505
|
+
if (event.transaction.origin !== "local") {
|
|
506
|
+
const cellXfs = new Map(Object.entries(yCellXfs.toJSON() ?? {}));
|
|
507
|
+
onChangeCellXfs?.(cellXfs);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
yCellXfs.observe(callback);
|
|
511
|
+
return () => {
|
|
512
|
+
yCellXfs.unobserve(callback);
|
|
513
|
+
};
|
|
514
|
+
}, [onChangeCellXfs, yCellXfs]);
|
|
515
|
+
// All others
|
|
516
|
+
useIsomorphicLayoutEffect(() => {
|
|
517
|
+
const subs = [];
|
|
518
|
+
for (const [mappingKey, [ydoc, callback]] of documentMap.entries()) {
|
|
519
|
+
// Only observe Arrays (shared strings Map is handled separately if needed)
|
|
520
|
+
if (ydoc instanceof Y.Array) {
|
|
521
|
+
const subscription = createDeepObservable(ydoc).subscribe((events) => {
|
|
522
|
+
for (const event of events) {
|
|
523
|
+
if (event.transaction.origin !== "local") {
|
|
524
|
+
let json = ydoc.toJSON();
|
|
525
|
+
if (Array.isArray(json) &&
|
|
526
|
+
json.length &&
|
|
527
|
+
json[0]?.userId) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
callback?.(json);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
subs.push(subscription);
|
|
535
|
+
}
|
|
536
|
+
else if (ydoc instanceof Y.Map && mappingKey === "sharedStrings") {
|
|
537
|
+
// Handle shared strings Map observation
|
|
538
|
+
const callback_map = (event) => {
|
|
539
|
+
if (event.transaction.origin !== "local") {
|
|
540
|
+
const json = ydoc.toJSON();
|
|
541
|
+
const sharedStringsMap = new Map(Object.entries(json ?? {}));
|
|
542
|
+
callback?.(sharedStringsMap);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
ydoc.observe(callback_map);
|
|
546
|
+
subs.push({
|
|
547
|
+
unsubscribe: () => ydoc.unobserve(callback_map),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return () => {
|
|
552
|
+
subs.forEach((sub) => sub.unsubscribe());
|
|
553
|
+
};
|
|
554
|
+
}, [documentMap]);
|
|
555
|
+
// Only for recalc cells
|
|
556
|
+
useIsomorphicLayoutEffect(() => {
|
|
557
|
+
if (!synced) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// Sync all cells that are updated in the backend
|
|
561
|
+
// // Observe recalc cells separat
|
|
562
|
+
const onRecalcCellsChange = (event) => {
|
|
563
|
+
const patches = getPatchDeltasFromEvent(event);
|
|
564
|
+
const origin = event.transaction.origin;
|
|
565
|
+
if (origin !== "local") {
|
|
566
|
+
requestAnimationFrame(() => {
|
|
567
|
+
syncRecalcCells(patches, origin);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
// Skip the first recalc cells change
|
|
572
|
+
const sub = createObservable(yRecalcCells).subscribe(onRecalcCellsChange);
|
|
573
|
+
return () => sub.unsubscribe();
|
|
574
|
+
}, [syncRecalcCells, recalcCellsMaxEntries, synced, yRecalcCells]);
|
|
575
|
+
// Broadcast patch
|
|
576
|
+
const onBroadcastPatch = useCallback((patches) => {
|
|
577
|
+
// Ignore push flag
|
|
578
|
+
const disableRecalc = Boolean(patches.find(([patch]) => patch?.disableRecalc));
|
|
579
|
+
// Check if this is a full recalc
|
|
580
|
+
const isFullRecalc = Boolean(patches.find(([patch]) => patch?.isFullRecalc));
|
|
581
|
+
const disableSync = isFullRecalc ? false : disableRecalc && !isLeader;
|
|
582
|
+
// Only leader can broadcast recalc patches. Error effective values are
|
|
583
|
+
// the exception: async custom-function failures (#VALUE!, #NAME?, …)
|
|
584
|
+
// only exist as disableRecalc calc-result patches, and the leader's
|
|
585
|
+
// write is deferred/lossy — if non-leaders drop them too, the error
|
|
586
|
+
// paints during the live session but the cell is blank after reload.
|
|
587
|
+
// Sync just the error-bearing cell patches; ordinary recalc results
|
|
588
|
+
// stay leader-gated so N clients don't all write identical results.
|
|
589
|
+
if (disableSync) {
|
|
590
|
+
const errorCellPatches = [];
|
|
591
|
+
for (let [patch, type = "redo"] of patches) {
|
|
592
|
+
if (!patch?.sheetData) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
type = type ?? "redo";
|
|
596
|
+
const patchKey = type === "redo" ? "patches" : "inversePatches";
|
|
597
|
+
const sheetDataPatches = patch.sheetData[patchKey];
|
|
598
|
+
if (sheetDataPatches?.length) {
|
|
599
|
+
errorCellPatches.push(...filterErrorCellPatches(sheetDataPatches));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Containment for this non-leader write path: an error may only
|
|
603
|
+
// fill a cell with no effective value (the blank-after-reload case)
|
|
604
|
+
// or replace an existing error — never a non-error result; a cell
|
|
605
|
+
// whose formula (ue) changed since the calculation started is
|
|
606
|
+
// skipped; identical persisted errors are skipped so concurrent
|
|
607
|
+
// non-leaders don't append redundant Y.js updates; and a proceeding
|
|
608
|
+
// write merges ONLY the error effective value into the existing doc
|
|
609
|
+
// cell, preserving every other persisted field.
|
|
610
|
+
const persistableErrorCellPatches = [];
|
|
611
|
+
for (const errorPatch of errorCellPatches) {
|
|
612
|
+
const [patchSheetId, rowIndex, , columnIndex] = errorPatch.path;
|
|
613
|
+
const currentCellData = useV3
|
|
614
|
+
? ySheetDataV3.get(createCellKeyV3(Number(patchSheetId), Number(rowIndex), Number(columnIndex)))?.value
|
|
615
|
+
: getCellDataFromYSheetData(ySheetDataV2, patchSheetId, Number(rowIndex), Number(columnIndex));
|
|
616
|
+
const incomingCellData = errorPatch.value;
|
|
617
|
+
if (!shouldPersistErrorCellPatch(currentCellData, incomingCellData)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
persistableErrorCellPatches.push({
|
|
621
|
+
...errorPatch,
|
|
622
|
+
value: mergeErrorEffectiveValueIntoCellData(currentCellData, incomingCellData),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (persistableErrorCellPatches.length) {
|
|
626
|
+
doc.transact(() => {
|
|
627
|
+
if (useV3) {
|
|
628
|
+
addSheetDataPatchesV3(ySheetDataV3, persistableErrorCellPatches);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
addSheetDataPatches(ySheetDataV2, persistableErrorCellPatches);
|
|
632
|
+
}
|
|
633
|
+
}, origin);
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
doc.transact(() => {
|
|
638
|
+
for (let [patch, type = "redo"] of patches) {
|
|
639
|
+
// Check for existence of patch
|
|
640
|
+
// Investigate when when importing csv, it create a [undefined, undefined] patch object
|
|
641
|
+
// todo @Vinay
|
|
642
|
+
if (!patch) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
type = type ?? "redo";
|
|
646
|
+
const patchKey = type === "redo" ? "patches" : "inversePatches";
|
|
647
|
+
if (patch.sheetData) {
|
|
648
|
+
const patches = patch.sheetData?.[patchKey];
|
|
649
|
+
if (useV3) {
|
|
650
|
+
addSheetDataPatchesV3(ySheetDataV3, patches);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
addSheetDataPatches(ySheetDataV2, patches);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (patch.cellXfs) {
|
|
657
|
+
const patches = patch.cellXfs?.[patchKey];
|
|
658
|
+
applyMapPatches(yCellXfs, patches);
|
|
659
|
+
}
|
|
660
|
+
for (const [mappingKey, [ydoc]] of documentMap.entries()) {
|
|
661
|
+
if (patch?.[mappingKey]) {
|
|
662
|
+
const patches = patch[mappingKey]?.[patchKey];
|
|
663
|
+
// Handle shared strings Map (always use Map now)
|
|
664
|
+
if (mappingKey === "sharedStrings" && ydoc instanceof Y.Map) {
|
|
665
|
+
applyMapPatches(ydoc, patches);
|
|
666
|
+
}
|
|
667
|
+
else if (ydoc instanceof Y.Array) {
|
|
668
|
+
// Check if we are updating sheets
|
|
669
|
+
if (mappingKey === "sheets" && ydoc.length === 0) {
|
|
670
|
+
// Add initial sheets
|
|
671
|
+
ydoc.insert(0, getInitialSheets());
|
|
672
|
+
}
|
|
673
|
+
updateArrayType(ydoc, patches);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Recalc cells
|
|
678
|
+
if (patch.recalcCells?.[type]) {
|
|
679
|
+
if (isLeader) {
|
|
680
|
+
// Remove old cells
|
|
681
|
+
yRecalcCells.delete(0, yRecalcCells.length);
|
|
682
|
+
}
|
|
683
|
+
yRecalcCells.push([
|
|
684
|
+
{
|
|
685
|
+
userId,
|
|
686
|
+
patches: Array.from(patch.recalcCells[type]),
|
|
687
|
+
},
|
|
688
|
+
]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}, origin);
|
|
692
|
+
onAfterBroadcastPatch?.(patches, doc);
|
|
693
|
+
}, [
|
|
694
|
+
onAfterBroadcastPatch,
|
|
695
|
+
documentMap,
|
|
696
|
+
yRecalcCells,
|
|
697
|
+
doc,
|
|
698
|
+
userId,
|
|
699
|
+
yDisableRecalc,
|
|
700
|
+
ySheetDataV2,
|
|
701
|
+
ySheetDataV3,
|
|
702
|
+
isLeader,
|
|
703
|
+
recalcCellsMaxEntries,
|
|
704
|
+
useV3,
|
|
705
|
+
]);
|
|
706
|
+
// Listen to active cell change
|
|
707
|
+
useIsomorphicLayoutEffect(() => {
|
|
708
|
+
if (activeCell) {
|
|
709
|
+
awareness?.setLocalStateField("activeCell", activeCell);
|
|
710
|
+
}
|
|
711
|
+
}, [awareness, activeCell]);
|
|
712
|
+
// Listen to sheet change
|
|
713
|
+
useIsomorphicLayoutEffect(() => {
|
|
714
|
+
awareness?.setLocalStateField("sheetId", sheetId);
|
|
715
|
+
}, [awareness, sheetId]);
|
|
716
|
+
// Listen to selection range change
|
|
717
|
+
useIsomorphicLayoutEffect(() => {
|
|
718
|
+
if (selections && selections.length > 0) {
|
|
719
|
+
awareness?.setLocalStateField("selections", selections);
|
|
720
|
+
}
|
|
721
|
+
}, [awareness, selections]);
|
|
722
|
+
// Awareness
|
|
723
|
+
useIsomorphicLayoutEffect(() => {
|
|
724
|
+
const onChangeAwareness = () => {
|
|
725
|
+
if (awareness) {
|
|
726
|
+
const collaborators = [];
|
|
727
|
+
for (const [_, value] of awareness.getStates().entries()) {
|
|
728
|
+
if (!value)
|
|
729
|
+
continue;
|
|
730
|
+
// States with no docGuid are accepted as a wildcard so REST
|
|
731
|
+
// writers or pre-scoped clients still appear as collaborators.
|
|
732
|
+
const docMatches = !resolvedAwarenessScopeId ||
|
|
733
|
+
!value.docGuid ||
|
|
734
|
+
value.docGuid === resolvedAwarenessScopeId;
|
|
735
|
+
if (!docMatches)
|
|
736
|
+
continue;
|
|
737
|
+
// Canonical shape is the nested `state.user = { kind, name, userId,
|
|
738
|
+
// agentInitiator? }` used by Olympus, pptx-studio, and the keryx
|
|
739
|
+
// agentTask helper. Fall back to legacy flat fields for one
|
|
740
|
+
// release so callers can migrate at their own pace.
|
|
741
|
+
const userBlock = (value.user ?? null);
|
|
742
|
+
const resolvedUserId = userBlock?.userId ?? value.userId;
|
|
743
|
+
const resolvedTitle = userBlock?.name ?? value.title;
|
|
744
|
+
const resolvedKind = userBlock?.kind ?? value.kind;
|
|
745
|
+
const resolvedAgentInitiator = userBlock?.agentInitiator ?? value.agentInitiator;
|
|
746
|
+
if (!resolvedUserId)
|
|
747
|
+
continue;
|
|
748
|
+
if (!value.activeCell && !value.selections)
|
|
749
|
+
continue;
|
|
750
|
+
collaborators.push({
|
|
751
|
+
activeCell: value.activeCell,
|
|
752
|
+
title: (resolvedTitle ?? ""),
|
|
753
|
+
userId: resolvedUserId,
|
|
754
|
+
sheetId: value.sheetId,
|
|
755
|
+
kind: resolvedKind,
|
|
756
|
+
agentInitiator: resolvedAgentInitiator,
|
|
757
|
+
selections: value.selections,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
setUsers(collaborators);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
// Listen to awareness
|
|
764
|
+
awareness?.on("change", onChangeAwareness);
|
|
765
|
+
return () => {
|
|
766
|
+
awareness?.off("change", onChangeAwareness);
|
|
767
|
+
};
|
|
768
|
+
}, [awareness, resolvedAwarenessScopeId]);
|
|
769
|
+
// Set local awareness state.
|
|
770
|
+
//
|
|
771
|
+
// Use setLocalStateField for each field individually so sibling consumers
|
|
772
|
+
// (Olympus's unified presence layer, agent-task helpers, etc.) can compose
|
|
773
|
+
// their own fields on the same awareness state without us clobbering them.
|
|
774
|
+
useIsomorphicLayoutEffect(() => {
|
|
775
|
+
if (!awareness)
|
|
776
|
+
return;
|
|
777
|
+
awareness.setLocalStateField("userId", userId);
|
|
778
|
+
awareness.setLocalStateField("title", title);
|
|
779
|
+
awareness.setLocalStateField("sheetId", sheetId);
|
|
780
|
+
if (resolvedAwarenessScopeId) {
|
|
781
|
+
awareness.setLocalStateField("docGuid", resolvedAwarenessScopeId);
|
|
782
|
+
}
|
|
783
|
+
// Mirror the canonical nested shape so other Athena editors can read
|
|
784
|
+
// state.user.* without falling back to the legacy flat fields.
|
|
785
|
+
const userBlock = {
|
|
786
|
+
userId,
|
|
787
|
+
name: title,
|
|
788
|
+
};
|
|
789
|
+
if (kind)
|
|
790
|
+
userBlock.kind = kind;
|
|
791
|
+
if (agentInitiator)
|
|
792
|
+
userBlock.agentInitiator = agentInitiator;
|
|
793
|
+
awareness.setLocalStateField("user", userBlock);
|
|
794
|
+
if (kind)
|
|
795
|
+
awareness.setLocalStateField("kind", kind);
|
|
796
|
+
if (agentInitiator)
|
|
797
|
+
awareness.setLocalStateField("agentInitiator", agentInitiator);
|
|
798
|
+
}, [
|
|
799
|
+
awareness,
|
|
800
|
+
resolvedAwarenessScopeId,
|
|
801
|
+
userId,
|
|
802
|
+
title,
|
|
803
|
+
sheetId,
|
|
804
|
+
kind,
|
|
805
|
+
agentInitiator,
|
|
806
|
+
]);
|
|
807
|
+
return {
|
|
808
|
+
synced,
|
|
809
|
+
users,
|
|
810
|
+
onBroadcastPatch,
|
|
811
|
+
isLeader,
|
|
812
|
+
setupInitialSync,
|
|
813
|
+
};
|
|
814
|
+
};
|