@fluidframework/matrix 2.0.0-dev.2.3.0.115467 → 2.0.0-dev.3.1.0.125672

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 (110) hide show
  1. package/.eslintrc.js +23 -11
  2. package/.mocharc.js +2 -2
  3. package/README.md +21 -21
  4. package/api-extractor.json +2 -2
  5. package/bench/bsp-set-optimizations.md +5 -5
  6. package/bench/src/index.ts +38 -20
  7. package/bench/src/read/map.ts +16 -10
  8. package/bench/src/read/nativearray.ts +16 -10
  9. package/bench/src/read/test.ts +17 -19
  10. package/bench/src/read/tiled.ts +240 -181
  11. package/bench/src/util.ts +19 -18
  12. package/bench/tsconfig.json +8 -13
  13. package/dist/bspSet.d.ts.map +1 -1
  14. package/dist/bspSet.js.map +1 -1
  15. package/dist/handlecache.d.ts.map +1 -1
  16. package/dist/handlecache.js +1 -3
  17. package/dist/handlecache.js.map +1 -1
  18. package/dist/handletable.d.ts.map +1 -1
  19. package/dist/handletable.js +7 -3
  20. package/dist/handletable.js.map +1 -1
  21. package/dist/matrix.d.ts +2 -1
  22. package/dist/matrix.d.ts.map +1 -1
  23. package/dist/matrix.js +39 -24
  24. package/dist/matrix.js.map +1 -1
  25. package/dist/ops.d.ts.map +1 -1
  26. package/dist/ops.js.map +1 -1
  27. package/dist/packageVersion.d.ts +1 -1
  28. package/dist/packageVersion.js +1 -1
  29. package/dist/packageVersion.js.map +1 -1
  30. package/dist/permutationvector.d.ts.map +1 -1
  31. package/dist/permutationvector.js +17 -10
  32. package/dist/permutationvector.js.map +1 -1
  33. package/dist/productSet.d.ts.map +1 -1
  34. package/dist/productSet.js +6 -3
  35. package/dist/productSet.js.map +1 -1
  36. package/dist/range.d.ts.map +1 -1
  37. package/dist/range.js.map +1 -1
  38. package/dist/runtime.d.ts.map +1 -1
  39. package/dist/runtime.js.map +1 -1
  40. package/dist/serialization.d.ts.map +1 -1
  41. package/dist/serialization.js.map +1 -1
  42. package/dist/sparsearray2d.d.ts.map +1 -1
  43. package/dist/sparsearray2d.js +12 -12
  44. package/dist/sparsearray2d.js.map +1 -1
  45. package/dist/split.d.ts.map +1 -1
  46. package/dist/split.js +5 -3
  47. package/dist/split.js.map +1 -1
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js.map +1 -1
  50. package/dist/undoprovider.d.ts.map +1 -1
  51. package/dist/undoprovider.js.map +1 -1
  52. package/lib/bspSet.d.ts.map +1 -1
  53. package/lib/bspSet.js.map +1 -1
  54. package/lib/handlecache.d.ts.map +1 -1
  55. package/lib/handlecache.js +1 -3
  56. package/lib/handlecache.js.map +1 -1
  57. package/lib/handletable.d.ts.map +1 -1
  58. package/lib/handletable.js +7 -3
  59. package/lib/handletable.js.map +1 -1
  60. package/lib/matrix.d.ts +2 -1
  61. package/lib/matrix.d.ts.map +1 -1
  62. package/lib/matrix.js +40 -25
  63. package/lib/matrix.js.map +1 -1
  64. package/lib/ops.d.ts.map +1 -1
  65. package/lib/ops.js.map +1 -1
  66. package/lib/packageVersion.d.ts +1 -1
  67. package/lib/packageVersion.js +1 -1
  68. package/lib/packageVersion.js.map +1 -1
  69. package/lib/permutationvector.d.ts.map +1 -1
  70. package/lib/permutationvector.js +17 -10
  71. package/lib/permutationvector.js.map +1 -1
  72. package/lib/productSet.d.ts.map +1 -1
  73. package/lib/productSet.js +6 -3
  74. package/lib/productSet.js.map +1 -1
  75. package/lib/range.d.ts.map +1 -1
  76. package/lib/range.js.map +1 -1
  77. package/lib/runtime.d.ts.map +1 -1
  78. package/lib/runtime.js.map +1 -1
  79. package/lib/serialization.d.ts.map +1 -1
  80. package/lib/serialization.js.map +1 -1
  81. package/lib/sparsearray2d.d.ts.map +1 -1
  82. package/lib/sparsearray2d.js +12 -12
  83. package/lib/sparsearray2d.js.map +1 -1
  84. package/lib/split.d.ts.map +1 -1
  85. package/lib/split.js +5 -3
  86. package/lib/split.js.map +1 -1
  87. package/lib/types.d.ts.map +1 -1
  88. package/lib/types.js.map +1 -1
  89. package/lib/undoprovider.d.ts.map +1 -1
  90. package/lib/undoprovider.js +1 -1
  91. package/lib/undoprovider.js.map +1 -1
  92. package/package.json +23 -20
  93. package/prettier.config.cjs +1 -1
  94. package/src/bspSet.ts +507 -434
  95. package/src/handlecache.ts +114 -112
  96. package/src/handletable.ts +66 -62
  97. package/src/matrix.ts +781 -696
  98. package/src/ops.ts +11 -11
  99. package/src/packageVersion.ts +1 -1
  100. package/src/permutationvector.ts +425 -368
  101. package/src/productSet.ts +852 -788
  102. package/src/range.ts +8 -8
  103. package/src/runtime.ts +35 -35
  104. package/src/serialization.ts +13 -9
  105. package/src/sparsearray2d.ts +196 -192
  106. package/src/split.ts +111 -90
  107. package/src/types.ts +3 -3
  108. package/src/undoprovider.ts +161 -144
  109. package/tsconfig.esnext.json +6 -6
  110. package/tsconfig.json +8 -12
package/src/matrix.ts CHANGED
@@ -6,27 +6,28 @@
6
6
  import { assert } from "@fluidframework/common-utils";
7
7
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
8
8
  import {
9
- IFluidDataStoreRuntime,
10
- IChannelStorageService,
11
- Serializable,
12
- IChannelAttributes,
9
+ IFluidDataStoreRuntime,
10
+ IChannelStorageService,
11
+ Serializable,
12
+ IChannelAttributes,
13
13
  } from "@fluidframework/datastore-definitions";
14
14
  import {
15
- IFluidSerializer,
16
- makeHandlesSerializable,
17
- parseHandles,
18
- SharedObject,
19
- SummarySerializer,
15
+ IFluidSerializer,
16
+ makeHandlesSerializable,
17
+ parseHandles,
18
+ SharedObject,
19
+ SummarySerializer,
20
20
  } from "@fluidframework/shared-object-base";
21
21
  import { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
22
22
  import { ObjectStoragePartition, SummaryTreeBuilder } from "@fluidframework/runtime-utils";
23
+ import { IMatrixProducer, IMatrixConsumer, IMatrixReader, IMatrixWriter } from "@tiny-calc/nano";
23
24
  import {
24
- IMatrixProducer,
25
- IMatrixConsumer,
26
- IMatrixReader,
27
- IMatrixWriter,
28
- } from "@tiny-calc/nano";
29
- import { MergeTreeDeltaType, IMergeTreeOp, SegmentGroup, ISegment } from "@fluidframework/merge-tree";
25
+ MergeTreeDeltaType,
26
+ IMergeTreeOp,
27
+ SegmentGroup,
28
+ ISegment,
29
+ Client,
30
+ } from "@fluidframework/merge-tree";
30
31
  import { MatrixOp } from "./ops";
31
32
  import { PermutationVector, PermutationSegment } from "./permutationvector";
32
33
  import { SparseArray2D } from "./sparsearray2d";
@@ -38,24 +39,24 @@ import { IUndoConsumer } from "./types";
38
39
  import { MatrixUndoProvider } from "./undoprovider";
39
40
 
40
41
  const enum SnapshotPath {
41
- rows = "rows",
42
- cols = "cols",
43
- cells = "cells",
42
+ rows = "rows",
43
+ cols = "cols",
44
+ cells = "cells",
44
45
  }
45
46
 
46
47
  interface ISetOp<T> {
47
- type: MatrixOp.set;
48
- row: number;
49
- col: number;
50
- value: MatrixItem<T>;
48
+ type: MatrixOp.set;
49
+ row: number;
50
+ col: number;
51
+ value: MatrixItem<T>;
51
52
  }
52
53
 
53
54
  interface ISetOpMetadata {
54
- rowHandle: Handle;
55
- colHandle: Handle;
56
- localSeq: number;
57
- rowsRefSeq: number;
58
- colsRefSeq: number;
55
+ rowHandle: Handle;
56
+ colHandle: Handle;
57
+ localSeq: number;
58
+ rowsRefSeq: number;
59
+ colsRefSeq: number;
59
60
  }
60
61
 
61
62
  /**
@@ -77,673 +78,757 @@ export type MatrixItem<T> = Serializable<Exclude<T, null>> | undefined;
77
78
  * for more details.)
78
79
  */
79
80
  export class SharedMatrix<T = any>
80
- extends SharedObject
81
- implements IMatrixProducer<MatrixItem<T>>,
82
- IMatrixReader<MatrixItem<T>>,
83
- IMatrixWriter<MatrixItem<T>> {
84
- private readonly consumers = new Set<IMatrixConsumer<MatrixItem<T>>>();
85
-
86
- public static getFactory() { return new SharedMatrixFactory(); }
87
-
88
- private readonly rows: PermutationVector; // Map logical row to storage handle (if any)
89
- private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
90
-
91
- private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
92
- private pending = new SparseArray2D<number>(); // Tracks pending writes.
93
-
94
- constructor(runtime: IFluidDataStoreRuntime, public id: string, attributes: IChannelAttributes) {
95
- super(id, runtime, attributes, "fluid_matrix_");
96
-
97
- this.rows = new PermutationVector(
98
- SnapshotPath.rows,
99
- this.logger,
100
- runtime,
101
- this.onRowDelta,
102
- this.onRowHandlesRecycled);
103
-
104
- this.cols = new PermutationVector(
105
- SnapshotPath.cols,
106
- this.logger,
107
- runtime,
108
- this.onColDelta,
109
- this.onColHandlesRecycled);
110
- }
111
-
112
- private undo?: MatrixUndoProvider<T>;
113
-
114
- /**
115
- * Subscribes the given IUndoConsumer to the matrix.
116
- */
117
- public openUndo(consumer: IUndoConsumer) {
118
- assert(this.undo === undefined,
119
- 0x019 /* "SharedMatrix.openUndo() supports at most a single IUndoConsumer." */);
120
-
121
- this.undo = new MatrixUndoProvider(consumer, this, this.rows, this.cols);
122
- }
123
-
124
- // TODO: closeUndo()?
125
-
126
- private get rowHandles() { return this.rows.handleCache; }
127
- private get colHandles() { return this.cols.handleCache; }
128
-
129
- /**
130
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
131
- */
132
- public static create<T>(runtime: IFluidDataStoreRuntime, id?: string) {
133
- return runtime.createChannel(id, SharedMatrixFactory.Type) as SharedMatrix<T>;
134
- }
135
-
136
- // #region IMatrixProducer
137
-
138
- openMatrix(
139
- consumer: IMatrixConsumer<MatrixItem<T>>,
140
- ): IMatrixReader<MatrixItem<T>> {
141
- this.consumers.add(consumer);
142
- return this;
143
- }
144
-
145
- closeMatrix(consumer: IMatrixConsumer<MatrixItem<T>>): void {
146
- this.consumers.delete(consumer);
147
- }
148
-
149
- // #endregion IMatrixProducer
150
-
151
- // #region IMatrixReader
152
-
153
- public get rowCount() { return this.rows.getLength(); }
154
- public get colCount() { return this.cols.getLength(); }
155
-
156
- public getCell(row: number, col: number): MatrixItem<T> {
157
- // Perf: When possible, bounds checking is performed inside the implementation for
158
- // 'getHandle()' so that it can be elided in the case of a cache hit. This
159
- // yields an ~40% improvement in the case of a cache hit (node v12 x64)
160
-
161
- // Map the logical (row, col) to associated storage handles.
162
- const rowHandle = this.rowHandles.getHandle(row);
163
- if (isHandleValid(rowHandle)) {
164
- const colHandle = this.colHandles.getHandle(col);
165
- if (isHandleValid(colHandle)) {
166
- return this.cells.getCell(rowHandle, colHandle);
167
- }
168
- } else {
169
- // If we early exit because the given rowHandle is unallocated, we still need to
170
- // bounds-check the 'col' parameter.
171
- ensureRange(col, this.cols.getLength());
172
- }
173
-
174
- return undefined;
175
- }
176
-
177
- public get matrixProducer(): IMatrixProducer<MatrixItem<T>> { return this; }
178
-
179
- // #endregion IMatrixReader
180
-
181
- public setCell(row: number, col: number, value: MatrixItem<T>) {
182
- assert(0 <= row && row < this.rowCount
183
- && 0 <= col && col < this.colCount,
184
- 0x01a /* "Trying to set out-of-bounds cell!" */);
185
-
186
- this.setCellCore(row, col, value);
187
-
188
- // Avoid reentrancy by raising change notifications after the op is queued.
189
- for (const consumer of this.consumers.values()) {
190
- consumer.cellsChanged(row, col, 1, 1, this);
191
- }
192
- }
193
-
194
- public setCells(
195
- rowStart: number,
196
- colStart: number,
197
- colCount: number,
198
- values: readonly (MatrixItem<T>)[],
199
- ) {
200
- const rowCount = Math.ceil(values.length / colCount);
201
-
202
- assert((0 <= rowStart && rowStart < this.rowCount)
203
- && (0 <= colStart && colStart < this.colCount)
204
- && (1 <= colCount && colCount <= (this.colCount - colStart))
205
- && (rowCount <= (this.rowCount - rowStart)),
206
- 0x01b /* "Trying to set multiple out-of-bounds cells!" */);
207
-
208
- const endCol = colStart + colCount;
209
- let r = rowStart;
210
- let c = colStart;
211
-
212
- for (const value of values) {
213
- this.setCellCore(r, c, value);
214
-
215
- if (++c === endCol) {
216
- c = colStart;
217
- r++;
218
- }
219
- }
220
-
221
- // Avoid reentrancy by raising change notifications after the op is queued.
222
- for (const consumer of this.consumers.values()) {
223
- consumer.cellsChanged(rowStart, colStart, rowCount, colCount, this);
224
- }
225
- }
226
-
227
- private setCellCore(
228
- row: number,
229
- col: number,
230
- value: MatrixItem<T>,
231
- rowHandle = this.rows.getAllocatedHandle(row),
232
- colHandle = this.cols.getAllocatedHandle(col),
233
- ) {
234
- if (this.undo !== undefined) {
235
- let oldValue = this.cells.getCell(rowHandle, colHandle);
236
- if (oldValue === null) {
237
- oldValue = undefined;
238
- }
239
-
240
- this.undo.cellSet(
241
- rowHandle,
242
- colHandle,
243
- oldValue);
244
- }
245
-
246
- this.cells.setCell(rowHandle, colHandle, value);
247
-
248
- if (this.isAttached()) {
249
- this.sendSetCellOp(row, col, value, rowHandle, colHandle);
250
- }
251
- }
252
-
253
- private sendSetCellOp(
254
- row: number,
255
- col: number,
256
- value: MatrixItem<T>,
257
- rowHandle: Handle,
258
- colHandle: Handle,
259
- localSeq = this.nextLocalSeq(),
260
- rowsRefSeq = this.rows.getCollabWindow().currentSeq,
261
- colsRefSeq = this.cols.getCollabWindow().currentSeq,
262
- ) {
263
- assert(this.isAttached(), 0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */);
264
-
265
- const op: ISetOp<T> = {
266
- type: MatrixOp.set,
267
- row,
268
- col,
269
- value,
270
- };
271
-
272
- const metadata: ISetOpMetadata = {
273
- rowHandle,
274
- colHandle,
275
- localSeq,
276
- rowsRefSeq,
277
- colsRefSeq,
278
- };
279
-
280
- this.submitLocalMessage(op, metadata);
281
- this.pending.setCell(rowHandle, colHandle, localSeq);
282
- }
283
-
284
- private submitVectorMessage(
285
- currentVector: PermutationVector,
286
- oppositeVector: PermutationVector,
287
- dimension: SnapshotPath.rows | SnapshotPath.cols,
288
- message: any,
289
- ) {
290
- // Ideally, we would have a single 'localSeq' counter that is shared between both PermutationVectors
291
- // and the SharedMatrix's cell data. Instead, we externally advance each MergeTree's 'localSeq' counter
292
- // for each submitted op it not aware of to keep them synchronized.
293
- const localSeq = currentVector.getCollabWindow().localSeq;
294
- const oppositeWindow = oppositeVector.getCollabWindow();
295
-
296
- // Note that the comparison is '>=' because, in the case the MergeTree is regenerating ops for reconnection,
297
- // the MergeTree submits the op with the original 'localSeq'.
298
- assert(localSeq >= oppositeWindow.localSeq,
299
- 0x01c /* "The 'localSeq' of the vector submitting an op must >= the 'localSeq' of the other vector." */);
300
-
301
- oppositeWindow.localSeq = localSeq;
302
-
303
- // If the SharedMatrix is local, it's state will be submitted via a Snapshot when initially connected.
304
- // Do not queue a message or track the pending op, as there will never be an ACK, etc.
305
- if (this.isAttached()) {
306
- // Record whether this `op` targets rows or cols. (See dispatch in `processCore()`)
307
- message.target = dimension;
308
-
309
- this.submitLocalMessage(
310
- message,
311
- currentVector.peekPendingSegmentGroups(
312
- message.type === MergeTreeDeltaType.GROUP
313
- ? message.ops.length
314
- : 1));
315
- }
316
- }
317
-
318
- private submitColMessage(message: any) {
319
- this.submitVectorMessage(this.cols, this.rows, SnapshotPath.cols, message);
320
- }
321
-
322
- public insertCols(colStart: number, count: number) {
323
- this.submitColMessage(this.cols.insert(colStart, count));
324
- }
325
-
326
- public removeCols(colStart: number, count: number) {
327
- this.submitColMessage(this.cols.remove(colStart, count));
328
- }
329
-
330
- private submitRowMessage(message: any) {
331
- this.submitVectorMessage(this.rows, this.cols, SnapshotPath.rows, message);
332
- }
333
-
334
- public insertRows(rowStart: number, count: number) {
335
- this.submitRowMessage(this.rows.insert(rowStart, count));
336
- }
337
-
338
- public removeRows(rowStart: number, count: number) {
339
- this.submitRowMessage(this.rows.remove(rowStart, count));
340
- }
341
-
342
- /** @internal */ public _undoRemoveRows(segment: ISegment) {
343
- const original = segment as PermutationSegment;
344
-
345
- // (Re)insert the removed number of rows at the original position.
346
- const { op, inserted } = this.rows.insertRelative(original, original.cachedLength);
347
- this.submitRowMessage(op);
348
-
349
- // Transfer handles and undo/redo tracking groups from the original segment to the
350
- // newly inserted segment.
351
- original.transferToReplacement(inserted);
352
-
353
- // Invalidate the handleCache in case it was populated during the 'rowsChanged'
354
- // callback, which occurs before the handle span is populated.
355
- const rowStart = this.rows.getPosition(inserted);
356
- this.rows.handleCache.itemsChanged(
357
- rowStart,
358
- /* removedCount: */ 0,
359
- /* insertedCount: */ inserted.cachedLength);
360
-
361
- // Generate setCell ops for each populated cell in the reinserted rows.
362
- let rowHandle = inserted.start;
363
- const rowCount = inserted.cachedLength;
364
- for (let row = rowStart; row < rowStart + rowCount; row++, rowHandle++) {
365
- for (let col = 0; col < this.colCount; col++) {
366
- const colHandle = this.colHandles.getHandle(col);
367
- const value = this.cells.getCell(rowHandle, colHandle);
368
- if (this.isAttached() && value !== undefined && value !== null) {
369
- this.sendSetCellOp(
370
- row,
371
- col,
372
- value,
373
- rowHandle,
374
- colHandle);
375
- }
376
- }
377
- }
378
-
379
- // Avoid reentrancy by raising change notifications after the op is queued.
380
- for (const consumer of this.consumers.values()) {
381
- consumer.cellsChanged(rowStart, /* colStart: */ 0, rowCount, this.colCount, this);
382
- }
383
- }
384
-
385
- /** @internal */ public _undoRemoveCols(segment: ISegment) {
386
- const original = segment as PermutationSegment;
387
-
388
- // (Re)insert the removed number of columns at the original position.
389
- const { op, inserted } = this.cols.insertRelative(original, original.cachedLength);
390
- this.submitColMessage(op);
391
-
392
- // Transfer handles and undo/redo tracking groups from the original segment to the
393
- // newly inserted segment.
394
- original.transferToReplacement(inserted);
395
-
396
- // Invalidate the handleCache in case it was populated during the 'colsChanged'
397
- // callback, which occurs before the handle span is populated.
398
- const colStart = this.cols.getPosition(inserted);
399
- this.cols.handleCache.itemsChanged(
400
- colStart,
401
- /* removedCount: */ 0,
402
- /* insertedCount: */ inserted.cachedLength);
403
-
404
- // Generate setCell ops for each populated cell in the reinserted cols.
405
- let colHandle = inserted.start;
406
- const colCount = inserted.cachedLength;
407
- for (let col = colStart; col < colStart + colCount; col++, colHandle++) {
408
- for (let row = 0; row < this.rowCount; row++) {
409
- const rowHandle = this.rowHandles.getHandle(row);
410
- const value = this.cells.getCell(rowHandle, colHandle);
411
- if (this.isAttached() && value !== undefined && value !== null) {
412
- this.sendSetCellOp(
413
- row,
414
- col,
415
- value,
416
- rowHandle,
417
- colHandle);
418
- }
419
- }
420
- }
421
-
422
- // Avoid reentrancy by raising change notifications after the op is queued.
423
- for (const consumer of this.consumers.values()) {
424
- consumer.cellsChanged(/* rowStart: */ 0, colStart, this.rowCount, colCount, this);
425
- }
426
- }
427
-
428
- protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
429
- const builder = new SummaryTreeBuilder();
430
- builder.addWithStats(SnapshotPath.rows, this.rows.summarize(this.runtime, this.handle, serializer));
431
- builder.addWithStats(SnapshotPath.cols, this.cols.summarize(this.runtime, this.handle, serializer));
432
- builder.addBlob(SnapshotPath.cells,
433
- serializer.stringify([
434
- this.cells.snapshot(),
435
- this.pending.snapshot(),
436
- ], this.handle));
437
- return builder.getSummaryTree();
438
- }
439
-
440
- /**
441
- * Runs serializer on the GC data for this SharedMatrix.
442
- * All the IFluidHandle's stored in the cells represent routes to other objects.
443
- */
444
- protected processGCDataCore(serializer: SummarySerializer) {
445
- for (let row = 0; row < this.rowCount; row++) {
446
- for (let col = 0; col < this.colCount; col++) {
447
- serializer.stringify(this.getCell(row, col), this.handle);
448
- }
449
- }
450
- }
451
-
452
- /**
453
- * Advances the 'localSeq' counter for the cell data operation currently being queued.
454
- *
455
- * Do not use with 'submitColMessage()/submitRowMessage()' as these helpers + the MergeTree will
456
- * automatically advance 'localSeq'.
457
- */
458
- private nextLocalSeq() {
459
- // Ideally, we would have a single 'localSeq' counter that is shared between both PermutationVectors
460
- // and the SharedMatrix's cell data. Instead, we externally bump each MergeTree's 'localSeq' counter
461
- // for SharedMatrix ops it's not aware of to keep them synchronized. (For cell data operations, we
462
- // need to bump both counters.)
463
-
464
- this.cols.getCollabWindow().localSeq++;
465
- return ++this.rows.getCollabWindow().localSeq;
466
- }
467
-
468
- protected submitLocalMessage(message: any, localOpMetadata?: any) {
469
- // TODO: Recommend moving this assertion into SharedObject
470
- // (See https://github.com/microsoft/FluidFramework/issues/2559)
471
- assert(this.isAttached() === true, 0x01d /* "Trying to submit message to runtime while detached!" */);
472
-
473
- super.submitLocalMessage(makeHandlesSerializable(message, this.serializer, this.handle), localOpMetadata);
474
-
475
- // Ensure that row/col 'localSeq' are synchronized (see 'nextLocalSeq()').
476
- assert(
477
- this.rows.getCollabWindow().localSeq === this.cols.getCollabWindow().localSeq,
478
- 0x01e /* "Row and col collab window 'localSeq' desynchronized!" */,
479
- );
480
- }
481
-
482
- protected didAttach() {
483
- // We've attached we need to start generating and sending ops.
484
- // so start collaboration and provide a default client id incase we are not connected
485
- if (this.isAttached()) {
486
- this.rows.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
487
- this.cols.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
488
- }
489
- }
490
-
491
- protected onConnect() {
492
- assert(this.rows.getCollabWindow().collaborating === this.cols.getCollabWindow().collaborating,
493
- 0x01f /* "Row and col collab window 'collaborating' status desynchronized!" */);
494
-
495
- // Update merge tree collaboration information with new client ID and then resend pending ops
496
- this.rows.startOrUpdateCollaboration(this.runtime.clientId as string);
497
- this.cols.startOrUpdateCollaboration(this.runtime.clientId as string);
498
- }
499
-
500
- protected reSubmitCore(content: any, localOpMetadata: unknown) {
501
- switch (content.target) {
502
- case SnapshotPath.cols:
503
- this.submitColMessage(this.cols.regeneratePendingOp(
504
- content as IMergeTreeOp,
505
- localOpMetadata as SegmentGroup | SegmentGroup[]));
506
- break;
507
- case SnapshotPath.rows:
508
- this.submitRowMessage(this.rows.regeneratePendingOp(
509
- content as IMergeTreeOp,
510
- localOpMetadata as SegmentGroup | SegmentGroup[]));
511
- break;
512
- default: {
513
- assert(content.type === MatrixOp.set, 0x020 /* "Unknown SharedMatrix 'op' type." */);
514
-
515
- const setOp = content as ISetOp<T>;
516
- const { rowHandle, colHandle, localSeq, rowsRefSeq, colsRefSeq } = localOpMetadata as ISetOpMetadata;
517
-
518
- // If there are more pending local writes to the same row/col handle, it is important
519
- // to skip resubmitting this op since it is possible the row/col handle has been recycled
520
- // and now refers to a different position than when this op was originally submitted.
521
- if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
522
- const row = this.rows.rebasePositionWithoutSegmentSlide(setOp.row, rowsRefSeq, localSeq);
523
- const col = this.cols.rebasePositionWithoutSegmentSlide(setOp.col, colsRefSeq, localSeq);
524
-
525
- if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
526
- this.sendSetCellOp(
527
- row,
528
- col,
529
- setOp.value,
530
- rowHandle,
531
- colHandle,
532
- localSeq,
533
- rowsRefSeq,
534
- colsRefSeq,
535
- );
536
- }
537
- }
538
- break;
539
- }
540
- }
541
- }
542
-
543
- protected onDisconnect() {}
544
-
545
- /**
546
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
547
- */
548
- protected async loadCore(storage: IChannelStorageService) {
549
- try {
550
- await this.rows.load(
551
- this.runtime,
552
- new ObjectStoragePartition(storage, SnapshotPath.rows),
553
- this.serializer);
554
- await this.cols.load(
555
- this.runtime,
556
- new ObjectStoragePartition(storage, SnapshotPath.cols),
557
- this.serializer);
558
- const [cellData, pendingCliSeqData] = await deserializeBlob(storage, SnapshotPath.cells, this.serializer);
559
-
560
- this.cells = SparseArray2D.load(cellData);
561
- this.pending = SparseArray2D.load(pendingCliSeqData);
562
- } catch (error) {
563
- this.logger.sendErrorEvent({ eventName: "MatrixLoadFailed" }, error);
564
- }
565
- }
566
-
567
- protected processCore(rawMessage: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) {
568
- const msg = parseHandles(rawMessage, this.serializer);
569
-
570
- const contents = msg.contents;
571
-
572
- switch (contents.target) {
573
- case SnapshotPath.cols:
574
- this.cols.applyMsg(msg, local);
575
- break;
576
- case SnapshotPath.rows:
577
- this.rows.applyMsg(msg, local);
578
- break;
579
- default: {
580
- assert(contents.type === MatrixOp.set,
581
- 0x021 /* "SharedMatrix message contents have unexpected type!" */);
582
-
583
- const { row, col } = contents;
584
-
585
- if (local) {
586
- // We are receiving the ACK for a local pending set operation.
587
- const { rowHandle, colHandle, localSeq } = localOpMetadata as ISetOpMetadata;
588
-
589
- // If this is the most recent write to the cell by the local client, remove our
590
- // entry from 'pendingCliSeqs' to resume allowing remote writes.
591
- if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
592
- this.pending.setCell(rowHandle, colHandle, undefined);
593
- }
594
- } else {
595
- const adjustedRow = this.rows.adjustPosition(row, rawMessage);
596
-
597
- if (adjustedRow !== undefined) {
598
- const adjustedCol = this.cols.adjustPosition(col, rawMessage);
599
-
600
- if (adjustedCol !== undefined) {
601
- const rowHandle = this.rows.getAllocatedHandle(adjustedRow);
602
- const colHandle = this.cols.getAllocatedHandle(adjustedCol);
603
-
604
- assert(isHandleValid(rowHandle) && isHandleValid(colHandle),
605
- 0x022 /* "SharedMatrix row and/or col handles are invalid!" */);
606
-
607
- // If there is a pending (unACKed) local write to the same cell, skip the current op
608
- // since it "happened before" the pending write.
609
- if (this.pending.getCell(rowHandle, colHandle) === undefined) {
610
- const { value } = contents;
611
- this.cells.setCell(rowHandle, colHandle, value);
612
-
613
- for (const consumer of this.consumers.values()) {
614
- consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
615
- }
616
- }
617
- }
618
- }
619
- }
620
- }
621
- }
622
- }
623
-
624
- // Invoked by PermutationVector to notify IMatrixConsumers of row insertion/deletions.
625
- private readonly onRowDelta = (position: number, removedCount: number, insertedCount: number) => {
626
- for (const consumer of this.consumers) {
627
- consumer.rowsChanged(position, removedCount, insertedCount, this);
628
- }
629
- };
630
-
631
- // Invoked by PermutationVector to notify IMatrixConsumers of col insertion/deletions.
632
- private readonly onColDelta = (position: number, removedCount: number, insertedCount: number) => {
633
- for (const consumer of this.consumers) {
634
- consumer.colsChanged(position, removedCount, insertedCount, this);
635
- }
636
- };
637
-
638
- private readonly onRowHandlesRecycled = (rowHandles: Handle[]) => {
639
- for (const rowHandle of rowHandles) {
640
- this.cells.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
641
- this.pending.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
642
- }
643
- };
644
-
645
- private readonly onColHandlesRecycled = (colHandles: Handle[]) => {
646
- for (const colHandle of colHandles) {
647
- this.cells.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
648
- this.pending.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
649
- }
650
- };
651
-
652
- /**
653
- * Returns true if the latest pending write to the cell indicated by the given row/col handles
654
- * matches the given 'localSeq'.
655
- *
656
- * A return value of `true` indicates that there are no later local operations queued that will
657
- * clobber the write op at the given 'localSeq'. This includes later ops that overwrite the cell
658
- * with a different value as well as row/col removals that might recycled the given row/col handles.
659
- */
660
- private isLatestPendingWrite(rowHandle: Handle, colHandle: Handle, localSeq: number) {
661
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
662
- const pendingLocalSeq = this.pending.getCell(rowHandle, colHandle)!;
663
-
664
- // Note while we're awaiting the ACK for a local set, it's possible for the row/col to be
665
- // locally removed and the row/col handles recycled. If this happens, the pendingLocalSeq will
666
- // be 'undefined' or > 'localSeq'.
667
- assert(!(pendingLocalSeq < localSeq),
668
- // eslint-disable-next-line max-len
669
- 0x023 /* "The 'localSeq' of pending write (if any) must be <= the localSeq of the currently processed op." */);
670
-
671
- // If this is the most recent write to the cell by the local client, the stored localSeq
672
- // will be an exact match for the given 'localSeq'.
673
- return pendingLocalSeq === localSeq;
674
- }
675
-
676
- public toString() {
677
- let s = `client:${this.runtime.clientId}\nrows: ${this.rows.toString()}\ncols: ${this.cols.toString()}\n\n`;
678
-
679
- for (let r = 0; r < this.rowCount; r++) {
680
- s += ` [`;
681
- for (let c = 0; c < this.colCount; c++) {
682
- if (c > 0) {
683
- s += ", ";
684
- }
685
-
686
- s += `${this.serializer.stringify(this.getCell(r, c), this.handle)}`;
687
- }
688
- s += "]\n";
689
- }
690
-
691
- return `${s}\n`;
692
- }
693
-
694
- /**
695
- * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
696
- */
697
- protected applyStashedOp(content: any): unknown {
698
- if (content.target === SnapshotPath.cols || content.target === SnapshotPath.rows) {
699
- const op = content as IMergeTreeOp;
700
- const currentVector = content.target === SnapshotPath.cols ? this.cols : this.rows;
701
- const oppositeVector = content.target === SnapshotPath.cols ? this.rows : this.cols;
702
- const metadata = currentVector.applyStashedOp(op);
703
- const localSeq = currentVector.getCollabWindow().localSeq;
704
- const oppositeWindow = oppositeVector.getCollabWindow();
705
-
706
- assert(
707
- localSeq > oppositeWindow.localSeq,
708
- 0x2d9,
709
- /* "The 'localSeq' of the vector applying stashed op must > the 'localSeq' of the other vector." */
710
- );
711
-
712
- oppositeWindow.localSeq = localSeq;
713
-
714
- return metadata;
715
- } else {
716
- assert(content.type === MatrixOp.set, 0x2da /* "Unknown SharedMatrix 'op' type." */);
717
-
718
- const setOp = content as ISetOp<T>;
719
- const rowHandle = this.rows.getAllocatedHandle(setOp.row);
720
- const colHandle = this.cols.getAllocatedHandle(setOp.col);
721
- const rowsRefSeq = this.rows.getCollabWindow().currentSeq;
722
- const colsRefSeq = this.cols.getCollabWindow().currentSeq;
723
- if (this.undo !== undefined) {
724
- let oldValue = this.cells.getCell(rowHandle, colHandle);
725
- if (oldValue === null) {
726
- oldValue = undefined;
727
- }
728
-
729
- this.undo.cellSet(
730
- rowHandle,
731
- colHandle,
732
- oldValue);
733
- }
734
-
735
- this.cells.setCell(rowHandle, colHandle, setOp.value);
736
- const localSeq = this.nextLocalSeq();
737
- const metadata: ISetOpMetadata = {
738
- rowHandle,
739
- colHandle,
740
- localSeq,
741
- rowsRefSeq,
742
- colsRefSeq,
743
- };
744
-
745
- this.pending.setCell(rowHandle, colHandle, localSeq);
746
- return metadata;
747
- }
748
- }
81
+ extends SharedObject
82
+ implements
83
+ IMatrixProducer<MatrixItem<T>>,
84
+ IMatrixReader<MatrixItem<T>>,
85
+ IMatrixWriter<MatrixItem<T>>
86
+ {
87
+ private readonly consumers = new Set<IMatrixConsumer<MatrixItem<T>>>();
88
+
89
+ public static getFactory() {
90
+ return new SharedMatrixFactory();
91
+ }
92
+
93
+ private readonly rows: PermutationVector; // Map logical row to storage handle (if any)
94
+ private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
95
+
96
+ private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
97
+ private pending = new SparseArray2D<number>(); // Tracks pending writes.
98
+
99
+ constructor(
100
+ runtime: IFluidDataStoreRuntime,
101
+ public id: string,
102
+ attributes: IChannelAttributes,
103
+ ) {
104
+ super(id, runtime, attributes, "fluid_matrix_");
105
+
106
+ this.rows = new PermutationVector(
107
+ SnapshotPath.rows,
108
+ this.logger,
109
+ runtime,
110
+ this.onRowDelta,
111
+ this.onRowHandlesRecycled,
112
+ );
113
+
114
+ this.cols = new PermutationVector(
115
+ SnapshotPath.cols,
116
+ this.logger,
117
+ runtime,
118
+ this.onColDelta,
119
+ this.onColHandlesRecycled,
120
+ );
121
+ }
122
+
123
+ private undo?: MatrixUndoProvider<T>;
124
+
125
+ /**
126
+ * Subscribes the given IUndoConsumer to the matrix.
127
+ */
128
+ public openUndo(consumer: IUndoConsumer) {
129
+ assert(
130
+ this.undo === undefined,
131
+ 0x019 /* "SharedMatrix.openUndo() supports at most a single IUndoConsumer." */,
132
+ );
133
+
134
+ this.undo = new MatrixUndoProvider(consumer, this, this.rows, this.cols);
135
+ }
136
+
137
+ // TODO: closeUndo()?
138
+
139
+ private get rowHandles() {
140
+ return this.rows.handleCache;
141
+ }
142
+ private get colHandles() {
143
+ return this.cols.handleCache;
144
+ }
145
+
146
+ /**
147
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
148
+ */
149
+ public static create<T>(runtime: IFluidDataStoreRuntime, id?: string) {
150
+ return runtime.createChannel(id, SharedMatrixFactory.Type) as SharedMatrix<T>;
151
+ }
152
+
153
+ // #region IMatrixProducer
154
+
155
+ openMatrix(consumer: IMatrixConsumer<MatrixItem<T>>): IMatrixReader<MatrixItem<T>> {
156
+ this.consumers.add(consumer);
157
+ return this;
158
+ }
159
+
160
+ closeMatrix(consumer: IMatrixConsumer<MatrixItem<T>>): void {
161
+ this.consumers.delete(consumer);
162
+ }
163
+
164
+ // #endregion IMatrixProducer
165
+
166
+ // #region IMatrixReader
167
+
168
+ public get rowCount() {
169
+ return this.rows.getLength();
170
+ }
171
+ public get colCount() {
172
+ return this.cols.getLength();
173
+ }
174
+
175
+ public getCell(row: number, col: number): MatrixItem<T> {
176
+ // Perf: When possible, bounds checking is performed inside the implementation for
177
+ // 'getHandle()' so that it can be elided in the case of a cache hit. This
178
+ // yields an ~40% improvement in the case of a cache hit (node v12 x64)
179
+
180
+ // Map the logical (row, col) to associated storage handles.
181
+ const rowHandle = this.rowHandles.getHandle(row);
182
+ if (isHandleValid(rowHandle)) {
183
+ const colHandle = this.colHandles.getHandle(col);
184
+ if (isHandleValid(colHandle)) {
185
+ return this.cells.getCell(rowHandle, colHandle);
186
+ }
187
+ } else {
188
+ // If we early exit because the given rowHandle is unallocated, we still need to
189
+ // bounds-check the 'col' parameter.
190
+ ensureRange(col, this.cols.getLength());
191
+ }
192
+
193
+ return undefined;
194
+ }
195
+
196
+ public get matrixProducer(): IMatrixProducer<MatrixItem<T>> {
197
+ return this;
198
+ }
199
+
200
+ // #endregion IMatrixReader
201
+
202
+ public setCell(row: number, col: number, value: MatrixItem<T>) {
203
+ assert(
204
+ 0 <= row && row < this.rowCount && 0 <= col && col < this.colCount,
205
+ 0x01a /* "Trying to set out-of-bounds cell!" */,
206
+ );
207
+
208
+ this.setCellCore(row, col, value);
209
+
210
+ // Avoid reentrancy by raising change notifications after the op is queued.
211
+ for (const consumer of this.consumers.values()) {
212
+ consumer.cellsChanged(row, col, 1, 1, this);
213
+ }
214
+ }
215
+
216
+ public setCells(
217
+ rowStart: number,
218
+ colStart: number,
219
+ colCount: number,
220
+ values: readonly MatrixItem<T>[],
221
+ ) {
222
+ const rowCount = Math.ceil(values.length / colCount);
223
+
224
+ assert(
225
+ 0 <= rowStart &&
226
+ rowStart < this.rowCount &&
227
+ 0 <= colStart &&
228
+ colStart < this.colCount &&
229
+ 1 <= colCount &&
230
+ colCount <= this.colCount - colStart &&
231
+ rowCount <= this.rowCount - rowStart,
232
+ 0x01b /* "Trying to set multiple out-of-bounds cells!" */,
233
+ );
234
+
235
+ const endCol = colStart + colCount;
236
+ let r = rowStart;
237
+ let c = colStart;
238
+
239
+ for (const value of values) {
240
+ this.setCellCore(r, c, value);
241
+
242
+ if (++c === endCol) {
243
+ c = colStart;
244
+ r++;
245
+ }
246
+ }
247
+
248
+ // Avoid reentrancy by raising change notifications after the op is queued.
249
+ for (const consumer of this.consumers.values()) {
250
+ consumer.cellsChanged(rowStart, colStart, rowCount, colCount, this);
251
+ }
252
+ }
253
+
254
+ private setCellCore(
255
+ row: number,
256
+ col: number,
257
+ value: MatrixItem<T>,
258
+ rowHandle = this.rows.getAllocatedHandle(row),
259
+ colHandle = this.cols.getAllocatedHandle(col),
260
+ ) {
261
+ if (this.undo !== undefined) {
262
+ let oldValue = this.cells.getCell(rowHandle, colHandle);
263
+ if (oldValue === null) {
264
+ oldValue = undefined;
265
+ }
266
+
267
+ this.undo.cellSet(rowHandle, colHandle, oldValue);
268
+ }
269
+
270
+ this.cells.setCell(rowHandle, colHandle, value);
271
+
272
+ if (this.isAttached()) {
273
+ this.sendSetCellOp(row, col, value, rowHandle, colHandle);
274
+ }
275
+ }
276
+
277
+ private sendSetCellOp(
278
+ row: number,
279
+ col: number,
280
+ value: MatrixItem<T>,
281
+ rowHandle: Handle,
282
+ colHandle: Handle,
283
+ localSeq = this.nextLocalSeq(),
284
+ rowsRefSeq = this.rows.getCollabWindow().currentSeq,
285
+ colsRefSeq = this.cols.getCollabWindow().currentSeq,
286
+ ) {
287
+ assert(
288
+ this.isAttached(),
289
+ 0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */,
290
+ );
291
+
292
+ const op: ISetOp<T> = {
293
+ type: MatrixOp.set,
294
+ row,
295
+ col,
296
+ value,
297
+ };
298
+
299
+ const metadata: ISetOpMetadata = {
300
+ rowHandle,
301
+ colHandle,
302
+ localSeq,
303
+ rowsRefSeq,
304
+ colsRefSeq,
305
+ };
306
+
307
+ this.submitLocalMessage(op, metadata);
308
+ this.pending.setCell(rowHandle, colHandle, localSeq);
309
+ }
310
+
311
+ private submitVectorMessage(
312
+ currentVector: PermutationVector,
313
+ oppositeVector: PermutationVector,
314
+ dimension: SnapshotPath.rows | SnapshotPath.cols,
315
+ message: any,
316
+ ) {
317
+ // Ideally, we would have a single 'localSeq' counter that is shared between both PermutationVectors
318
+ // and the SharedMatrix's cell data. Instead, we externally advance each MergeTree's 'localSeq' counter
319
+ // for each submitted op it not aware of to keep them synchronized.
320
+ const localSeq = currentVector.getCollabWindow().localSeq;
321
+ const oppositeWindow = oppositeVector.getCollabWindow();
322
+
323
+ // Note that the comparison is '>=' because, in the case the MergeTree is regenerating ops for reconnection,
324
+ // the MergeTree submits the op with the original 'localSeq'.
325
+ assert(
326
+ localSeq >= oppositeWindow.localSeq,
327
+ 0x01c /* "The 'localSeq' of the vector submitting an op must >= the 'localSeq' of the other vector." */,
328
+ );
329
+
330
+ oppositeWindow.localSeq = localSeq;
331
+
332
+ // If the SharedMatrix is local, it's state will be submitted via a Snapshot when initially connected.
333
+ // Do not queue a message or track the pending op, as there will never be an ACK, etc.
334
+ if (this.isAttached()) {
335
+ // Record whether this `op` targets rows or cols. (See dispatch in `processCore()`)
336
+ message.target = dimension;
337
+
338
+ this.submitLocalMessage(
339
+ message,
340
+ currentVector.peekPendingSegmentGroups(
341
+ message.type === MergeTreeDeltaType.GROUP ? message.ops.length : 1,
342
+ ),
343
+ );
344
+ }
345
+ }
346
+
347
+ private submitColMessage(message: any) {
348
+ this.submitVectorMessage(this.cols, this.rows, SnapshotPath.cols, message);
349
+ }
350
+
351
+ public insertCols(colStart: number, count: number) {
352
+ this.submitColMessage(this.cols.insert(colStart, count));
353
+ }
354
+
355
+ public removeCols(colStart: number, count: number) {
356
+ this.submitColMessage(this.cols.remove(colStart, count));
357
+ }
358
+
359
+ private submitRowMessage(message: any) {
360
+ this.submitVectorMessage(this.rows, this.cols, SnapshotPath.rows, message);
361
+ }
362
+
363
+ public insertRows(rowStart: number, count: number) {
364
+ this.submitRowMessage(this.rows.insert(rowStart, count));
365
+ }
366
+
367
+ public removeRows(rowStart: number, count: number) {
368
+ this.submitRowMessage(this.rows.remove(rowStart, count));
369
+ }
370
+
371
+ /** @internal */ public _undoRemoveRows(segment: ISegment) {
372
+ const original = segment as PermutationSegment;
373
+
374
+ // (Re)insert the removed number of rows at the original position.
375
+ const { op, inserted } = this.rows.insertRelative(original, original.cachedLength);
376
+ this.submitRowMessage(op);
377
+
378
+ // Transfer handles and undo/redo tracking groups from the original segment to the
379
+ // newly inserted segment.
380
+ original.transferToReplacement(inserted);
381
+
382
+ // Invalidate the handleCache in case it was populated during the 'rowsChanged'
383
+ // callback, which occurs before the handle span is populated.
384
+ const rowStart = this.rows.getPosition(inserted);
385
+ this.rows.handleCache.itemsChanged(
386
+ rowStart,
387
+ /* removedCount: */ 0,
388
+ /* insertedCount: */ inserted.cachedLength,
389
+ );
390
+
391
+ // Generate setCell ops for each populated cell in the reinserted rows.
392
+ let rowHandle = inserted.start;
393
+ const rowCount = inserted.cachedLength;
394
+ for (let row = rowStart; row < rowStart + rowCount; row++, rowHandle++) {
395
+ for (let col = 0; col < this.colCount; col++) {
396
+ const colHandle = this.colHandles.getHandle(col);
397
+ const value = this.cells.getCell(rowHandle, colHandle);
398
+ if (this.isAttached() && value !== undefined && value !== null) {
399
+ this.sendSetCellOp(row, col, value, rowHandle, colHandle);
400
+ }
401
+ }
402
+ }
403
+
404
+ // Avoid reentrancy by raising change notifications after the op is queued.
405
+ for (const consumer of this.consumers.values()) {
406
+ consumer.cellsChanged(rowStart, /* colStart: */ 0, rowCount, this.colCount, this);
407
+ }
408
+ }
409
+
410
+ /** @internal */ public _undoRemoveCols(segment: ISegment) {
411
+ const original = segment as PermutationSegment;
412
+
413
+ // (Re)insert the removed number of columns at the original position.
414
+ const { op, inserted } = this.cols.insertRelative(original, original.cachedLength);
415
+ this.submitColMessage(op);
416
+
417
+ // Transfer handles and undo/redo tracking groups from the original segment to the
418
+ // newly inserted segment.
419
+ original.transferToReplacement(inserted);
420
+
421
+ // Invalidate the handleCache in case it was populated during the 'colsChanged'
422
+ // callback, which occurs before the handle span is populated.
423
+ const colStart = this.cols.getPosition(inserted);
424
+ this.cols.handleCache.itemsChanged(
425
+ colStart,
426
+ /* removedCount: */ 0,
427
+ /* insertedCount: */ inserted.cachedLength,
428
+ );
429
+
430
+ // Generate setCell ops for each populated cell in the reinserted cols.
431
+ let colHandle = inserted.start;
432
+ const colCount = inserted.cachedLength;
433
+ for (let col = colStart; col < colStart + colCount; col++, colHandle++) {
434
+ for (let row = 0; row < this.rowCount; row++) {
435
+ const rowHandle = this.rowHandles.getHandle(row);
436
+ const value = this.cells.getCell(rowHandle, colHandle);
437
+ if (this.isAttached() && value !== undefined && value !== null) {
438
+ this.sendSetCellOp(row, col, value, rowHandle, colHandle);
439
+ }
440
+ }
441
+ }
442
+
443
+ // Avoid reentrancy by raising change notifications after the op is queued.
444
+ for (const consumer of this.consumers.values()) {
445
+ consumer.cellsChanged(/* rowStart: */ 0, colStart, this.rowCount, colCount, this);
446
+ }
447
+ }
448
+
449
+ protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
450
+ const builder = new SummaryTreeBuilder();
451
+ builder.addWithStats(
452
+ SnapshotPath.rows,
453
+ this.rows.summarize(this.runtime, this.handle, serializer),
454
+ );
455
+ builder.addWithStats(
456
+ SnapshotPath.cols,
457
+ this.cols.summarize(this.runtime, this.handle, serializer),
458
+ );
459
+ builder.addBlob(
460
+ SnapshotPath.cells,
461
+ serializer.stringify([this.cells.snapshot(), this.pending.snapshot()], this.handle),
462
+ );
463
+ return builder.getSummaryTree();
464
+ }
465
+
466
+ /**
467
+ * Runs serializer on the GC data for this SharedMatrix.
468
+ * All the IFluidHandle's stored in the cells represent routes to other objects.
469
+ */
470
+ protected processGCDataCore(serializer: SummarySerializer) {
471
+ for (let row = 0; row < this.rowCount; row++) {
472
+ for (let col = 0; col < this.colCount; col++) {
473
+ serializer.stringify(this.getCell(row, col), this.handle);
474
+ }
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Advances the 'localSeq' counter for the cell data operation currently being queued.
480
+ *
481
+ * Do not use with 'submitColMessage()/submitRowMessage()' as these helpers + the MergeTree will
482
+ * automatically advance 'localSeq'.
483
+ */
484
+ private nextLocalSeq() {
485
+ // Ideally, we would have a single 'localSeq' counter that is shared between both PermutationVectors
486
+ // and the SharedMatrix's cell data. Instead, we externally bump each MergeTree's 'localSeq' counter
487
+ // for SharedMatrix ops it's not aware of to keep them synchronized. (For cell data operations, we
488
+ // need to bump both counters.)
489
+
490
+ this.cols.getCollabWindow().localSeq++;
491
+ return ++this.rows.getCollabWindow().localSeq;
492
+ }
493
+
494
+ protected submitLocalMessage(message: any, localOpMetadata?: any) {
495
+ // TODO: Recommend moving this assertion into SharedObject
496
+ // (See https://github.com/microsoft/FluidFramework/issues/2559)
497
+ assert(
498
+ this.isAttached() === true,
499
+ 0x01d /* "Trying to submit message to runtime while detached!" */,
500
+ );
501
+
502
+ super.submitLocalMessage(
503
+ makeHandlesSerializable(message, this.serializer, this.handle),
504
+ localOpMetadata,
505
+ );
506
+
507
+ // Ensure that row/col 'localSeq' are synchronized (see 'nextLocalSeq()').
508
+ assert(
509
+ this.rows.getCollabWindow().localSeq === this.cols.getCollabWindow().localSeq,
510
+ 0x01e /* "Row and col collab window 'localSeq' desynchronized!" */,
511
+ );
512
+ }
513
+
514
+ protected didAttach() {
515
+ // We've attached we need to start generating and sending ops.
516
+ // so start collaboration and provide a default client id incase we are not connected
517
+ if (this.isAttached()) {
518
+ this.rows.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
519
+ this.cols.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
520
+ }
521
+ }
522
+
523
+ protected onConnect() {
524
+ assert(
525
+ this.rows.getCollabWindow().collaborating === this.cols.getCollabWindow().collaborating,
526
+ 0x01f /* "Row and col collab window 'collaborating' status desynchronized!" */,
527
+ );
528
+
529
+ // Update merge tree collaboration information with new client ID and then resend pending ops
530
+ this.rows.startOrUpdateCollaboration(this.runtime.clientId as string);
531
+ this.cols.startOrUpdateCollaboration(this.runtime.clientId as string);
532
+ }
533
+
534
+ private rebasePosition(
535
+ client: Client,
536
+ pos: number,
537
+ referenceSequenceNumber: number,
538
+ localSeq: number,
539
+ ): number | undefined {
540
+ const { clientId } = client.getCollabWindow();
541
+ const { segment, offset } = client.getContainingSegment(
542
+ pos,
543
+ { referenceSequenceNumber, clientId: client.getLongClientId(clientId) },
544
+ localSeq,
545
+ );
546
+ if (segment === undefined || offset === undefined) {
547
+ return;
548
+ }
549
+
550
+ return client.findReconnectionPosition(segment, localSeq) + offset;
551
+ }
552
+
553
+ protected reSubmitCore(content: any, localOpMetadata: unknown) {
554
+ switch (content.target) {
555
+ case SnapshotPath.cols:
556
+ this.submitColMessage(
557
+ this.cols.regeneratePendingOp(
558
+ content as IMergeTreeOp,
559
+ localOpMetadata as SegmentGroup | SegmentGroup[],
560
+ ),
561
+ );
562
+ break;
563
+ case SnapshotPath.rows:
564
+ this.submitRowMessage(
565
+ this.rows.regeneratePendingOp(
566
+ content as IMergeTreeOp,
567
+ localOpMetadata as SegmentGroup | SegmentGroup[],
568
+ ),
569
+ );
570
+ break;
571
+ default: {
572
+ assert(
573
+ content.type === MatrixOp.set,
574
+ 0x020 /* "Unknown SharedMatrix 'op' type." */,
575
+ );
576
+
577
+ const setOp = content as ISetOp<T>;
578
+ const { rowHandle, colHandle, localSeq, rowsRefSeq, colsRefSeq } =
579
+ localOpMetadata as ISetOpMetadata;
580
+
581
+ // If there are more pending local writes to the same row/col handle, it is important
582
+ // to skip resubmitting this op since it is possible the row/col handle has been recycled
583
+ // and now refers to a different position than when this op was originally submitted.
584
+ if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
585
+ const row = this.rebasePosition(this.rows, setOp.row, rowsRefSeq, localSeq);
586
+ const col = this.rebasePosition(this.cols, setOp.col, colsRefSeq, localSeq);
587
+
588
+ if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
589
+ this.sendSetCellOp(
590
+ row,
591
+ col,
592
+ setOp.value,
593
+ rowHandle,
594
+ colHandle,
595
+ localSeq,
596
+ rowsRefSeq,
597
+ colsRefSeq,
598
+ );
599
+ }
600
+ }
601
+ break;
602
+ }
603
+ }
604
+ }
605
+
606
+ protected onDisconnect() {}
607
+
608
+ /**
609
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
610
+ */
611
+ protected async loadCore(storage: IChannelStorageService) {
612
+ try {
613
+ await this.rows.load(
614
+ this.runtime,
615
+ new ObjectStoragePartition(storage, SnapshotPath.rows),
616
+ this.serializer,
617
+ );
618
+ await this.cols.load(
619
+ this.runtime,
620
+ new ObjectStoragePartition(storage, SnapshotPath.cols),
621
+ this.serializer,
622
+ );
623
+ const [cellData, pendingCliSeqData] = await deserializeBlob(
624
+ storage,
625
+ SnapshotPath.cells,
626
+ this.serializer,
627
+ );
628
+
629
+ this.cells = SparseArray2D.load(cellData);
630
+ this.pending = SparseArray2D.load(pendingCliSeqData);
631
+ } catch (error) {
632
+ this.logger.sendErrorEvent({ eventName: "MatrixLoadFailed" }, error);
633
+ }
634
+ }
635
+
636
+ protected processCore(
637
+ rawMessage: ISequencedDocumentMessage,
638
+ local: boolean,
639
+ localOpMetadata: unknown,
640
+ ) {
641
+ const msg = parseHandles(rawMessage, this.serializer);
642
+
643
+ const contents = msg.contents;
644
+
645
+ switch (contents.target) {
646
+ case SnapshotPath.cols:
647
+ this.cols.applyMsg(msg, local);
648
+ break;
649
+ case SnapshotPath.rows:
650
+ this.rows.applyMsg(msg, local);
651
+ break;
652
+ default: {
653
+ assert(
654
+ contents.type === MatrixOp.set,
655
+ 0x021 /* "SharedMatrix message contents have unexpected type!" */,
656
+ );
657
+
658
+ const { row, col } = contents;
659
+
660
+ if (local) {
661
+ // We are receiving the ACK for a local pending set operation.
662
+ const { rowHandle, colHandle, localSeq } = localOpMetadata as ISetOpMetadata;
663
+
664
+ // If this is the most recent write to the cell by the local client, remove our
665
+ // entry from 'pendingCliSeqs' to resume allowing remote writes.
666
+ if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
667
+ this.pending.setCell(rowHandle, colHandle, undefined);
668
+ }
669
+ } else {
670
+ const adjustedRow = this.rows.adjustPosition(row, rawMessage);
671
+
672
+ if (adjustedRow !== undefined) {
673
+ const adjustedCol = this.cols.adjustPosition(col, rawMessage);
674
+
675
+ if (adjustedCol !== undefined) {
676
+ const rowHandle = this.rows.getAllocatedHandle(adjustedRow);
677
+ const colHandle = this.cols.getAllocatedHandle(adjustedCol);
678
+
679
+ assert(
680
+ isHandleValid(rowHandle) && isHandleValid(colHandle),
681
+ 0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
682
+ );
683
+
684
+ // If there is a pending (unACKed) local write to the same cell, skip the current op
685
+ // since it "happened before" the pending write.
686
+ if (this.pending.getCell(rowHandle, colHandle) === undefined) {
687
+ const { value } = contents;
688
+ this.cells.setCell(rowHandle, colHandle, value);
689
+
690
+ for (const consumer of this.consumers.values()) {
691
+ consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ // Invoked by PermutationVector to notify IMatrixConsumers of row insertion/deletions.
702
+ private readonly onRowDelta = (
703
+ position: number,
704
+ removedCount: number,
705
+ insertedCount: number,
706
+ ) => {
707
+ for (const consumer of this.consumers) {
708
+ consumer.rowsChanged(position, removedCount, insertedCount, this);
709
+ }
710
+ };
711
+
712
+ // Invoked by PermutationVector to notify IMatrixConsumers of col insertion/deletions.
713
+ private readonly onColDelta = (
714
+ position: number,
715
+ removedCount: number,
716
+ insertedCount: number,
717
+ ) => {
718
+ for (const consumer of this.consumers) {
719
+ consumer.colsChanged(position, removedCount, insertedCount, this);
720
+ }
721
+ };
722
+
723
+ private readonly onRowHandlesRecycled = (rowHandles: Handle[]) => {
724
+ for (const rowHandle of rowHandles) {
725
+ this.cells.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
726
+ this.pending.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
727
+ }
728
+ };
729
+
730
+ private readonly onColHandlesRecycled = (colHandles: Handle[]) => {
731
+ for (const colHandle of colHandles) {
732
+ this.cells.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
733
+ this.pending.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
734
+ }
735
+ };
736
+
737
+ /**
738
+ * Returns true if the latest pending write to the cell indicated by the given row/col handles
739
+ * matches the given 'localSeq'.
740
+ *
741
+ * A return value of `true` indicates that there are no later local operations queued that will
742
+ * clobber the write op at the given 'localSeq'. This includes later ops that overwrite the cell
743
+ * with a different value as well as row/col removals that might recycled the given row/col handles.
744
+ */
745
+ private isLatestPendingWrite(rowHandle: Handle, colHandle: Handle, localSeq: number) {
746
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
747
+ const pendingLocalSeq = this.pending.getCell(rowHandle, colHandle)!;
748
+
749
+ // Note while we're awaiting the ACK for a local set, it's possible for the row/col to be
750
+ // locally removed and the row/col handles recycled. If this happens, the pendingLocalSeq will
751
+ // be 'undefined' or > 'localSeq'.
752
+ assert(
753
+ !(pendingLocalSeq < localSeq),
754
+ 0x023 /* "The 'localSeq' of pending write (if any) must be <= the localSeq of the currently processed op." */,
755
+ );
756
+
757
+ // If this is the most recent write to the cell by the local client, the stored localSeq
758
+ // will be an exact match for the given 'localSeq'.
759
+ return pendingLocalSeq === localSeq;
760
+ }
761
+
762
+ public toString() {
763
+ let s = `client:${
764
+ this.runtime.clientId
765
+ }\nrows: ${this.rows.toString()}\ncols: ${this.cols.toString()}\n\n`;
766
+
767
+ for (let r = 0; r < this.rowCount; r++) {
768
+ s += ` [`;
769
+ for (let c = 0; c < this.colCount; c++) {
770
+ if (c > 0) {
771
+ s += ", ";
772
+ }
773
+
774
+ s += `${this.serializer.stringify(this.getCell(r, c), this.handle)}`;
775
+ }
776
+ s += "]\n";
777
+ }
778
+
779
+ return `${s}\n`;
780
+ }
781
+
782
+ /**
783
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
784
+ */
785
+ protected applyStashedOp(content: any): unknown {
786
+ if (content.target === SnapshotPath.cols || content.target === SnapshotPath.rows) {
787
+ const op = content as IMergeTreeOp;
788
+ const currentVector = content.target === SnapshotPath.cols ? this.cols : this.rows;
789
+ const oppositeVector = content.target === SnapshotPath.cols ? this.rows : this.cols;
790
+ const metadata = currentVector.applyStashedOp(op);
791
+ const localSeq = currentVector.getCollabWindow().localSeq;
792
+ const oppositeWindow = oppositeVector.getCollabWindow();
793
+
794
+ assert(
795
+ localSeq > oppositeWindow.localSeq,
796
+ 0x2d9,
797
+ /* "The 'localSeq' of the vector applying stashed op must > the 'localSeq' of the other vector." */
798
+ );
799
+
800
+ oppositeWindow.localSeq = localSeq;
801
+
802
+ return metadata;
803
+ } else {
804
+ assert(content.type === MatrixOp.set, 0x2da /* "Unknown SharedMatrix 'op' type." */);
805
+
806
+ const setOp = content as ISetOp<T>;
807
+ const rowHandle = this.rows.getAllocatedHandle(setOp.row);
808
+ const colHandle = this.cols.getAllocatedHandle(setOp.col);
809
+ const rowsRefSeq = this.rows.getCollabWindow().currentSeq;
810
+ const colsRefSeq = this.cols.getCollabWindow().currentSeq;
811
+ if (this.undo !== undefined) {
812
+ let oldValue = this.cells.getCell(rowHandle, colHandle);
813
+ if (oldValue === null) {
814
+ oldValue = undefined;
815
+ }
816
+
817
+ this.undo.cellSet(rowHandle, colHandle, oldValue);
818
+ }
819
+
820
+ this.cells.setCell(rowHandle, colHandle, setOp.value);
821
+ const localSeq = this.nextLocalSeq();
822
+ const metadata: ISetOpMetadata = {
823
+ rowHandle,
824
+ colHandle,
825
+ localSeq,
826
+ rowsRefSeq,
827
+ colsRefSeq,
828
+ };
829
+
830
+ this.pending.setCell(rowHandle, colHandle, localSeq);
831
+ return metadata;
832
+ }
833
+ }
749
834
  }