@fluidframework/legacy-dds 2.50.0-345060

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 (140) hide show
  1. package/.eslintrc.cjs +11 -0
  2. package/.mocharc.cjs +12 -0
  3. package/CHANGELOG.md +13 -0
  4. package/LICENSE +21 -0
  5. package/api-extractor/api-extractor-lint-bundle.json +5 -0
  6. package/api-extractor/api-extractor-lint-legacy.cjs.json +5 -0
  7. package/api-extractor/api-extractor-lint-legacy.esm.json +5 -0
  8. package/api-extractor/api-extractor-lint-public.cjs.json +5 -0
  9. package/api-extractor/api-extractor-lint-public.esm.json +5 -0
  10. package/api-extractor/api-extractor.current.json +5 -0
  11. package/api-extractor/api-extractor.legacy.json +4 -0
  12. package/api-extractor-lint.json +4 -0
  13. package/api-extractor.json +4 -0
  14. package/api-report/legacy-dds.beta.api.md +9 -0
  15. package/api-report/legacy-dds.legacy.alpha.api.md +140 -0
  16. package/api-report/legacy-dds.legacy.public.api.md +9 -0
  17. package/api-report/legacy-dds.public.api.md +9 -0
  18. package/biome.jsonc +4 -0
  19. package/dist/array/index.d.ts +10 -0
  20. package/dist/array/index.d.ts.map +1 -0
  21. package/dist/array/index.js +16 -0
  22. package/dist/array/index.js.map +1 -0
  23. package/dist/array/interfaces.d.ts +142 -0
  24. package/dist/array/interfaces.d.ts.map +1 -0
  25. package/dist/array/interfaces.js +7 -0
  26. package/dist/array/interfaces.js.map +1 -0
  27. package/dist/array/sharedArray.d.ts +175 -0
  28. package/dist/array/sharedArray.d.ts.map +1 -0
  29. package/dist/array/sharedArray.js +652 -0
  30. package/dist/array/sharedArray.js.map +1 -0
  31. package/dist/array/sharedArrayFactory.d.ts +31 -0
  32. package/dist/array/sharedArrayFactory.d.ts.map +1 -0
  33. package/dist/array/sharedArrayFactory.js +61 -0
  34. package/dist/array/sharedArrayFactory.js.map +1 -0
  35. package/dist/array/sharedArrayOperations.d.ts +77 -0
  36. package/dist/array/sharedArrayOperations.d.ts.map +1 -0
  37. package/dist/array/sharedArrayOperations.js +19 -0
  38. package/dist/array/sharedArrayOperations.js.map +1 -0
  39. package/dist/array/sharedArrayRevertible.d.ts +17 -0
  40. package/dist/array/sharedArrayRevertible.d.ts.map +1 -0
  41. package/dist/array/sharedArrayRevertible.js +47 -0
  42. package/dist/array/sharedArrayRevertible.js.map +1 -0
  43. package/dist/index.d.ts +14 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +20 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/legacy.d.ts +31 -0
  48. package/dist/package.json +4 -0
  49. package/dist/packageVersion.d.ts +9 -0
  50. package/dist/packageVersion.d.ts.map +1 -0
  51. package/dist/packageVersion.js +12 -0
  52. package/dist/packageVersion.js.map +1 -0
  53. package/dist/public.d.ts +12 -0
  54. package/dist/signal/index.d.ts +7 -0
  55. package/dist/signal/index.d.ts.map +1 -0
  56. package/dist/signal/index.js +11 -0
  57. package/dist/signal/index.js.map +1 -0
  58. package/dist/signal/interfaces.d.ts +36 -0
  59. package/dist/signal/interfaces.d.ts.map +1 -0
  60. package/dist/signal/interfaces.js +7 -0
  61. package/dist/signal/interfaces.js.map +1 -0
  62. package/dist/signal/sharedSignal.d.ts +68 -0
  63. package/dist/signal/sharedSignal.d.ts.map +1 -0
  64. package/dist/signal/sharedSignal.js +122 -0
  65. package/dist/signal/sharedSignal.js.map +1 -0
  66. package/dist/signal/sharedSignalFactory.d.ts +24 -0
  67. package/dist/signal/sharedSignalFactory.d.ts.map +1 -0
  68. package/dist/signal/sharedSignalFactory.js +45 -0
  69. package/dist/signal/sharedSignalFactory.js.map +1 -0
  70. package/internal.d.ts +11 -0
  71. package/legacy.d.ts +11 -0
  72. package/lib/array/index.d.ts +10 -0
  73. package/lib/array/index.d.ts.map +1 -0
  74. package/lib/array/index.js +8 -0
  75. package/lib/array/index.js.map +1 -0
  76. package/lib/array/interfaces.d.ts +142 -0
  77. package/lib/array/interfaces.d.ts.map +1 -0
  78. package/lib/array/interfaces.js +6 -0
  79. package/lib/array/interfaces.js.map +1 -0
  80. package/lib/array/sharedArray.d.ts +175 -0
  81. package/lib/array/sharedArray.d.ts.map +1 -0
  82. package/lib/array/sharedArray.js +648 -0
  83. package/lib/array/sharedArray.js.map +1 -0
  84. package/lib/array/sharedArrayFactory.d.ts +31 -0
  85. package/lib/array/sharedArrayFactory.d.ts.map +1 -0
  86. package/lib/array/sharedArrayFactory.js +56 -0
  87. package/lib/array/sharedArrayFactory.js.map +1 -0
  88. package/lib/array/sharedArrayOperations.d.ts +77 -0
  89. package/lib/array/sharedArrayOperations.d.ts.map +1 -0
  90. package/lib/array/sharedArrayOperations.js +16 -0
  91. package/lib/array/sharedArrayOperations.js.map +1 -0
  92. package/lib/array/sharedArrayRevertible.d.ts +17 -0
  93. package/lib/array/sharedArrayRevertible.d.ts.map +1 -0
  94. package/lib/array/sharedArrayRevertible.js +43 -0
  95. package/lib/array/sharedArrayRevertible.js.map +1 -0
  96. package/lib/index.d.ts +14 -0
  97. package/lib/index.d.ts.map +1 -0
  98. package/lib/index.js +10 -0
  99. package/lib/index.js.map +1 -0
  100. package/lib/legacy.d.ts +31 -0
  101. package/lib/packageVersion.d.ts +9 -0
  102. package/lib/packageVersion.d.ts.map +1 -0
  103. package/lib/packageVersion.js +9 -0
  104. package/lib/packageVersion.js.map +1 -0
  105. package/lib/public.d.ts +12 -0
  106. package/lib/signal/index.d.ts +7 -0
  107. package/lib/signal/index.d.ts.map +1 -0
  108. package/lib/signal/index.js +6 -0
  109. package/lib/signal/index.js.map +1 -0
  110. package/lib/signal/interfaces.d.ts +36 -0
  111. package/lib/signal/interfaces.d.ts.map +1 -0
  112. package/lib/signal/interfaces.js +6 -0
  113. package/lib/signal/interfaces.js.map +1 -0
  114. package/lib/signal/sharedSignal.d.ts +68 -0
  115. package/lib/signal/sharedSignal.d.ts.map +1 -0
  116. package/lib/signal/sharedSignal.js +118 -0
  117. package/lib/signal/sharedSignal.js.map +1 -0
  118. package/lib/signal/sharedSignalFactory.d.ts +24 -0
  119. package/lib/signal/sharedSignalFactory.d.ts.map +1 -0
  120. package/lib/signal/sharedSignalFactory.js +41 -0
  121. package/lib/signal/sharedSignalFactory.js.map +1 -0
  122. package/lib/tsdoc-metadata.json +11 -0
  123. package/package.json +158 -0
  124. package/src/array/README.md +32 -0
  125. package/src/array/index.ts +28 -0
  126. package/src/array/interfaces.ts +169 -0
  127. package/src/array/sharedArray.ts +835 -0
  128. package/src/array/sharedArrayFactory.ts +88 -0
  129. package/src/array/sharedArrayOperations.ts +89 -0
  130. package/src/array/sharedArrayRevertible.ts +50 -0
  131. package/src/index.ts +32 -0
  132. package/src/packageVersion.ts +9 -0
  133. package/src/signal/README.md +25 -0
  134. package/src/signal/index.ts +12 -0
  135. package/src/signal/interfaces.ts +53 -0
  136. package/src/signal/sharedSignal.ts +169 -0
  137. package/src/signal/sharedSignalFactory.ts +62 -0
  138. package/tsconfig.cjs.json +7 -0
  139. package/tsconfig.json +10 -0
  140. package/tsdoc.json +4 -0
@@ -0,0 +1,835 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { assert } from "@fluidframework/core-utils/internal";
7
+ import type {
8
+ Serializable,
9
+ IChannelAttributes,
10
+ IFluidDataStoreRuntime,
11
+ IChannelFactory,
12
+ IChannelStorageService,
13
+ } from "@fluidframework/datastore-definitions/internal";
14
+ import type {
15
+ ISequencedDocumentMessage,
16
+ ITree,
17
+ } from "@fluidframework/driver-definitions/internal";
18
+ import { FileMode, MessageType, TreeEntry } from "@fluidframework/driver-definitions/internal";
19
+ import type { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions/internal";
20
+ import { convertToSummaryTreeWithStats } from "@fluidframework/runtime-utils/internal";
21
+ import type { IFluidSerializer } from "@fluidframework/shared-object-base/internal";
22
+ import { SharedObject } from "@fluidframework/shared-object-base/internal";
23
+ import { v4 as uuid } from "uuid";
24
+
25
+ import type {
26
+ ISharedArrayEvents,
27
+ ISharedArray,
28
+ ISharedArrayRevertible,
29
+ SerializableTypeForSharedArray,
30
+ SharedArrayEntry,
31
+ SnapshotFormat,
32
+ SharedArrayEntryCore,
33
+ } from "./interfaces.js";
34
+ import { SharedArrayFactory } from "./sharedArrayFactory.js";
35
+ import type {
36
+ ISharedArrayOperation,
37
+ IDeleteOperation,
38
+ IMoveOperation,
39
+ IToggleMoveOperation,
40
+ IToggleOperation,
41
+ } from "./sharedArrayOperations.js";
42
+ import { OperationType } from "./sharedArrayOperations.js";
43
+ import { SharedArrayRevertible } from "./sharedArrayRevertible.js";
44
+
45
+ const snapshotFileName = "header";
46
+
47
+ /**
48
+ * Represents a shared array that allows communication between distributed clients.
49
+ *
50
+ * @internal
51
+ */
52
+ export class SharedArrayClass<T extends SerializableTypeForSharedArray>
53
+ extends SharedObject<ISharedArrayEvents>
54
+ implements ISharedArray<T>, ISharedArrayRevertible
55
+ {
56
+ /**
57
+ * Stores the data held by this shared array.
58
+ */
59
+ private sharedArray: SharedArrayEntry<T>[];
60
+
61
+ /**
62
+ * Stores a map of entryid to entries of the sharedArray. This is meant of search optimizations and
63
+ * so shouldn't be snapshotted.
64
+ * Note: This map needs to be updated only when the sharedArray is being deserialized and when new entries are
65
+ * being added. New entries are added upon insert ops and the second leg of the move op.
66
+ * As we don't delete the entries once created, deletion or move to another position needs no special
67
+ * handling for this data structure
68
+ */
69
+ private readonly idToEntryMap: Map<string, SharedArrayEntry<T>>;
70
+
71
+ /**
72
+ * Create a new shared array
73
+ *
74
+ * @param runtime - data store runtime the new shared array belongs to
75
+ * @param id - optional name of the shared array
76
+ * @returns newly create shared array (but not attached yet)
77
+ */
78
+ public static create<T extends SerializableTypeForSharedArray>(
79
+ runtime: IFluidDataStoreRuntime,
80
+ id?: string,
81
+ ): SharedArrayClass<T> {
82
+ return runtime.createChannel(id, SharedArrayFactory.Type) as SharedArrayClass<T>;
83
+ }
84
+
85
+ /**
86
+ * Get a factory for SharedArray to register with the data store.
87
+ *
88
+ * @returns a factory that creates and load SharedArray
89
+ */
90
+ public static getFactory<T extends SerializableTypeForSharedArray>(): IChannelFactory {
91
+ return new SharedArrayFactory<T>();
92
+ }
93
+
94
+ /**
95
+ * Constructs a new shared array. If the object is non-local an id and service interfaces will
96
+ * be provided
97
+ *
98
+ * @param id - optional name of the shared array
99
+ * @param runtime - data store runtime the shared array belongs to
100
+ * @param attributes - represents the attributes of a channel/DDS.
101
+ */
102
+ public constructor(
103
+ id: string,
104
+ runtime: IFluidDataStoreRuntime,
105
+ attributes: IChannelAttributes,
106
+ ) {
107
+ super(id, runtime, attributes, "loop_sharedArray_" /* telemetryContextPrefix */);
108
+ this.sharedArray = [];
109
+ this.idToEntryMap = new Map<string, SharedArrayEntry<T>>();
110
+ }
111
+
112
+ /**
113
+ * Method that returns the ordered list of the items held in the DDS at this point in time.
114
+ * Note: This is only a snapshot of the array
115
+ */
116
+ public get(): readonly T[] {
117
+ return this.sharedArray.filter((item) => !item.isDeleted).map((entry) => entry.value);
118
+ }
119
+
120
+ protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
121
+ // Deep copy and unset the local flags. Needed when snapshotting is happening for runtime not attached
122
+ const dataArrayCopy: SharedArrayEntryCore<T>[] = [];
123
+ for (const entry of this.sharedArray) {
124
+ dataArrayCopy.push({
125
+ entryId: entry.entryId,
126
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
127
+ value: JSON.parse(serializer.stringify(entry.value, this.handle)),
128
+ isDeleted: entry.isDeleted,
129
+ prevEntryId: entry.prevEntryId,
130
+ nextEntryId: entry.nextEntryId,
131
+ });
132
+ }
133
+
134
+ // We are snapshotting current client data so autoacking pending local.
135
+ // Assumption : This should happen only for offline client creating the array. All other scenarios should
136
+ // get to MSN - where there can be no local pending possible.
137
+ for (const entry of this.sharedArray) {
138
+ this.unsetLocalFlags(entry);
139
+ }
140
+ const contents: SnapshotFormat<SharedArrayEntryCore<T>> = {
141
+ dataArray: dataArrayCopy,
142
+ };
143
+ const tree: ITree = {
144
+ entries: [
145
+ {
146
+ mode: FileMode.File,
147
+ path: snapshotFileName,
148
+ type: TreeEntry[TreeEntry.Blob],
149
+ value: {
150
+ contents: serializer.stringify(contents, this.handle),
151
+ // eslint-disable-next-line unicorn/text-encoding-identifier-case
152
+ encoding: "utf-8",
153
+ },
154
+ },
155
+ ],
156
+ };
157
+ const summaryTreeWithStats = convertToSummaryTreeWithStats(tree);
158
+
159
+ return summaryTreeWithStats;
160
+ }
161
+
162
+ public insertBulkAfter<TWrite>(
163
+ ref: T | undefined,
164
+ values: (Serializable<TWrite> & T)[],
165
+ ): void {
166
+ let itemIndex: number = 0;
167
+
168
+ if (ref !== undefined) {
169
+ for (itemIndex = this.sharedArray.length - 1; itemIndex > 0; itemIndex -= 1) {
170
+ const item = this.sharedArray[itemIndex];
171
+ if (item && !item.isDeleted && item.value === ref) {
172
+ break;
173
+ }
174
+ }
175
+ // Add one since we're inserting it after this rowId. If rowId is not found, we will get -1, which after
176
+ // adding one, will be 0, which will place the new rows at the right place too
177
+ itemIndex += 1;
178
+ }
179
+
180
+ // Insert new elements
181
+ for (const value of values) {
182
+ this.insertCore(itemIndex, value);
183
+ itemIndex += 1;
184
+ }
185
+ }
186
+
187
+ public insert<TWrite>(index: number, value: Serializable<TWrite> & T): void {
188
+ if (index < 0) {
189
+ throw new Error("Invalid input: Insertion index provided is less than 0.");
190
+ }
191
+ this.insertCore(this.findInternalInsertionIndex(index), value);
192
+ }
193
+
194
+ private insertCore<TWrite>(indexInternal: number, value: Serializable<TWrite> & T): void {
195
+ const insertAfterEntryId =
196
+ indexInternal >= 1 ? this.sharedArray[indexInternal - 1]?.entryId : undefined;
197
+ const newEntryId = this.createAddEntry(indexInternal, value);
198
+
199
+ const op = {
200
+ type: OperationType.insertEntry,
201
+ entryId: newEntryId,
202
+ value,
203
+ insertAfterEntryId,
204
+ };
205
+
206
+ this.emitValueChangedEvent(op, true /* isLocal */);
207
+ this.emitRevertibleEvent(op);
208
+
209
+ // If we are not attached, don't submit the op.
210
+ if (!this.isAttached()) {
211
+ return;
212
+ }
213
+
214
+ this.submitLocalMessage(op);
215
+ }
216
+
217
+ public delete(index: number): void {
218
+ if (index < 0) {
219
+ throw new Error("Invalid input: Deletion index provided is less than 0.");
220
+ }
221
+
222
+ const indexInternal: number = this.findInternalDeletionIndex(index);
223
+
224
+ const entry = this.sharedArray[indexInternal];
225
+ assert(entry !== undefined, 0xb90 /* Invalid index */);
226
+ const entryId = entry.entryId;
227
+ this.deleteCore(indexInternal);
228
+
229
+ const op: IDeleteOperation = {
230
+ type: OperationType.deleteEntry,
231
+ entryId,
232
+ };
233
+ this.emitValueChangedEvent(op, true /* isLocal */);
234
+ this.emitRevertibleEvent(op);
235
+
236
+ // If we are not attached, don't submit the op.
237
+ if (!this.isAttached()) {
238
+ return;
239
+ }
240
+
241
+ this.submitLocalMessage(op);
242
+ }
243
+
244
+ public rearrangeToFront(values: T[]): void {
245
+ for (let toIndex = 0; toIndex < values.length; toIndex += 1) {
246
+ const value = values[toIndex];
247
+ // Can skip searching first <toIndex> indices, as they contain elements we already moved.
248
+ for (let fromIndex = toIndex; fromIndex < this.sharedArray.length; fromIndex += 1) {
249
+ const item = this.sharedArray[fromIndex];
250
+ assert(item !== undefined, 0xb91 /* Invalid index */);
251
+ if (item.value !== value) {
252
+ continue;
253
+ }
254
+ if (
255
+ !item.isDeleted &&
256
+ // Moving to and from the same index makes no sense, so noOp
257
+ fromIndex !== toIndex &&
258
+ // Moving the same entry from current location to its immediate next makes no sense so noOp
259
+ toIndex !== fromIndex + 1
260
+ ) {
261
+ this.moveCore(fromIndex, toIndex);
262
+ }
263
+ break;
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Moves the DDS entry from one index to another
270
+ *
271
+ * @param fromIndex - User index of the element to be moved
272
+ * @param toIndex - User index to which the element should move to
273
+ */
274
+ public move(fromIndex: number, toIndex: number): void {
275
+ if (fromIndex < 0) {
276
+ throw new Error("Invalid input: fromIndex value provided is less than 0");
277
+ }
278
+
279
+ if (toIndex < 0) {
280
+ throw new Error("Invalid input: toIndex value provided is less than 0");
281
+ }
282
+
283
+ if (
284
+ // Moving to and from the same index makes no sense, so noOp
285
+ fromIndex === toIndex ||
286
+ // Moving the same entry from current location to its immediate next makes no sense so noOp
287
+ toIndex === fromIndex + 1
288
+ ) {
289
+ return;
290
+ }
291
+ const fromIndexInternal: number = this.findInternalDeletionIndex(fromIndex);
292
+ const toIndexInternal: number = this.findInternalInsertionIndex(toIndex);
293
+
294
+ this.moveCore(fromIndexInternal, toIndexInternal);
295
+ }
296
+
297
+ private moveCore(fromIndexInternal: number, toIndexInternal: number): void {
298
+ const insertAfterEntryId =
299
+ toIndexInternal >= 1 ? this.sharedArray[toIndexInternal - 1]?.entryId : undefined;
300
+ const entryId = this.sharedArray[fromIndexInternal]?.entryId;
301
+ assert(entryId !== undefined, 0xb92 /* Invalid index */);
302
+ const changedToEntryId = this.createMoveEntry(fromIndexInternal, toIndexInternal);
303
+
304
+ const op: IMoveOperation = {
305
+ type: OperationType.moveEntry,
306
+ entryId,
307
+ insertAfterEntryId,
308
+ changedToEntryId,
309
+ };
310
+ this.emitValueChangedEvent(op, true /* isLocal */);
311
+ this.emitRevertibleEvent(op);
312
+
313
+ // If we are not attached, don't submit the op.
314
+ if (!this.isAttached()) {
315
+ return;
316
+ }
317
+
318
+ this.submitLocalMessage(op);
319
+ }
320
+
321
+ /**
322
+ * Method used to do undo/redo operation for the given entry id. This method is
323
+ * used for undo/redo of only insert and delete operations. Move operation is NOT handled
324
+ * by this method
325
+ *
326
+ * @param entryId - Entry Id for which the the undo/redo operation is to be applied
327
+ */
328
+ public toggle(entryId: string): void {
329
+ const liveEntry = this.getLiveEntry(entryId);
330
+ const isDeleted = !liveEntry.isDeleted;
331
+
332
+ // Adding local pending counter
333
+ this.getEntryForId(entryId).isLocalPendingDelete += 1;
334
+
335
+ // Toggling the isDeleted flag to undo the last operation for the skip list payload/value
336
+ liveEntry.isDeleted = isDeleted;
337
+
338
+ const op: IToggleOperation = {
339
+ type: OperationType.toggle,
340
+ entryId,
341
+ isDeleted,
342
+ };
343
+ this.emitValueChangedEvent(op, true /* isLocal */);
344
+ this.emitRevertibleEvent(op);
345
+
346
+ // If we are not attached, don't submit the op.
347
+ if (!this.isAttached()) {
348
+ return;
349
+ }
350
+
351
+ this.submitLocalMessage(op);
352
+ }
353
+
354
+ /**
355
+ * Method to do undo/redo of move operation. All entries of the same payload/value are stored
356
+ * in the same doubly linked skip list. This skip list is updated upon every move by adding the
357
+ * new location as a new entry in the skip list and update the isDeleted flag to indicate the new
358
+ * entry is the cuurent live location for the user.
359
+ *
360
+ * @param oldEntryId - EntryId of the last live entry
361
+ * @param newEntryId - EntryId of the to be live entry
362
+ */
363
+ public toggleMove(oldEntryId: string, newEntryId: string): void {
364
+ if (this.getEntryForId(newEntryId).isDeleted) {
365
+ return;
366
+ }
367
+
368
+ // Adding local pending counter
369
+ this.getEntryForId(oldEntryId).isLocalPendingMove += 1;
370
+
371
+ this.updateLiveEntry(newEntryId, oldEntryId);
372
+
373
+ const op: IToggleMoveOperation = {
374
+ type: OperationType.toggleMove,
375
+ entryId: oldEntryId,
376
+ changedToEntryId: newEntryId,
377
+ };
378
+ this.emitValueChangedEvent(op, true /* isLocal */);
379
+ this.emitRevertibleEvent(op);
380
+
381
+ // If we are not attached, don't submit the op.
382
+ if (!this.isAttached()) {
383
+ return;
384
+ }
385
+
386
+ this.submitLocalMessage(op);
387
+ }
388
+
389
+ /**
390
+ * Load share array from snapshot
391
+ *
392
+ * @param storage - the storage to get the snapshot from
393
+ * @returns - promise that resolved when the load is completed
394
+ */
395
+ protected async loadCore(storage: IChannelStorageService): Promise<void> {
396
+ const header = await storage.readBlob(snapshotFileName);
397
+ // eslint-disable-next-line unicorn/text-encoding-identifier-case
398
+ const utf8 = new TextDecoder("utf-8").decode(header);
399
+ // Note: IFluidSerializer.parse() doesn't guarantee any typing; the explicit typing here is based on this code's
400
+ // knowledge of what it is deserializing.
401
+ const deserializedSharedArray = this.serializer.parse(utf8) as {
402
+ dataArray: SharedArrayEntry<T>[];
403
+ };
404
+ this.sharedArray = deserializedSharedArray.dataArray;
405
+ // Initializing the idToEntryMap optimizer data set
406
+ for (const entry of this.sharedArray) {
407
+ this.idToEntryMap.set(entry.entryId, entry);
408
+ this.unsetLocalFlags(entry);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Callback on disconnect
414
+ */
415
+ protected onDisconnect(): void {}
416
+
417
+ /**
418
+ * Tracks the doubly linked skip list for the given entry to identify local pending counter attribute.
419
+ * It signifies if a local pending operation exists for the payload/value being tracked in the skip list
420
+ *
421
+ * returns true if counterAttribute's count \> 0
422
+ * @param entryId - id for which counter attribute is to be tracked in chian.
423
+ * @param counterAttribute - flag or property name from SharedArrayEntry whose counter is to be tracked.
424
+ */
425
+ private isLocalPending(
426
+ entryId: string,
427
+ counterAttribute: keyof SharedArrayEntry<T>,
428
+ ): boolean {
429
+ const getCounterAttributeValue = (
430
+ entry: SharedArrayEntry<T>,
431
+ counterAttr: keyof SharedArrayEntry<T>,
432
+ ): number => {
433
+ return entry[counterAttr] as number;
434
+ };
435
+
436
+ const inputEntry = this.getEntryForId(entryId);
437
+ let prevEntryId = inputEntry.prevEntryId;
438
+ let nextEntryId = inputEntry.nextEntryId;
439
+ if (getCounterAttributeValue(inputEntry, counterAttribute) > 0) {
440
+ return true;
441
+ }
442
+ // track back in chain
443
+ while (prevEntryId !== undefined && prevEntryId) {
444
+ const prevEntry = this.getEntryForId(prevEntryId);
445
+ if (getCounterAttributeValue(prevEntry, counterAttribute)) {
446
+ return true;
447
+ }
448
+ prevEntryId = prevEntry.prevEntryId;
449
+ }
450
+
451
+ // track forward in the chain
452
+ while (nextEntryId !== undefined && nextEntryId) {
453
+ const nextEntry = this.getEntryForId(nextEntryId);
454
+ if (getCounterAttributeValue(nextEntry, counterAttribute)) {
455
+ return true;
456
+ }
457
+ nextEntryId = nextEntry.nextEntryId;
458
+ }
459
+
460
+ return false;
461
+ }
462
+
463
+ private getEntryForId(entryId: string): SharedArrayEntry<T> {
464
+ return this.idToEntryMap.get(entryId) as SharedArrayEntry<T>;
465
+ }
466
+
467
+ /**
468
+ * Process a shared array operation
469
+ *
470
+ * @param message - the message to prepare
471
+ * @param local - whether the message was sent by the local client
472
+ * @param _localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
473
+ * For messages from a remote client, this will be undefined.
474
+ */
475
+ protected processCore(
476
+ message: ISequencedDocumentMessage,
477
+ local: boolean,
478
+ _localOpMetadata: unknown,
479
+ ): void {
480
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
481
+ if (message.type === MessageType.Operation) {
482
+ const op = message.contents as ISharedArrayOperation<T>;
483
+ const opEntry = this.getEntryForId(op.entryId);
484
+
485
+ switch (op.type) {
486
+ case OperationType.insertEntry: {
487
+ this.handleInsertOp<SerializableTypeForSharedArray>(
488
+ op.entryId,
489
+ op.insertAfterEntryId,
490
+ local,
491
+ op.value,
492
+ );
493
+
494
+ break;
495
+ }
496
+
497
+ case OperationType.deleteEntry: {
498
+ if (local) {
499
+ // Decrementing local pending counter as op is already applied to local state
500
+ opEntry.isLocalPendingDelete -= 1;
501
+ } else {
502
+ // If local pending, then ignore else apply the remote op
503
+ if (!this.isLocalPending(op.entryId, "isLocalPendingDelete")) {
504
+ // last element in skip list is the most recent and live entry, so marking it deleted
505
+ this.getLiveEntry(op.entryId).isDeleted = true;
506
+ }
507
+ }
508
+
509
+ break;
510
+ }
511
+
512
+ case OperationType.moveEntry: {
513
+ this.handleInsertOp<SerializableTypeForSharedArray>(
514
+ op.changedToEntryId,
515
+ op.insertAfterEntryId,
516
+ local,
517
+ opEntry.value,
518
+ );
519
+ if (local) {
520
+ // decrement the local pending move op as its already applied to local state
521
+ opEntry.isLocalPendingMove -= 1;
522
+ } else {
523
+ const newElementEntryId = op.changedToEntryId;
524
+ const newElement = this.getEntryForId(newElementEntryId);
525
+ // If local pending then simply mark the new location dead as finally the local op will win
526
+ if (
527
+ this.isLocalPending(op.entryId, "isLocalPendingDelete") ||
528
+ this.isLocalPending(op.entryId, "isLocalPendingMove")
529
+ ) {
530
+ this.updateDeadEntry(op.entryId, newElementEntryId);
531
+ } else {
532
+ // move the element
533
+ const liveEntry = this.getLiveEntry(op.entryId);
534
+ const isDeleted = liveEntry.isDeleted;
535
+ this.updateLiveEntry(liveEntry.entryId, newElementEntryId);
536
+ // mark newly added element as deleted if existing live element was already deleted
537
+ if (isDeleted) {
538
+ newElement.isDeleted = isDeleted;
539
+ }
540
+ }
541
+ }
542
+ break;
543
+ }
544
+
545
+ case OperationType.toggle: {
546
+ if (local) {
547
+ // decrement the local pending delete op as its already applied to local state
548
+ if (opEntry.isLocalPendingDelete) {
549
+ opEntry.isLocalPendingDelete -= 1;
550
+ }
551
+ } else {
552
+ if (!this.isLocalPending(op.entryId, "isLocalPendingDelete")) {
553
+ this.getLiveEntry(op.entryId).isDeleted = op.isDeleted;
554
+ }
555
+ }
556
+ break;
557
+ }
558
+
559
+ case OperationType.toggleMove: {
560
+ if (local) {
561
+ // decrement the local pending move op as its already applied to local state
562
+ if (opEntry.isLocalPendingMove) {
563
+ opEntry.isLocalPendingMove -= 1;
564
+ }
565
+ } else if (
566
+ !this.isLocalPending(op.entryId, "isLocalPendingDelete") &&
567
+ !this.isLocalPending(op.entryId, "isLocalPendingMove")
568
+ ) {
569
+ this.updateLiveEntry(this.getLiveEntry(op.entryId).entryId, op.entryId);
570
+ }
571
+ break;
572
+ }
573
+
574
+ default: {
575
+ throw new Error("Unknown operation");
576
+ }
577
+ }
578
+ if (!local) {
579
+ this.emitValueChangedEvent(op, local);
580
+ }
581
+ }
582
+ }
583
+
584
+ private findInternalIndex(countEntries: number): number {
585
+ if (countEntries < 0) {
586
+ throw new Error("Input count is zero");
587
+ }
588
+
589
+ let countDown = countEntries;
590
+ let entriesIterator = 0;
591
+ for (; entriesIterator < this.sharedArray.length; entriesIterator = entriesIterator + 1) {
592
+ const entry = this.sharedArray[entriesIterator];
593
+ assert(entry !== undefined, 0xb93 /* Invalid index */);
594
+ if (entry.isDeleted === false) {
595
+ if (countDown === 0) {
596
+ return entriesIterator;
597
+ }
598
+ countDown = countDown - 1;
599
+ }
600
+ }
601
+ throw new Error(`Count of live entries is less than required`);
602
+ }
603
+
604
+ private findInternalInsertionIndex(index: number): number {
605
+ return index === 0 ? index : this.findInternalIndex(index - 1) + 1;
606
+ }
607
+
608
+ private findInternalDeletionIndex(index: number): number {
609
+ return this.findInternalIndex(index);
610
+ }
611
+
612
+ private createAddEntry<TWrite>(index: number, value: Serializable<TWrite> & T): string {
613
+ const newEntry = this.createNewEntry(uuid(), value);
614
+ this.addEntry(index, newEntry);
615
+ return newEntry.entryId;
616
+ }
617
+
618
+ private addEntry(insertIndex: number, newEntry: SharedArrayEntry<T>): void {
619
+ // in scenario where we populate 100K rows, we insert them all at the end of array.
620
+ // slicing array is way slower than pushing elements.
621
+ if (insertIndex === this.sharedArray.length) {
622
+ this.sharedArray.push(newEntry);
623
+ } else {
624
+ this.sharedArray.splice(insertIndex, 0 /* deleteCount */, newEntry);
625
+ }
626
+
627
+ // Updating the idToEntryMap optimizer data set as new entry has been added
628
+ this.idToEntryMap.set(newEntry.entryId, newEntry);
629
+ }
630
+
631
+ private emitValueChangedEvent(op: ISharedArrayOperation, isLocal: boolean): void {
632
+ this.emit("valueChanged", op, isLocal, this);
633
+ }
634
+
635
+ private emitRevertibleEvent(op: ISharedArrayOperation): void {
636
+ const revertible = new SharedArrayRevertible(this, op);
637
+ this.emit("revertible", revertible);
638
+ }
639
+
640
+ private deleteCore(index: number): void {
641
+ const entry = this.sharedArray[index];
642
+ assert(entry !== undefined, 0xb94 /* Invalid index */);
643
+
644
+ if (entry.isDeleted) {
645
+ throw new Error("Entry already deleted.");
646
+ }
647
+ entry.isDeleted = true;
648
+
649
+ // Adding local pending counter
650
+ entry.isLocalPendingDelete += 1;
651
+ }
652
+
653
+ private createMoveEntry(oldIndex: number, newIndex: number): string {
654
+ const oldEntry = this.sharedArray[oldIndex];
655
+ assert(oldEntry !== undefined, 0xb95 /* Invalid index */);
656
+ const newEntry = this.createNewEntry<SerializableTypeForSharedArray>(
657
+ uuid(),
658
+ oldEntry.value,
659
+ oldEntry.entryId,
660
+ );
661
+
662
+ oldEntry.isDeleted = true;
663
+ oldEntry.nextEntryId = newEntry.entryId;
664
+
665
+ // Adding local pending counter
666
+ oldEntry.isLocalPendingMove += 1;
667
+
668
+ this.addEntry(newIndex /* insertIndex */, newEntry);
669
+
670
+ return newEntry.entryId;
671
+ }
672
+
673
+ /**
674
+ * Creates new entry of type SharedArrayEntry interface.
675
+ * @param entryId - id for which new entry is created
676
+ * @param value - value for the new entry
677
+ * @param prevEntryId - prevEntryId if exists to update the previous pointer of double ended linked list
678
+ */
679
+ private createNewEntry<TWrite>(
680
+ entryId: string,
681
+ value: Serializable<TWrite> & T,
682
+ prevEntryId?: string,
683
+ ): SharedArrayEntry<T> {
684
+ return {
685
+ entryId,
686
+ value,
687
+ isAckPending: true,
688
+ isDeleted: false,
689
+ prevEntryId,
690
+ nextEntryId: undefined,
691
+ isLocalPendingDelete: 0,
692
+ isLocalPendingMove: 0,
693
+ };
694
+ }
695
+
696
+ /**
697
+ * Unsets all local flags used by the DDS. This method can be used after reading from snapshott to ensure
698
+ * local flags are initialized for use by the DDS.
699
+ * @param entry - Entry for which the local flags have to be cleaned up
700
+ */
701
+ private unsetLocalFlags(entry: SharedArrayEntry<T>): void {
702
+ entry.isAckPending = false;
703
+ entry.isLocalPendingDelete = 0;
704
+ entry.isLocalPendingMove = 0;
705
+ }
706
+
707
+ /**
708
+ * Returns the index of the first entry starting with startIndex that does not have the isAckPending flag
709
+ */
710
+ private getInternalInsertIndexByIgnoringLocalPendingInserts(startIndex: number): number {
711
+ let localOpsIterator = startIndex;
712
+ for (
713
+ ;
714
+ localOpsIterator < this.sharedArray.length;
715
+ localOpsIterator = localOpsIterator + 1
716
+ ) {
717
+ const entry = this.sharedArray[localOpsIterator];
718
+ assert(entry !== undefined, 0xb96 /* Invalid index */);
719
+ if (!entry.isAckPending) {
720
+ break;
721
+ }
722
+ }
723
+ return localOpsIterator;
724
+ }
725
+
726
+ private handleInsertOp<TWrite>(
727
+ entryId: string,
728
+ insertAfterEntryId: string | undefined,
729
+ local: boolean,
730
+ value: Serializable<TWrite> & T,
731
+ ): void {
732
+ let index = 0;
733
+ if (local) {
734
+ this.getEntryForId(entryId).isAckPending = false;
735
+ } else {
736
+ if (insertAfterEntryId !== undefined) {
737
+ index = this.findIndexOfEntryId(insertAfterEntryId) + 1;
738
+ }
739
+ const newEntry = this.createNewEntry(entryId, value);
740
+ newEntry.isAckPending = false;
741
+ this.addEntry(this.getInternalInsertIndexByIgnoringLocalPendingInserts(index), newEntry);
742
+ }
743
+ }
744
+
745
+ private findIndexOfEntryId(entryId: string | undefined): number {
746
+ for (let index = 0; index < this.sharedArray.length; index = index + 1) {
747
+ if (this.sharedArray[index]?.entryId === entryId) {
748
+ return index;
749
+ }
750
+ }
751
+ return -1;
752
+ }
753
+
754
+ private prepareToMakeEntryIdLive(entry: SharedArrayEntry<T>): void {
755
+ const prevIndex = this.findIndexOfEntryId(entry.prevEntryId);
756
+ const nextIndex = this.findIndexOfEntryId(entry.nextEntryId);
757
+ if (prevIndex !== -1) {
758
+ const prevEntry = this.sharedArray[prevIndex];
759
+ assert(prevEntry !== undefined, 0xb97 /* Invalid index */);
760
+ prevEntry.nextEntryId = entry.nextEntryId;
761
+ }
762
+ if (nextIndex !== -1) {
763
+ const nextEntry = this.sharedArray[nextIndex];
764
+ assert(nextEntry !== undefined, 0xb98 /* Invalid index */);
765
+ nextEntry.prevEntryId = entry.prevEntryId;
766
+ }
767
+ entry.prevEntryId = undefined;
768
+ entry.nextEntryId = undefined;
769
+ }
770
+
771
+ /**
772
+ * Method that returns the live entry.
773
+ * The shared array internally can store a skip list of all related entries which got created
774
+ * due to move operations for the same payload/value. However, all elements except for one element
775
+ * can have isDeleted flag as false indicating this is the live entry for the value.
776
+ * Current implementation ensures that the last element in the skip list of entries is the liveEntry/
777
+ * last live entry
778
+ *
779
+ * @param entryId - Entry id of any node in the skip list for the same payload/value
780
+ */
781
+ private getLiveEntry(entryId: string): SharedArrayEntry<T> {
782
+ let liveEntry = this.getEntryForId(entryId);
783
+ while (liveEntry.nextEntryId !== undefined && liveEntry.nextEntryId) {
784
+ liveEntry = this.getEntryForId(liveEntry.nextEntryId);
785
+ }
786
+ return liveEntry;
787
+ }
788
+
789
+ /**
790
+ * We track sequence of moves for a entry in the shared array using doubly linked skip list.
791
+ * This utility function helps us keep track of the current position of an entry.value by marking the entry
792
+ * at previous position deleted and appending the entry at the new position at the end of the double linked
793
+ * list for that entry.value.
794
+ */
795
+ private updateLiveEntry(oldLiveEntryEntryId: string, newLiveEntryEntryId: string): void {
796
+ const oldLiveEntry = this.getEntryForId(oldLiveEntryEntryId);
797
+ const newLiveEntry = this.getEntryForId(newLiveEntryEntryId);
798
+ if (oldLiveEntryEntryId === newLiveEntryEntryId) {
799
+ oldLiveEntry.isDeleted = false;
800
+ } else {
801
+ this.prepareToMakeEntryIdLive(newLiveEntry);
802
+ // Make entryId live
803
+ oldLiveEntry.nextEntryId = newLiveEntryEntryId;
804
+ newLiveEntry.prevEntryId = oldLiveEntryEntryId;
805
+ newLiveEntry.isDeleted = false;
806
+ oldLiveEntry.isDeleted = true;
807
+ }
808
+ }
809
+
810
+ /**
811
+ * We track sequence of moves for a entry in the shared array using doubly linked skip list.
812
+ * This utility function helps to insert the new entry as dead entry and reconnecting the double linked list with
813
+ * existingEntry -\> deadeEntry(appended) -\> existing chain(if any).
814
+ */
815
+ private updateDeadEntry(existingEntryId: string, deadEntryId: string): void {
816
+ const existingEntry = this.getEntryForId(existingEntryId);
817
+ const deadEntry = this.getEntryForId(deadEntryId);
818
+
819
+ // update dead entry's next to existingEntry's next, if existingEntry's next entry exists.
820
+ // It can be undefined if the exiting element is the last element (or only element) of chain.
821
+ if (existingEntry.nextEntryId !== undefined) {
822
+ deadEntry.nextEntryId = existingEntry.nextEntryId;
823
+ this.getEntryForId(existingEntry.nextEntryId).prevEntryId = deadEntryId;
824
+ }
825
+
826
+ // update current entry's next pointer to dead entry and vice versa.
827
+ existingEntry.nextEntryId = deadEntryId;
828
+ deadEntry.prevEntryId = existingEntryId;
829
+ deadEntry.isDeleted = true;
830
+ }
831
+
832
+ protected applyStashedOp(_content: unknown): void {
833
+ throw new Error("Not implemented");
834
+ }
835
+ }