@fluidframework/matrix 2.0.0-internal.3.0.2 → 2.0.0-internal.3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.eslintrc.js +20 -21
  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 +1 -1
  22. package/dist/matrix.d.ts.map +1 -1
  23. package/dist/matrix.js +28 -19
  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 +15 -8
  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 +1 -1
  61. package/lib/matrix.d.ts.map +1 -1
  62. package/lib/matrix.js +29 -20
  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 +15 -8
  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 +52 -52
  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 -710
  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
@@ -5,18 +5,21 @@
5
5
 
6
6
  import { assert } from "@fluidframework/common-utils";
7
7
  import { ChildLogger } from "@fluidframework/telemetry-utils";
8
- import { IFluidDataStoreRuntime, IChannelStorageService } from "@fluidframework/datastore-definitions";
8
+ import {
9
+ IFluidDataStoreRuntime,
10
+ IChannelStorageService,
11
+ } from "@fluidframework/datastore-definitions";
9
12
  import { ITelemetryBaseLogger } from "@fluidframework/common-definitions";
10
13
  import {
11
- BaseSegment,
12
- ISegment,
13
- Client,
14
- IMergeTreeDeltaOpArgs,
15
- IMergeTreeDeltaCallbackArgs,
16
- MergeTreeDeltaType,
17
- IMergeTreeMaintenanceCallbackArgs,
18
- MergeTreeMaintenanceType,
19
- ReferenceType,
14
+ BaseSegment,
15
+ ISegment,
16
+ Client,
17
+ IMergeTreeDeltaOpArgs,
18
+ IMergeTreeDeltaCallbackArgs,
19
+ MergeTreeDeltaType,
20
+ IMergeTreeMaintenanceCallbackArgs,
21
+ MergeTreeMaintenanceType,
22
+ ReferenceType,
20
23
  } from "@fluidframework/merge-tree";
21
24
  import { IFluidHandle } from "@fluidframework/core-interfaces";
22
25
  import { IFluidSerializer } from "@fluidframework/shared-object-base";
@@ -29,370 +32,424 @@ import { HandleCache } from "./handlecache";
29
32
  import { VectorUndoProvider } from "./undoprovider";
30
33
 
31
34
  const enum SnapshotPath {
32
- segments = "segments",
33
- handleTable = "handleTable",
35
+ segments = "segments",
36
+ handleTable = "handleTable",
34
37
  }
35
38
 
36
39
  type PermutationSegmentSpec = [number, number];
37
40
 
38
41
  export class PermutationSegment extends BaseSegment {
39
- public static readonly typeString: string = "PermutationSegment";
40
- private _start = Handle.unallocated;
41
-
42
- public static fromJSONObject(spec: any) {
43
- const [length, start] = spec as PermutationSegmentSpec;
44
- return new PermutationSegment(length, start);
45
- }
46
-
47
- public readonly type = PermutationSegment.typeString;
48
-
49
- constructor(length: number, start = Handle.unallocated) {
50
- super();
51
- this._start = start;
52
- this.cachedLength = length;
53
- }
54
-
55
- public get start() { return this._start; }
56
- public set start(value: Handle) {
57
- assert(this._start === Handle.unallocated, 0x024 /* "Start of PermutationSegment already allocated!" */);
58
- assert(isHandleValid(value), 0x025 /* "Trying to set start of PermutationSegment to invalid handle!" */);
59
-
60
- this._start = value;
61
- }
62
-
63
- /**
64
- * Invoked by '_undoRow/ColRemove' to prepare the newly inserted destination
65
- * segment to serve as the replacement for this removed segment. This moves handle
66
- * allocations from this segment to the replacement as well as maintains tracking
67
- * groups for the undo/redo stack.
68
- */
69
- public transferToReplacement(destination: PermutationSegment) {
70
- // When this segment was removed, it may have been split from a larger original
71
- // segment. In this case, it will have been added to an undo/redo tracking group
72
- // that associates all of the fragments from the original insertion.
73
- //
74
- // Move this association from the this removed segment to its replacement so that
75
- // it is included if the undo stack continues to unwind to the original insertion.
76
- //
77
- // Out of paranoia we link and unlink in separate loops to avoid mutating the underlying
78
- // set during enumeration. In practice, this is unlikely to matter since there should be
79
- // exactly 0 or 1 items in the enumeration.
80
- for (const group of this.trackingCollection.trackingGroups) {
81
- group.link(destination);
82
- }
83
- for (const group of this.trackingCollection.trackingGroups) {
84
- group.unlink(this);
85
- }
86
-
87
- // Move handle allocations from this segment to its replacement.
88
- destination._start = this._start;
89
- this.reset();
90
- }
91
-
92
- public reset() {
93
- this._start = Handle.unallocated;
94
- }
95
-
96
- public toJSONObject() {
97
- return [this.cachedLength, this.start];
98
- }
99
-
100
- public clone(start = 0, end = this.cachedLength) {
101
- const b = new PermutationSegment(
102
- /* length: */ end - start,
103
- /* start: */ this.start + start);
104
- this.cloneInto(b);
105
- return b;
106
- }
107
-
108
- public canAppend(segment: ISegment) {
109
- const asPerm = segment as PermutationSegment;
110
-
111
- return this.start === Handle.unallocated
112
- ? asPerm.start === Handle.unallocated
113
- : asPerm.start === this.start + this.cachedLength;
114
- }
115
-
116
- protected createSplitSegmentAt(pos: number) {
117
- assert(0 < pos && pos < this.cachedLength, 0x026 /* "Trying to split segment at out-of-bounds position!" */);
118
-
119
- const leafSegment = new PermutationSegment(
120
- /* length: */ this.cachedLength - pos,
121
- /* start: */ this.start === Handle.unallocated
122
- ? Handle.unallocated
123
- : this.start + pos);
124
-
125
- this.cachedLength = pos;
126
-
127
- return leafSegment;
128
- }
129
-
130
- public toString() {
131
- return this.start === Handle.unallocated
132
- ? `<${this.cachedLength} empty>`
133
- : `<${this.cachedLength}: ${this.start}..${this.start + this.cachedLength - 1}>`;
134
- }
42
+ public static readonly typeString: string = "PermutationSegment";
43
+ private _start = Handle.unallocated;
44
+
45
+ public static fromJSONObject(spec: any) {
46
+ const [length, start] = spec as PermutationSegmentSpec;
47
+ return new PermutationSegment(length, start);
48
+ }
49
+
50
+ public readonly type = PermutationSegment.typeString;
51
+
52
+ constructor(length: number, start = Handle.unallocated) {
53
+ super();
54
+ this._start = start;
55
+ this.cachedLength = length;
56
+ }
57
+
58
+ public get start() {
59
+ return this._start;
60
+ }
61
+ public set start(value: Handle) {
62
+ assert(
63
+ this._start === Handle.unallocated,
64
+ 0x024 /* "Start of PermutationSegment already allocated!" */,
65
+ );
66
+ assert(
67
+ isHandleValid(value),
68
+ 0x025 /* "Trying to set start of PermutationSegment to invalid handle!" */,
69
+ );
70
+
71
+ this._start = value;
72
+ }
73
+
74
+ /**
75
+ * Invoked by '_undoRow/ColRemove' to prepare the newly inserted destination
76
+ * segment to serve as the replacement for this removed segment. This moves handle
77
+ * allocations from this segment to the replacement as well as maintains tracking
78
+ * groups for the undo/redo stack.
79
+ */
80
+ public transferToReplacement(destination: PermutationSegment) {
81
+ // When this segment was removed, it may have been split from a larger original
82
+ // segment. In this case, it will have been added to an undo/redo tracking group
83
+ // that associates all of the fragments from the original insertion.
84
+ //
85
+ // Move this association from the this removed segment to its replacement so that
86
+ // it is included if the undo stack continues to unwind to the original insertion.
87
+ //
88
+ // Out of paranoia we link and unlink in separate loops to avoid mutating the underlying
89
+ // set during enumeration. In practice, this is unlikely to matter since there should be
90
+ // exactly 0 or 1 items in the enumeration.
91
+ for (const group of this.trackingCollection.trackingGroups) {
92
+ group.link(destination);
93
+ }
94
+ for (const group of this.trackingCollection.trackingGroups) {
95
+ group.unlink(this);
96
+ }
97
+
98
+ // Move handle allocations from this segment to its replacement.
99
+ destination._start = this._start;
100
+ this.reset();
101
+ }
102
+
103
+ public reset() {
104
+ this._start = Handle.unallocated;
105
+ }
106
+
107
+ public toJSONObject() {
108
+ return [this.cachedLength, this.start];
109
+ }
110
+
111
+ public clone(start = 0, end = this.cachedLength) {
112
+ const b = new PermutationSegment(
113
+ /* length: */ end - start,
114
+ /* start: */ this.start + start,
115
+ );
116
+ this.cloneInto(b);
117
+ return b;
118
+ }
119
+
120
+ public canAppend(segment: ISegment) {
121
+ const asPerm = segment as PermutationSegment;
122
+
123
+ return this.start === Handle.unallocated
124
+ ? asPerm.start === Handle.unallocated
125
+ : asPerm.start === this.start + this.cachedLength;
126
+ }
127
+
128
+ protected createSplitSegmentAt(pos: number) {
129
+ assert(
130
+ 0 < pos && pos < this.cachedLength,
131
+ 0x026 /* "Trying to split segment at out-of-bounds position!" */,
132
+ );
133
+
134
+ const leafSegment = new PermutationSegment(
135
+ /* length: */ this.cachedLength - pos,
136
+ /* start: */ this.start === Handle.unallocated ? Handle.unallocated : this.start + pos,
137
+ );
138
+
139
+ this.cachedLength = pos;
140
+
141
+ return leafSegment;
142
+ }
143
+
144
+ public toString() {
145
+ return this.start === Handle.unallocated
146
+ ? `<${this.cachedLength} empty>`
147
+ : `<${this.cachedLength}: ${this.start}..${this.start + this.cachedLength - 1}>`;
148
+ }
135
149
  }
136
150
 
137
151
  export class PermutationVector extends Client {
138
- private handleTable = new HandleTable<never>(); // Tracks available storage handles for rows.
139
- public readonly handleCache = new HandleCache(this);
140
- public undo: VectorUndoProvider | undefined;
141
-
142
- constructor(
143
- path: string,
144
- logger: ITelemetryBaseLogger,
145
- runtime: IFluidDataStoreRuntime,
146
- private readonly deltaCallback: (position: number, numRemoved: number, numInserted: number) => void,
147
- private readonly handlesRecycledCallback: (handles: Handle[]) => void,
148
- ) {
149
- super(
150
- PermutationSegment.fromJSONObject,
151
- ChildLogger.create(logger, `Matrix.${path}.MergeTreeClient`), {
152
- ...runtime.options,
153
- newMergeTreeSnapshotFormat: true, // Temporarily force new snapshot format until it is the default.
154
- }); // (See https://github.com/microsoft/FluidFramework/issues/84)
155
-
156
- this.on("delta", this.onDelta);
157
- this.on("maintenance", this.onMaintenance);
158
- }
159
-
160
- public insert(start: number, length: number) {
161
- return this.insertSegmentLocal(
162
- start,
163
- new PermutationSegment(length));
164
- }
165
-
166
- public insertRelative(segment: ISegment, length: number) {
167
- const inserted = new PermutationSegment(length);
168
-
169
- return {
170
- op: this.insertAtReferencePositionLocal(
171
- this.createLocalReferencePosition(segment, 0, ReferenceType.Transient, undefined),
172
- inserted),
173
- inserted,
174
- };
175
- }
176
-
177
- public remove(start: number, length: number) {
178
- return this.removeRangeLocal(start, start + length);
179
- }
180
-
181
- public getMaybeHandle(pos: number): Handle {
182
- assert(0 <= pos && pos < this.getLength(), 0x027 /* "Trying to get handle of out-of-bounds position!" */);
183
-
184
- return this.handleCache.getHandle(pos);
185
- }
186
-
187
- public getAllocatedHandle(pos: number): Handle {
188
- let handle = this.getMaybeHandle(pos);
189
- if (isHandleValid(handle)) {
190
- return handle;
191
- }
192
-
193
- this.walkSegments(
194
- (segment) => {
195
- const asPerm = segment as PermutationSegment;
196
- asPerm.start = handle = this.handleTable.allocate();
197
- return true;
198
- },
199
- pos,
200
- pos + 1,
201
- /* accum: */ undefined,
202
- /* splitRange: */ true);
203
-
204
- this.handleCache.addHandle(pos, handle);
205
-
206
- return handle;
207
- }
208
-
209
- public adjustPosition(pos: number, op: ISequencedDocumentMessage) {
210
- const { segment, offset } = this.getContainingSegment(pos, { referenceSequenceNumber: op.referenceSequenceNumber, clientId: op.clientId });
211
-
212
- // Note that until the MergeTree GCs, the segment is still reachable via `getContainingSegment()` with
213
- // a `refSeq` in the past. Prevent remote ops from accidentally allocating or using recycled handles
214
- // by checking for the presence of 'removedSeq'.
215
- if (segment === undefined || segment.removedSeq !== undefined) {
216
- return undefined;
217
- }
218
-
219
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
220
- return this.getPosition(segment) + offset!;
221
- }
222
-
223
- public handleToPosition(handle: Handle, localSeq = this.getCollabWindow().localSeq) {
224
- assert(localSeq <= this.getCollabWindow().localSeq,
225
- 0x028 /* "'localSeq' for op being resubmitted must be <= the 'localSeq' of the last submitted op." */);
226
-
227
- // TODO: In theory, the MergeTree should be able to map the (position, refSeq, localSeq) from
228
- // the original operation to the current position for undo/redo scenarios. This is probably the
229
- // ideal solution, as we would no longer need to store row/col handles in the op metadata.
230
- //
231
- // Failing that, we could avoid the O(n) search below by building a temporary map in the
232
- // opposite direction from the handle to either it's current position or segment + offset
233
- // and reuse it for the duration of undo/redo. (Ideally, we would know when the undo/redo
234
- // ended so we could discard this map.)
235
- //
236
- // If we find that we frequently need a reverse handle -> position lookup, we could maintain
237
- // one using the Tiny-Calc adjust tree.
238
- let containingSegment!: PermutationSegment;
239
- let containingOffset: number;
240
-
241
- this.walkAllSegments(
242
- (segment) => {
243
- const { start, cachedLength } = segment as PermutationSegment;
244
-
245
- // If the segment is unallocated, skip it.
246
- if (!isHandleValid(start)) {
247
- return true;
248
- }
249
-
250
- const end = start + cachedLength;
251
-
252
- if (start <= handle && handle < end) {
253
- containingSegment = segment as PermutationSegment;
254
- containingOffset = handle - start;
255
- return false;
256
- }
257
-
258
- return true;
259
- });
260
-
261
- // We are guaranteed to find the handle in the PermutationVector, even if the corresponding
262
- // row/col has been removed, because handles are not recycled until the containing segment
263
- // is unlinked from the MergeTree.
264
- //
265
- // Therefore, either a row/col removal has been ACKed, in which case there will be no pending
266
- // ops that reference the stale handle, or the removal is unACKed, in which case the handle
267
- // has not yet been recycled.
268
-
269
- assert(isHandleValid(containingSegment.start), 0x029 /* "Invalid handle at start of containing segment!" */);
270
-
271
- // Once we know the current position of the handle, we can use the MergeTree to get the segment
272
- // containing this position and use 'findReconnectionPosition' to adjust for the local ops that
273
- // have not yet been submitted.
274
-
275
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276
- return this.findReconnectionPosition(containingSegment, localSeq) + containingOffset!;
277
- }
278
-
279
- // Constructs an ISummaryTreeWithStats for the cell data.
280
- public summarize(runtime: IFluidDataStoreRuntime, handle: IFluidHandle, serializer: IFluidSerializer):
281
- ISummaryTreeWithStats {
282
- const builder = new SummaryTreeBuilder();
283
- builder.addWithStats(SnapshotPath.segments, super.summarize(runtime, handle, serializer, /* catchUpMsgs: */[]));
284
- builder.addBlob(SnapshotPath.handleTable, serializer.stringify(this.handleTable.getSummaryContent(), handle));
285
- return builder.getSummaryTree();
286
- }
287
-
288
- public async load(
289
- runtime: IFluidDataStoreRuntime,
290
- storage: IChannelStorageService,
291
- serializer: IFluidSerializer,
292
- ) {
293
- const handleTableData = await deserializeBlob(storage, SnapshotPath.handleTable, serializer);
294
-
295
- this.handleTable = HandleTable.load<never>(handleTableData);
296
-
297
- return super.load(runtime, new ObjectStoragePartition(storage, SnapshotPath.segments), serializer);
298
- }
299
-
300
- private readonly onDelta = (
301
- opArgs: IMergeTreeDeltaOpArgs,
302
- { operation, deltaSegments }: IMergeTreeDeltaCallbackArgs,
303
- ) => {
304
- // Apply deltas in descending order to prevent positions from shifting.
305
- const ranges = deltaSegments
306
- .map(({ segment }) => ({
307
- segment: segment as PermutationSegment,
308
- position: this.getPosition(segment),
309
- }))
310
- .sort((left, right) => left.position - right.position);
311
-
312
- const isLocal = opArgs.sequencedMessage === undefined;
313
-
314
- // Notify the undo provider, if any is attached.
315
- if (this.undo !== undefined && isLocal) {
316
- this.undo.record(operation, ranges);
317
- }
318
-
319
- switch (operation) {
320
- case MergeTreeDeltaType.INSERT:
321
- // Pass 1: Perform any internal maintenance first to avoid reentrancy.
322
- for (const { segment, position } of ranges) {
323
- // HACK: We need to include the allocated handle in the segment's JSON representation
324
- // for snapshots, but need to ignore the remote client's handle allocations when
325
- // processing remote ops.
326
- segment.reset();
327
-
328
- this.handleCache.itemsChanged(
329
- position,
330
- /* deleteCount: */ 0,
331
- /* insertCount: */ segment.cachedLength);
332
- }
333
-
334
- // Pass 2: Notify the 'deltaCallback', which may involve callbacks into user code.
335
- for (const { segment, position } of ranges) {
336
- this.deltaCallback(position, /* numRemoved: */ 0, /* numInserted: */ segment.cachedLength);
337
- }
338
- break;
339
-
340
- case MergeTreeDeltaType.REMOVE: {
341
- // Pass 1: Perform any internal maintenance first to avoid reentrancy.
342
- for (const { segment, position } of ranges) {
343
- this.handleCache.itemsChanged(
344
- position, /* deleteCount: */
345
- segment.cachedLength,
346
- /* insertCount: */ 0);
347
- }
348
-
349
- // Pass 2: Notify the 'deltaCallback', which may involve callbacks into user code.
350
- for (const { segment, position } of ranges) {
351
- this.deltaCallback(position, /* numRemoved: */ segment.cachedLength, /* numInsert: */ 0);
352
- }
353
- break;
354
- }
355
-
356
- default:
357
- throw new Error("Unhandled MergeTreeDeltaType");
358
- }
359
- };
360
-
361
- private readonly onMaintenance = (args: IMergeTreeMaintenanceCallbackArgs) => {
362
- if (args.operation === MergeTreeMaintenanceType.UNLINK) {
363
- let freed: number[] = [];
364
-
365
- for (const { segment } of args.deltaSegments) {
366
- const asPerm = segment as PermutationSegment;
367
- if (isHandleValid(asPerm.start)) {
368
- // Note: Using the spread operator with `.splice()` can exhaust the stack.
369
- freed = freed.concat(
370
- new Array(asPerm.cachedLength)
371
- .fill(0)
372
- .map((value, index) => index + asPerm.start),
373
- );
374
- }
375
- }
376
-
377
- // Notify matrix that handles are about to be freed. The matrix is responsible for clearing
378
- // the rows/cols prior to free to ensure recycled row/cols are initially empty.
379
- this.handlesRecycledCallback(freed);
380
-
381
- // Now that the physical storage has been cleared, add the recycled handles back to the free pool.
382
- for (const handle of freed) {
383
- this.handleTable.free(handle);
384
- }
385
- }
386
- };
387
-
388
- public toString() {
389
- const s: string[] = [];
390
-
391
- this.walkSegments((segment) => {
392
- s.push(`${segment}`);
393
- return true;
394
- });
395
-
396
- return s.join("");
397
- }
152
+ private handleTable = new HandleTable<never>(); // Tracks available storage handles for rows.
153
+ public readonly handleCache = new HandleCache(this);
154
+ public undo: VectorUndoProvider | undefined;
155
+
156
+ constructor(
157
+ path: string,
158
+ logger: ITelemetryBaseLogger,
159
+ runtime: IFluidDataStoreRuntime,
160
+ private readonly deltaCallback: (
161
+ position: number,
162
+ numRemoved: number,
163
+ numInserted: number,
164
+ ) => void,
165
+ private readonly handlesRecycledCallback: (handles: Handle[]) => void,
166
+ ) {
167
+ super(
168
+ PermutationSegment.fromJSONObject,
169
+ ChildLogger.create(logger, `Matrix.${path}.MergeTreeClient`),
170
+ {
171
+ ...runtime.options,
172
+ newMergeTreeSnapshotFormat: true, // Temporarily force new snapshot format until it is the default.
173
+ },
174
+ ); // (See https://github.com/microsoft/FluidFramework/issues/84)
175
+
176
+ this.on("delta", this.onDelta);
177
+ this.on("maintenance", this.onMaintenance);
178
+ }
179
+
180
+ public insert(start: number, length: number) {
181
+ return this.insertSegmentLocal(start, new PermutationSegment(length));
182
+ }
183
+
184
+ public insertRelative(segment: ISegment, length: number) {
185
+ const inserted = new PermutationSegment(length);
186
+
187
+ return {
188
+ op: this.insertAtReferencePositionLocal(
189
+ this.createLocalReferencePosition(segment, 0, ReferenceType.Transient, undefined),
190
+ inserted,
191
+ ),
192
+ inserted,
193
+ };
194
+ }
195
+
196
+ public remove(start: number, length: number) {
197
+ return this.removeRangeLocal(start, start + length);
198
+ }
199
+
200
+ public getMaybeHandle(pos: number): Handle {
201
+ assert(
202
+ 0 <= pos && pos < this.getLength(),
203
+ 0x027 /* "Trying to get handle of out-of-bounds position!" */,
204
+ );
205
+
206
+ return this.handleCache.getHandle(pos);
207
+ }
208
+
209
+ public getAllocatedHandle(pos: number): Handle {
210
+ let handle = this.getMaybeHandle(pos);
211
+ if (isHandleValid(handle)) {
212
+ return handle;
213
+ }
214
+
215
+ this.walkSegments(
216
+ (segment) => {
217
+ const asPerm = segment as PermutationSegment;
218
+ asPerm.start = handle = this.handleTable.allocate();
219
+ return true;
220
+ },
221
+ pos,
222
+ pos + 1,
223
+ /* accum: */ undefined,
224
+ /* splitRange: */ true,
225
+ );
226
+
227
+ this.handleCache.addHandle(pos, handle);
228
+
229
+ return handle;
230
+ }
231
+
232
+ public adjustPosition(pos: number, op: ISequencedDocumentMessage) {
233
+ const { segment, offset } = this.getContainingSegment(pos, {
234
+ referenceSequenceNumber: op.referenceSequenceNumber,
235
+ clientId: op.clientId,
236
+ });
237
+
238
+ // Note that until the MergeTree GCs, the segment is still reachable via `getContainingSegment()` with
239
+ // a `refSeq` in the past. Prevent remote ops from accidentally allocating or using recycled handles
240
+ // by checking for the presence of 'removedSeq'.
241
+ if (segment === undefined || segment.removedSeq !== undefined) {
242
+ return undefined;
243
+ }
244
+
245
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
246
+ return this.getPosition(segment) + offset!;
247
+ }
248
+
249
+ public handleToPosition(handle: Handle, localSeq = this.getCollabWindow().localSeq) {
250
+ assert(
251
+ localSeq <= this.getCollabWindow().localSeq,
252
+ 0x028 /* "'localSeq' for op being resubmitted must be <= the 'localSeq' of the last submitted op." */,
253
+ );
254
+
255
+ // TODO: In theory, the MergeTree should be able to map the (position, refSeq, localSeq) from
256
+ // the original operation to the current position for undo/redo scenarios. This is probably the
257
+ // ideal solution, as we would no longer need to store row/col handles in the op metadata.
258
+ //
259
+ // Failing that, we could avoid the O(n) search below by building a temporary map in the
260
+ // opposite direction from the handle to either it's current position or segment + offset
261
+ // and reuse it for the duration of undo/redo. (Ideally, we would know when the undo/redo
262
+ // ended so we could discard this map.)
263
+ //
264
+ // If we find that we frequently need a reverse handle -> position lookup, we could maintain
265
+ // one using the Tiny-Calc adjust tree.
266
+ let containingSegment!: PermutationSegment;
267
+ let containingOffset: number;
268
+
269
+ this.walkAllSegments((segment) => {
270
+ const { start, cachedLength } = segment as PermutationSegment;
271
+
272
+ // If the segment is unallocated, skip it.
273
+ if (!isHandleValid(start)) {
274
+ return true;
275
+ }
276
+
277
+ const end = start + cachedLength;
278
+
279
+ if (start <= handle && handle < end) {
280
+ containingSegment = segment as PermutationSegment;
281
+ containingOffset = handle - start;
282
+ return false;
283
+ }
284
+
285
+ return true;
286
+ });
287
+
288
+ // We are guaranteed to find the handle in the PermutationVector, even if the corresponding
289
+ // row/col has been removed, because handles are not recycled until the containing segment
290
+ // is unlinked from the MergeTree.
291
+ //
292
+ // Therefore, either a row/col removal has been ACKed, in which case there will be no pending
293
+ // ops that reference the stale handle, or the removal is unACKed, in which case the handle
294
+ // has not yet been recycled.
295
+
296
+ assert(
297
+ isHandleValid(containingSegment.start),
298
+ 0x029 /* "Invalid handle at start of containing segment!" */,
299
+ );
300
+
301
+ // Once we know the current position of the handle, we can use the MergeTree to get the segment
302
+ // containing this position and use 'findReconnectionPosition' to adjust for the local ops that
303
+ // have not yet been submitted.
304
+
305
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
306
+ return this.findReconnectionPosition(containingSegment, localSeq) + containingOffset!;
307
+ }
308
+
309
+ // Constructs an ISummaryTreeWithStats for the cell data.
310
+ public summarize(
311
+ runtime: IFluidDataStoreRuntime,
312
+ handle: IFluidHandle,
313
+ serializer: IFluidSerializer,
314
+ ): ISummaryTreeWithStats {
315
+ const builder = new SummaryTreeBuilder();
316
+ builder.addWithStats(
317
+ SnapshotPath.segments,
318
+ super.summarize(runtime, handle, serializer, /* catchUpMsgs: */ []),
319
+ );
320
+ builder.addBlob(
321
+ SnapshotPath.handleTable,
322
+ serializer.stringify(this.handleTable.getSummaryContent(), handle),
323
+ );
324
+ return builder.getSummaryTree();
325
+ }
326
+
327
+ public async load(
328
+ runtime: IFluidDataStoreRuntime,
329
+ storage: IChannelStorageService,
330
+ serializer: IFluidSerializer,
331
+ ) {
332
+ const handleTableData = await deserializeBlob(
333
+ storage,
334
+ SnapshotPath.handleTable,
335
+ serializer,
336
+ );
337
+
338
+ this.handleTable = HandleTable.load<never>(handleTableData);
339
+
340
+ return super.load(
341
+ runtime,
342
+ new ObjectStoragePartition(storage, SnapshotPath.segments),
343
+ serializer,
344
+ );
345
+ }
346
+
347
+ private readonly onDelta = (
348
+ opArgs: IMergeTreeDeltaOpArgs,
349
+ { operation, deltaSegments }: IMergeTreeDeltaCallbackArgs,
350
+ ) => {
351
+ // Apply deltas in descending order to prevent positions from shifting.
352
+ const ranges = deltaSegments
353
+ .map(({ segment }) => ({
354
+ segment: segment as PermutationSegment,
355
+ position: this.getPosition(segment),
356
+ }))
357
+ .sort((left, right) => left.position - right.position);
358
+
359
+ const isLocal = opArgs.sequencedMessage === undefined;
360
+
361
+ // Notify the undo provider, if any is attached.
362
+ if (this.undo !== undefined && isLocal) {
363
+ this.undo.record(operation, ranges);
364
+ }
365
+
366
+ switch (operation) {
367
+ case MergeTreeDeltaType.INSERT:
368
+ // Pass 1: Perform any internal maintenance first to avoid reentrancy.
369
+ for (const { segment, position } of ranges) {
370
+ // HACK: We need to include the allocated handle in the segment's JSON representation
371
+ // for snapshots, but need to ignore the remote client's handle allocations when
372
+ // processing remote ops.
373
+ segment.reset();
374
+
375
+ this.handleCache.itemsChanged(
376
+ position,
377
+ /* deleteCount: */ 0,
378
+ /* insertCount: */ segment.cachedLength,
379
+ );
380
+ }
381
+
382
+ // Pass 2: Notify the 'deltaCallback', which may involve callbacks into user code.
383
+ for (const { segment, position } of ranges) {
384
+ this.deltaCallback(
385
+ position,
386
+ /* numRemoved: */ 0,
387
+ /* numInserted: */ segment.cachedLength,
388
+ );
389
+ }
390
+ break;
391
+
392
+ case MergeTreeDeltaType.REMOVE: {
393
+ // Pass 1: Perform any internal maintenance first to avoid reentrancy.
394
+ for (const { segment, position } of ranges) {
395
+ this.handleCache.itemsChanged(
396
+ position /* deleteCount: */,
397
+ segment.cachedLength,
398
+ /* insertCount: */ 0,
399
+ );
400
+ }
401
+
402
+ // Pass 2: Notify the 'deltaCallback', which may involve callbacks into user code.
403
+ for (const { segment, position } of ranges) {
404
+ this.deltaCallback(
405
+ position,
406
+ /* numRemoved: */ segment.cachedLength,
407
+ /* numInsert: */ 0,
408
+ );
409
+ }
410
+ break;
411
+ }
412
+
413
+ default:
414
+ throw new Error("Unhandled MergeTreeDeltaType");
415
+ }
416
+ };
417
+
418
+ private readonly onMaintenance = (args: IMergeTreeMaintenanceCallbackArgs) => {
419
+ if (args.operation === MergeTreeMaintenanceType.UNLINK) {
420
+ let freed: number[] = [];
421
+
422
+ for (const { segment } of args.deltaSegments) {
423
+ const asPerm = segment as PermutationSegment;
424
+ if (isHandleValid(asPerm.start)) {
425
+ // Note: Using the spread operator with `.splice()` can exhaust the stack.
426
+ freed = freed.concat(
427
+ new Array(asPerm.cachedLength)
428
+ .fill(0)
429
+ .map((value, index) => index + asPerm.start),
430
+ );
431
+ }
432
+ }
433
+
434
+ // Notify matrix that handles are about to be freed. The matrix is responsible for clearing
435
+ // the rows/cols prior to free to ensure recycled row/cols are initially empty.
436
+ this.handlesRecycledCallback(freed);
437
+
438
+ // Now that the physical storage has been cleared, add the recycled handles back to the free pool.
439
+ for (const handle of freed) {
440
+ this.handleTable.free(handle);
441
+ }
442
+ }
443
+ };
444
+
445
+ public toString() {
446
+ const s: string[] = [];
447
+
448
+ this.walkSegments((segment) => {
449
+ s.push(`${segment}`);
450
+ return true;
451
+ });
452
+
453
+ return s.join("");
454
+ }
398
455
  }