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