@fluidframework/cell 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.
package/src/cell.ts CHANGED
@@ -6,21 +6,21 @@
6
6
  import { assert } from "@fluidframework/common-utils";
7
7
  import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
8
8
  import {
9
- IChannelAttributes,
10
- IFluidDataStoreRuntime,
11
- IChannelStorageService,
12
- IChannelFactory,
13
- Serializable,
9
+ IChannelAttributes,
10
+ IFluidDataStoreRuntime,
11
+ IChannelStorageService,
12
+ IChannelFactory,
13
+ Serializable,
14
14
  } from "@fluidframework/datastore-definitions";
15
- import { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
15
+ import { AttributionKey, ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
16
16
  import { readAndParse } from "@fluidframework/driver-utils";
17
- import { createSingleBlobSummary, IFluidSerializer, SharedObject } from "@fluidframework/shared-object-base";
18
- import { CellFactory } from "./cellFactory";
19
17
  import {
20
- ISharedCell,
21
- ISharedCellEvents,
22
- ICellLocalOpMetadata,
23
- } from "./interfaces";
18
+ createSingleBlobSummary,
19
+ IFluidSerializer,
20
+ SharedObject,
21
+ } from "@fluidframework/shared-object-base";
22
+ import { CellFactory } from "./cellFactory";
23
+ import { ISharedCell, ISharedCellEvents, ICellLocalOpMetadata, ICellOptions } from "./interfaces";
24
24
 
25
25
  /**
26
26
  * Description of a cell delta operation
@@ -28,19 +28,24 @@ import {
28
28
  type ICellOperation = ISetCellOperation | IDeleteCellOperation;
29
29
 
30
30
  interface ISetCellOperation {
31
- type: "setCell";
32
- value: ICellValue;
31
+ type: "setCell";
32
+ value: ICellValue;
33
33
  }
34
34
 
35
35
  interface IDeleteCellOperation {
36
- type: "deleteCell";
36
+ type: "deleteCell";
37
37
  }
38
38
 
39
39
  interface ICellValue {
40
- /**
41
- * The actual value contained in the `Cell`, which needs to be wrapped to handle `undefined`.
42
- */
43
- value: unknown;
40
+ /**
41
+ * The actual value contained in the `Cell`, which needs to be wrapped to handle `undefined`.
42
+ */
43
+ value: unknown;
44
+ /**
45
+ * The attribution key contained in the `Cell`.
46
+ * @alpha
47
+ */
48
+ attribution?: AttributionKey;
44
49
  }
45
50
 
46
51
  const snapshotFileName = "header";
@@ -50,269 +55,318 @@ const snapshotFileName = "header";
50
55
  */
51
56
  // TODO: use `unknown` instead (breaking change).
52
57
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
- export class SharedCell<T = any> extends SharedObject<ISharedCellEvents<T>> implements ISharedCell<T> {
54
- /**
55
- * Create a new `SharedCell`.
56
- *
57
- * @param runtime - The data store runtime to which the `SharedCell` belongs.
58
- * @param id - Unique identifier for the `SharedCell`.
59
- *
60
- * @returns The newly create `SharedCell`. Note that it will not yet be attached.
61
- */
62
- public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedCell {
63
- return runtime.createChannel(id, CellFactory.Type) as SharedCell;
64
- }
65
-
66
- /**
67
- * Gets the factory for the `SharedCell` to register with the data store.
68
- *
69
- * @returns A factory that creates and loads `SharedCell`s.
70
- */
71
- public static getFactory(): IChannelFactory {
72
- return new CellFactory();
73
- }
74
-
75
- /**
76
- * The data held by this cell.
77
- */
78
- private data: Serializable<T> | undefined;
79
-
80
- /**
81
- * This is used to assign a unique id to outgoing messages. It is used to track messages until
82
- * they are ack'd.
83
- */
84
- private messageId: number = -1;
85
-
86
- /**
87
- * This keeps track of the messageId of messages that have been ack'd. It is updated every time
88
- * we a message is ack'd with it's messageId.
89
- */
90
- private messageIdObserved: number = -1;
91
-
92
- private readonly pendingMessageIds: number[] = [];
93
-
94
- /**
95
- * Constructs a new `SharedCell`.
96
- * If the object is non-local an id and service interfaces will be provided.
97
- *
98
- * @param runtime - The data store runtime to which the `SharedCell` belongs.
99
- * @param id - Unique identifier for the `SharedCell`.
100
- */
101
- public constructor(id: string, runtime: IFluidDataStoreRuntime, attributes: IChannelAttributes) {
102
- super(id, runtime, attributes, "fluid_cell_");
103
- }
104
-
105
- /**
106
- * {@inheritDoc ISharedCell.get}
107
- */
108
- public get(): Serializable<T> | undefined {
109
- return this.data;
110
- }
111
-
112
- /**
113
- * {@inheritDoc ISharedCell.set}
114
- */
115
- public set(value: Serializable<T>): void {
116
- // Serialize the value if required.
117
- const operationValue: ICellValue = {
118
- value: this.serializer.encode(value, this.handle),
119
- };
120
-
121
- // Set the value locally.
122
- const previousValue = this.setCore(value);
123
-
124
- // If we are not attached, don't submit the op.
125
- if (!this.isAttached()) {
126
- return;
127
- }
128
-
129
- const op: ISetCellOperation = {
130
- type: "setCell",
131
- value: operationValue,
132
- };
133
- this.submitCellMessage(op, previousValue);
134
- }
135
-
136
- /**
137
- * {@inheritDoc ISharedCell.delete}
138
- */
139
- public delete(): void {
140
- // Delete the value locally.
141
- const previousValue = this.deleteCore();
142
-
143
- // If we are not attached, don't submit the op.
144
- if (!this.isAttached()) {
145
- return;
146
- }
147
-
148
- const op: IDeleteCellOperation = {
149
- type: "deleteCell",
150
- };
151
- this.submitCellMessage(op, previousValue);
152
- }
153
-
154
- /**
155
- * {@inheritDoc ISharedCell.empty}
156
- */
157
- public empty(): boolean {
158
- return this.data === undefined;
159
- }
160
-
161
- /**
162
- * Creates a summary for the Cell.
163
- *
164
- * @returns The summary of the current state of the Cell.
165
- */
166
- protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
167
- const content: ICellValue = { value: this.data };
168
- return createSingleBlobSummary(snapshotFileName, serializer.stringify(content, this.handle));
169
- }
170
-
171
- /**
172
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
173
- */
174
- protected async loadCore(storage: IChannelStorageService): Promise<void> {
175
- const content = await readAndParse<ICellValue>(storage, snapshotFileName);
176
-
177
- this.data = this.decode(content);
178
- }
179
-
180
- /**
181
- * Initialize a local instance of cell.
182
- */
183
- protected initializeLocalCore(): void {
184
- this.data = undefined;
185
- }
186
-
187
- /**
188
- * Call back on disconnect.
189
- */
190
- protected onDisconnect(): void { }
191
-
192
- /**
193
- * Apply inner op.
194
- *
195
- * @param content - ICellOperation content
196
- */
197
- private applyInnerOp(content: ICellOperation): Serializable<T> | undefined {
198
- switch (content.type) {
199
- case "setCell":
200
- return this.setCore(this.decode(content.value));
201
-
202
- case "deleteCell":
203
- return this.deleteCore();
204
-
205
- default:
206
- throw new Error("Unknown operation");
207
- }
208
- }
209
-
210
- /**
211
- * Process a cell operation (op).
212
- *
213
- * @param message - The message to prepare.
214
- * @param local - Whether or not the message was sent by the local client.
215
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
216
- * For messages from a remote client, this will be `undefined`.
217
- */
218
- protected processCore(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown): void {
219
- const cellOpMetadata = localOpMetadata as ICellLocalOpMetadata;
220
- if (this.messageId !== this.messageIdObserved) {
221
- // We are waiting for an ACK on our change to this cell - we will ignore all messages until we get it.
222
- if (local) {
223
- const messageIdReceived = cellOpMetadata.pendingMessageId;
224
- assert(messageIdReceived !== undefined && messageIdReceived <= this.messageId,
225
- 0x00c /* "messageId is incorrect from from the local client's ACK" */);
226
- assert(this.pendingMessageIds !== undefined &&
227
- this.pendingMessageIds[0] === cellOpMetadata.pendingMessageId,
228
- 0x471 /* Unexpected pending message received */);
229
- this.pendingMessageIds.shift();
230
- // We got an ACK. Update messageIdObserved.
231
- this.messageIdObserved = cellOpMetadata.pendingMessageId;
232
- }
233
- return;
234
- }
235
-
236
- if (message.type === MessageType.Operation && !local) {
237
- const op = message.contents as ICellOperation;
238
- this.applyInnerOp(op);
239
- }
240
- }
241
-
242
- private setCore(value: Serializable<T>): Serializable<T> | undefined {
243
- const previousLocalValue = this.get();
244
- this.data = value;
245
- this.emit("valueChanged", value);
246
- return previousLocalValue;
247
- }
248
-
249
- private deleteCore(): Serializable<T> | undefined {
250
- const previousLocalValue = this.get();
251
- this.data = undefined;
252
- this.emit("delete");
253
- return previousLocalValue;
254
- }
255
-
256
- private decode(cellValue: ICellValue): Serializable<T> {
257
- const value = cellValue.value;
258
- return this.serializer.decode(value) as Serializable<T> ;
259
- }
260
-
261
- private createLocalOpMetadata(op: ICellOperation, previousValue?: Serializable<T>): ICellLocalOpMetadata {
262
- const pendingMessageId = ++this.messageId;
263
- this.pendingMessageIds.push(pendingMessageId);
264
- const localMetadata: ICellLocalOpMetadata = {
265
- pendingMessageId, previousValue,
266
- };
267
- return localMetadata;
268
- }
269
-
270
- /**
271
- * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
272
- *
273
- * @internal
274
- */
275
- protected applyStashedOp(content: unknown): unknown {
276
- const cellContent = content as ICellOperation;
277
- const previousValue = this.applyInnerOp(cellContent);
278
- return this.createLocalOpMetadata(cellContent, previousValue);
279
- }
280
-
281
- /**
282
- * Rollback a local op.
283
- *
284
- * @param content - The operation to rollback.
285
- * @param localOpMetadata - The local metadata associated with the op.
286
- */
287
- // TODO: use `unknown` instead (breaking change).
288
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
289
- protected rollback(content: any, localOpMetadata: unknown): void {
290
- const cellOpMetadata = localOpMetadata as ICellLocalOpMetadata;
291
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
292
- if (content.type === "setCell" || content.type === "deleteCell") {
293
- if (cellOpMetadata.previousValue === undefined) {
294
- this.deleteCore();
295
- } else {
296
- this.setCore(cellOpMetadata.previousValue as Serializable<T>);
297
- }
298
-
299
- const lastPendingMessageId = this.pendingMessageIds.pop();
300
- if (lastPendingMessageId !== cellOpMetadata.pendingMessageId) {
301
- throw new Error("Rollback op does not match last pending");
302
- }
303
- } else {
304
- throw new Error("Unsupported op for rollback");
305
- }
306
- }
307
-
308
- /**
309
- * Submit a cell message to remote clients.
310
- *
311
- * @param op - The cell message.
312
- * @param previousValue - The value of the cell before this op.
313
- */
314
- private submitCellMessage(op: ICellOperation, previousValue?: Serializable<T>): void {
315
- const localMetadata = this.createLocalOpMetadata(op, previousValue);
316
- this.submitLocalMessage(op, localMetadata);
317
- }
58
+ export class SharedCell<T = any>
59
+ extends SharedObject<ISharedCellEvents<T>>
60
+ implements ISharedCell<T>
61
+ {
62
+ /**
63
+ * Create a new `SharedCell`.
64
+ *
65
+ * @param runtime - The data store runtime to which the `SharedCell` belongs.
66
+ * @param id - Unique identifier for the `SharedCell`.
67
+ *
68
+ * @returns The newly create `SharedCell`. Note that it will not yet be attached.
69
+ */
70
+ public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedCell {
71
+ return runtime.createChannel(id, CellFactory.Type) as SharedCell;
72
+ }
73
+
74
+ /**
75
+ * Gets the factory for the `SharedCell` to register with the data store.
76
+ *
77
+ * @returns A factory that creates and loads `SharedCell`s.
78
+ */
79
+ public static getFactory(): IChannelFactory {
80
+ return new CellFactory();
81
+ }
82
+
83
+ /**
84
+ * The data held by this cell.
85
+ */
86
+ private data: Serializable<T> | undefined;
87
+
88
+ /**
89
+ * This is used to assign a unique id to outgoing messages. It is used to track messages until
90
+ * they are ack'd.
91
+ */
92
+ private messageId: number = -1;
93
+
94
+ /**
95
+ * This keeps track of the messageId of messages that have been ack'd. It is updated every time
96
+ * we a message is ack'd with it's messageId.
97
+ */
98
+ private messageIdObserved: number = -1;
99
+
100
+ private readonly pendingMessageIds: number[] = [];
101
+
102
+ private attribution: AttributionKey | undefined;
103
+
104
+ private readonly options: ICellOptions | undefined;
105
+
106
+ /**
107
+ * Constructs a new `SharedCell`.
108
+ * If the object is non-local an id and service interfaces will be provided.
109
+ *
110
+ * @alpha
111
+ *
112
+ * @param runtime - The data store runtime to which the `SharedCell` belongs.
113
+ * @param id - Unique identifier for the `SharedCell`.
114
+ */
115
+ // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
116
+ constructor(id: string, runtime: IFluidDataStoreRuntime, attributes: IChannelAttributes) {
117
+ super(id, runtime, attributes, "fluid_cell_");
118
+
119
+ this.options = runtime.options as ICellOptions;
120
+ }
121
+
122
+ /**
123
+ * {@inheritDoc ISharedCell.get}
124
+ */
125
+ public get(): Serializable<T> | undefined {
126
+ return this.data;
127
+ }
128
+
129
+ /**
130
+ * {@inheritDoc ISharedCell.set}
131
+ */
132
+ public set(value: Serializable<T>): void {
133
+ // Serialize the value if required.
134
+ const operationValue: ICellValue = {
135
+ value: this.serializer.encode(value, this.handle),
136
+ };
137
+
138
+ // Set the value locally.
139
+ const previousValue = this.setCore(value);
140
+
141
+ // If we are not attached, don't submit the op.
142
+ if (!this.isAttached()) {
143
+ return;
144
+ }
145
+
146
+ const op: ISetCellOperation = {
147
+ type: "setCell",
148
+ value: operationValue,
149
+ };
150
+ this.submitCellMessage(op, previousValue);
151
+ }
152
+
153
+ /**
154
+ * {@inheritDoc ISharedCell.delete}
155
+ */
156
+ public delete(): void {
157
+ // Delete the value locally.
158
+ const previousValue = this.deleteCore();
159
+
160
+ // If we are not attached, don't submit the op.
161
+ if (!this.isAttached()) {
162
+ return;
163
+ }
164
+
165
+ const op: IDeleteCellOperation = {
166
+ type: "deleteCell",
167
+ };
168
+ this.submitCellMessage(op, previousValue);
169
+ }
170
+
171
+ /**
172
+ * {@inheritDoc ISharedCell.empty}
173
+ */
174
+ public empty(): boolean {
175
+ return this.data === undefined;
176
+ }
177
+
178
+ /**
179
+ * {@inheritDoc ISharedCell.getAttribution}
180
+ * @alpha
181
+ */
182
+ public getAttribution(): AttributionKey | undefined {
183
+ return this.attribution;
184
+ }
185
+
186
+ /**
187
+ * Set the attribution through the SequencedDocumentMessage
188
+ */
189
+ private setAttribution(message: ISequencedDocumentMessage): void {
190
+ if (this.options?.attribution?.track ?? false) {
191
+ this.attribution = { type: "op", seq: message.sequenceNumber };
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Creates a summary for the Cell.
197
+ *
198
+ * @returns The summary of the current state of the Cell.
199
+ */
200
+ protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
201
+ const content: ICellValue = { value: this.data, attribution: this.attribution };
202
+ return createSingleBlobSummary(
203
+ snapshotFileName,
204
+ serializer.stringify(content, this.handle),
205
+ );
206
+ }
207
+
208
+ /**
209
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
210
+ */
211
+ protected async loadCore(storage: IChannelStorageService): Promise<void> {
212
+ const content = await readAndParse<ICellValue>(storage, snapshotFileName);
213
+
214
+ this.data = this.decode(content);
215
+ this.attribution = content.attribution;
216
+ }
217
+
218
+ /**
219
+ * Initialize a local instance of cell.
220
+ */
221
+ protected initializeLocalCore(): void {
222
+ this.data = undefined;
223
+ }
224
+
225
+ /**
226
+ * Call back on disconnect.
227
+ */
228
+ protected onDisconnect(): void {}
229
+
230
+ /**
231
+ * Apply inner op.
232
+ *
233
+ * @param content - ICellOperation content
234
+ */
235
+ private applyInnerOp(content: ICellOperation): Serializable<T> | undefined {
236
+ switch (content.type) {
237
+ case "setCell":
238
+ return this.setCore(this.decode(content.value));
239
+
240
+ case "deleteCell":
241
+ return this.deleteCore();
242
+
243
+ default:
244
+ throw new Error("Unknown operation");
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Process a cell operation (op).
250
+ *
251
+ * @param message - The message to prepare.
252
+ * @param local - Whether or not the message was sent by the local client.
253
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
254
+ * For messages from a remote client, this will be `undefined`.
255
+ */
256
+ protected processCore(
257
+ message: ISequencedDocumentMessage,
258
+ local: boolean,
259
+ localOpMetadata: unknown,
260
+ ): void {
261
+ const cellOpMetadata = localOpMetadata as ICellLocalOpMetadata;
262
+ if (this.messageId !== this.messageIdObserved) {
263
+ // We are waiting for an ACK on our change to this cell - we will ignore all messages until we get it.
264
+ if (local) {
265
+ const messageIdReceived = cellOpMetadata.pendingMessageId;
266
+ assert(
267
+ messageIdReceived !== undefined && messageIdReceived <= this.messageId,
268
+ 0x00c /* "messageId is incorrect from from the local client's ACK" */,
269
+ );
270
+ assert(
271
+ this.pendingMessageIds !== undefined &&
272
+ this.pendingMessageIds[0] === cellOpMetadata.pendingMessageId,
273
+ 0x471 /* Unexpected pending message received */,
274
+ );
275
+ this.pendingMessageIds.shift();
276
+ // We got an ACK. Update messageIdObserved.
277
+ this.messageIdObserved = cellOpMetadata.pendingMessageId;
278
+ // update the attributor
279
+ this.setAttribution(message);
280
+ }
281
+ return;
282
+ }
283
+
284
+ if (message.type === MessageType.Operation && !local) {
285
+ const op = message.contents as ICellOperation;
286
+ // update the attributor
287
+ this.setAttribution(message);
288
+ this.applyInnerOp(op);
289
+ }
290
+ }
291
+
292
+ private setCore(value: Serializable<T>): Serializable<T> | undefined {
293
+ const previousLocalValue = this.get();
294
+ this.data = value;
295
+ this.emit("valueChanged", value);
296
+ return previousLocalValue;
297
+ }
298
+
299
+ private deleteCore(): Serializable<T> | undefined {
300
+ const previousLocalValue = this.get();
301
+ this.data = undefined;
302
+ this.emit("delete");
303
+ return previousLocalValue;
304
+ }
305
+
306
+ private decode(cellValue: ICellValue): Serializable<T> {
307
+ const value = cellValue.value;
308
+ return this.serializer.decode(value) as Serializable<T>;
309
+ }
310
+
311
+ private createLocalOpMetadata(
312
+ op: ICellOperation,
313
+ previousValue?: Serializable<T>,
314
+ ): ICellLocalOpMetadata {
315
+ const pendingMessageId = ++this.messageId;
316
+ this.pendingMessageIds.push(pendingMessageId);
317
+ const localMetadata: ICellLocalOpMetadata = {
318
+ pendingMessageId,
319
+ previousValue,
320
+ };
321
+ return localMetadata;
322
+ }
323
+
324
+ /**
325
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
326
+ *
327
+ * @internal
328
+ */
329
+ protected applyStashedOp(content: unknown): unknown {
330
+ const cellContent = content as ICellOperation;
331
+ const previousValue = this.applyInnerOp(cellContent);
332
+ return this.createLocalOpMetadata(cellContent, previousValue);
333
+ }
334
+
335
+ /**
336
+ * Rollback a local op.
337
+ *
338
+ * @param content - The operation to rollback.
339
+ * @param localOpMetadata - The local metadata associated with the op.
340
+ */
341
+ // TODO: use `unknown` instead (breaking change).
342
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
343
+ protected rollback(content: any, localOpMetadata: unknown): void {
344
+ const cellOpMetadata = localOpMetadata as ICellLocalOpMetadata;
345
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
346
+ if (content.type === "setCell" || content.type === "deleteCell") {
347
+ if (cellOpMetadata.previousValue === undefined) {
348
+ this.deleteCore();
349
+ } else {
350
+ this.setCore(cellOpMetadata.previousValue as Serializable<T>);
351
+ }
352
+
353
+ const lastPendingMessageId = this.pendingMessageIds.pop();
354
+ if (lastPendingMessageId !== cellOpMetadata.pendingMessageId) {
355
+ throw new Error("Rollback op does not match last pending");
356
+ }
357
+ } else {
358
+ throw new Error("Unsupported op for rollback");
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Submit a cell message to remote clients.
364
+ *
365
+ * @param op - The cell message.
366
+ * @param previousValue - The value of the cell before this op.
367
+ */
368
+ private submitCellMessage(op: ICellOperation, previousValue?: Serializable<T>): void {
369
+ const localMetadata = this.createLocalOpMetadata(op, previousValue);
370
+ this.submitLocalMessage(op, localMetadata);
371
+ }
318
372
  }