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

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