@fluidframework/counter 2.70.0 → 2.71.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @fluidframework/counter
2
2
 
3
+ ## 2.71.0
4
+
5
+ Dependency updates only.
6
+
3
7
  ## 2.70.0
4
8
 
5
9
  Dependency updates only.
package/dist/counter.d.ts CHANGED
@@ -7,6 +7,13 @@ import type { ISummaryTreeWithStats, IRuntimeMessageCollection } from "@fluidfra
7
7
  import type { IFluidSerializer } from "@fluidframework/shared-object-base/internal";
8
8
  import { SharedObject } from "@fluidframework/shared-object-base/internal";
9
9
  import type { ISharedCounter, ISharedCounterEvents } from "./interfaces.js";
10
+ /**
11
+ * Describes the operation (op) format for incrementing the {@link SharedCounter}.
12
+ */
13
+ export interface IIncrementOperation {
14
+ type: "increment";
15
+ incrementAmount: number;
16
+ }
10
17
  /**
11
18
  * {@inheritDoc ISharedCounter}
12
19
  * @legacy @beta
@@ -14,6 +21,14 @@ import type { ISharedCounter, ISharedCounterEvents } from "./interfaces.js";
14
21
  export declare class SharedCounter extends SharedObject<ISharedCounterEvents> implements ISharedCounter {
15
22
  constructor(id: string, runtime: IFluidDataStoreRuntime, attributes: IChannelAttributes);
16
23
  private _value;
24
+ /**
25
+ * Tracks pending local ops that have not been ack'd yet.
26
+ */
27
+ private readonly pendingOps;
28
+ /**
29
+ * The next message id to be used when submitting an op.
30
+ */
31
+ private nextPendingMessageId;
17
32
  /**
18
33
  * {@inheritDoc ISharedCounter.value}
19
34
  */
@@ -46,5 +61,10 @@ export declare class SharedCounter extends SharedObject<ISharedCounterEvents> im
46
61
  * {@inheritdoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
47
62
  */
48
63
  protected applyStashedOp(op: unknown): void;
64
+ /**
65
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
66
+ * @sealed
67
+ */
68
+ protected rollback(content: unknown, localOpMetadata: unknown): void;
49
69
  }
50
70
  //# sourceMappingURL=counter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,MAAM,gDAAgD,CAAC;AAGxD,OAAO,KAAK,EACX,qBAAqB,EACrB,yBAAyB,EAGzB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,EACN,YAAY,EAEZ,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAsB5E;;;GAGG;AACH,qBAAa,aACZ,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,cAAc;gBAGxB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,sBAAsB,EAC/B,UAAU,EAAE,kBAAkB;IAK/B,OAAO,CAAC,MAAM,CAAa;IAE3B;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED;;OAEG;IACI,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAgB/C,OAAO,CAAC,aAAa;IAKrB;;;;OAIG;IACH,SAAS,CAAC,aAAa,CAAC,UAAU,EAAE,gBAAgB,GAAG,qBAAqB;IAU5E;;OAEG;cACa,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxE;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAE9B;;OAEG;IACH,SAAS,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,yBAAyB,GAAG,IAAI;IAOlF,OAAO,CAAC,cAAc;IAsBtB;;OAEG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;CAS3C"}
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,MAAM,gDAAgD,CAAC;AAGxD,OAAO,KAAK,EACX,qBAAqB,EACrB,yBAAyB,EAGzB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,EACN,YAAY,EAEZ,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACxB;AAwBD;;;GAGG;AACH,qBAAa,aACZ,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,cAAc;gBAGxB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,sBAAsB,EAC/B,UAAU,EAAE,kBAAkB;IAK/B,OAAO,CAAC,MAAM,CAAa;IAE3B;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IAEtD;;OAEG;IACH,OAAO,CAAC,oBAAoB,CAAa;IAEzC;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED;;OAEG;IACI,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAqB/C,OAAO,CAAC,aAAa;IAKrB;;;;OAIG;IACH,SAAS,CAAC,aAAa,CAAC,UAAU,EAAE,gBAAgB,GAAG,qBAAqB;IAU5E;;OAEG;cACa,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxE;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAE9B;;OAEG;IACH,SAAS,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,yBAAyB,GAAG,IAAI;IAOlF,OAAO,CAAC,cAAc;IAsCtB;;OAEG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAU3C;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,GAAG,IAAI;CAkBpE"}
package/dist/counter.js CHANGED
@@ -18,6 +18,14 @@ class SharedCounter extends internal_4.SharedObject {
18
18
  constructor(id, runtime, attributes) {
19
19
  super(id, runtime, attributes, "fluid_counter_");
20
20
  this._value = 0;
21
+ /**
22
+ * Tracks pending local ops that have not been ack'd yet.
23
+ */
24
+ this.pendingOps = [];
25
+ /**
26
+ * The next message id to be used when submitting an op.
27
+ */
28
+ this.nextPendingMessageId = 0;
21
29
  }
22
30
  /**
23
31
  * {@inheritDoc ISharedCounter.value}
@@ -38,8 +46,13 @@ class SharedCounter extends internal_4.SharedObject {
38
46
  type: "increment",
39
47
  incrementAmount,
40
48
  };
49
+ const messageId = this.nextPendingMessageId++;
41
50
  this.incrementCore(incrementAmount);
42
- this.submitLocalMessage(op);
51
+ // We don't need to send the op if we are not attached yet.
52
+ if (this.isAttached()) {
53
+ this.pendingOps.push({ ...op, messageId });
54
+ this.submitLocalMessage(op, messageId);
55
+ }
43
56
  }
44
57
  incrementCore(incrementAmount) {
45
58
  this._value += incrementAmount;
@@ -80,15 +93,29 @@ class SharedCounter extends internal_4.SharedObject {
80
93
  }
81
94
  processMessage(messageEnvelope, messageContent, local) {
82
95
  // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
83
- if (messageEnvelope.type === internal_2.MessageType.Operation && !local) {
96
+ if (messageEnvelope.type === internal_2.MessageType.Operation) {
84
97
  const op = messageContent.contents;
85
- switch (op.type) {
86
- case "increment": {
87
- this.incrementCore(op.incrementAmount);
88
- break;
89
- }
90
- default: {
91
- throw new Error("Unknown operation");
98
+ // If the message is local we have already optimistically processed
99
+ // and we should now remove it from this.pendingOps.
100
+ // If the message is from a remote client, we should process it.
101
+ if (local) {
102
+ const pendingOp = this.pendingOps.shift();
103
+ const messageId = messageContent.localOpMetadata;
104
+ (0, internal_1.assert)(typeof messageId === "number", 0xc8e /* localOpMetadata should be a number */);
105
+ (0, internal_1.assert)(pendingOp !== undefined &&
106
+ pendingOp.messageId === messageId &&
107
+ pendingOp.type === op.type &&
108
+ pendingOp.incrementAmount === op.incrementAmount, 0xc8f /* local op mismatch */);
109
+ }
110
+ else {
111
+ switch (op.type) {
112
+ case "increment": {
113
+ this.incrementCore(op.incrementAmount);
114
+ break;
115
+ }
116
+ default: {
117
+ throw new Error("Unknown operation");
118
+ }
92
119
  }
93
120
  }
94
121
  }
@@ -102,6 +129,30 @@ class SharedCounter extends internal_4.SharedObject {
102
129
  (0, internal_1.assert)(counterOp.type === "increment", 0x3ec /* Op type is not increment */);
103
130
  this.increment(counterOp.incrementAmount);
104
131
  }
132
+ /**
133
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
134
+ * @sealed
135
+ */
136
+ rollback(content, localOpMetadata) {
137
+ assertIsIncrementOp(content);
138
+ (0, internal_1.assert)(typeof localOpMetadata === "number", 0xc90 /* localOpMetadata should be a number */);
139
+ const pendingOp = this.pendingOps.pop();
140
+ (0, internal_1.assert)(pendingOp !== undefined &&
141
+ pendingOp.messageId === localOpMetadata &&
142
+ pendingOp.type === content.type &&
143
+ pendingOp.incrementAmount === content.incrementAmount, 0xc91 /* op to rollback mismatch with pending op */);
144
+ // To rollback the optimistic increment we can increment by the opposite amount.
145
+ // This will also emit another incremented event with the opposite amount.
146
+ this.incrementCore(-content.incrementAmount);
147
+ }
105
148
  }
106
149
  exports.SharedCounter = SharedCounter;
150
+ function assertIsIncrementOp(op) {
151
+ (0, internal_1.assert)(typeof op === "object" &&
152
+ op !== null &&
153
+ "type" in op &&
154
+ "incrementAmount" in op &&
155
+ op.type === "increment" &&
156
+ typeof op.incrementAmount === "number", 0xc92 /* invalid increment op format */);
157
+ }
107
158
  //# sourceMappingURL=counter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { assert } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIChannelAttributes,\n\tIFluidDataStoreRuntime,\n\tIChannelStorageService,\n} from \"@fluidframework/datastore-definitions/internal\";\nimport { MessageType } from \"@fluidframework/driver-definitions/internal\";\nimport { readAndParse } from \"@fluidframework/driver-utils/internal\";\nimport type {\n\tISummaryTreeWithStats,\n\tIRuntimeMessageCollection,\n\tIRuntimeMessagesContent,\n\tISequencedMessageEnvelope,\n} from \"@fluidframework/runtime-definitions/internal\";\nimport type { IFluidSerializer } from \"@fluidframework/shared-object-base/internal\";\nimport {\n\tSharedObject,\n\tcreateSingleBlobSummary,\n} from \"@fluidframework/shared-object-base/internal\";\n\nimport type { ISharedCounter, ISharedCounterEvents } from \"./interfaces.js\";\n\n/**\n * Describes the operation (op) format for incrementing the {@link SharedCounter}.\n */\ninterface IIncrementOperation {\n\ttype: \"increment\";\n\tincrementAmount: number;\n}\n\n/**\n * @remarks Used in snapshotting.\n */\ninterface ICounterSnapshotFormat {\n\t/**\n\t * The value of the counter.\n\t */\n\tvalue: number;\n}\n\nconst snapshotFileName = \"header\";\n\n/**\n * {@inheritDoc ISharedCounter}\n * @legacy @beta\n */\nexport class SharedCounter\n\textends SharedObject<ISharedCounterEvents>\n\timplements ISharedCounter\n{\n\tpublic constructor(\n\t\tid: string,\n\t\truntime: IFluidDataStoreRuntime,\n\t\tattributes: IChannelAttributes,\n\t) {\n\t\tsuper(id, runtime, attributes, \"fluid_counter_\");\n\t}\n\n\tprivate _value: number = 0;\n\n\t/**\n\t * {@inheritDoc ISharedCounter.value}\n\t */\n\tpublic get value(): number {\n\t\treturn this._value;\n\t}\n\n\t/**\n\t * {@inheritDoc ISharedCounter.increment}\n\t */\n\tpublic increment(incrementAmount: number): void {\n\t\t// Incrementing by floating point numbers will be eventually inconsistent, since the order in which the\n\t\t// increments are applied affects the result. A more-robust solution would be required to support this.\n\t\tif (incrementAmount % 1 !== 0) {\n\t\t\tthrow new Error(\"Must increment by a whole number\");\n\t\t}\n\n\t\tconst op: IIncrementOperation = {\n\t\t\ttype: \"increment\",\n\t\t\tincrementAmount,\n\t\t};\n\n\t\tthis.incrementCore(incrementAmount);\n\t\tthis.submitLocalMessage(op);\n\t}\n\n\tprivate incrementCore(incrementAmount: number): void {\n\t\tthis._value += incrementAmount;\n\t\tthis.emit(\"incremented\", incrementAmount, this._value);\n\t}\n\n\t/**\n\t * Create a summary for the counter.\n\t *\n\t * @returns The summary of the current state of the counter.\n\t */\n\tprotected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {\n\t\t// Get a serializable form of data\n\t\tconst content: ICounterSnapshotFormat = {\n\t\t\tvalue: this.value,\n\t\t};\n\n\t\t// And then construct the summary for it\n\t\treturn createSingleBlobSummary(snapshotFileName, JSON.stringify(content));\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}\n\t */\n\tprotected async loadCore(storage: IChannelStorageService): Promise<void> {\n\t\tconst content = await readAndParse<ICounterSnapshotFormat>(storage, snapshotFileName);\n\n\t\tthis._value = content.value;\n\t}\n\n\t/**\n\t * Called when the object has disconnected from the delta stream.\n\t */\n\tprotected onDisconnect(): void {}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processMessagesCore}\n\t */\n\tprotected processMessagesCore(messagesCollection: IRuntimeMessageCollection): void {\n\t\tconst { envelope, local, messagesContent } = messagesCollection;\n\t\tfor (const messageContent of messagesContent) {\n\t\t\tthis.processMessage(envelope, messageContent, local);\n\t\t}\n\t}\n\n\tprivate processMessage(\n\t\tmessageEnvelope: ISequencedMessageEnvelope,\n\t\tmessageContent: IRuntimeMessagesContent,\n\t\tlocal: boolean,\n\t): void {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison\n\t\tif (messageEnvelope.type === MessageType.Operation && !local) {\n\t\t\tconst op = messageContent.contents as IIncrementOperation;\n\n\t\t\tswitch (op.type) {\n\t\t\t\tcase \"increment\": {\n\t\t\t\t\tthis.incrementCore(op.incrementAmount);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tdefault: {\n\t\t\t\t\tthrow new Error(\"Unknown operation\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * {@inheritdoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}\n\t */\n\tprotected applyStashedOp(op: unknown): void {\n\t\tconst counterOp = op as IIncrementOperation;\n\n\t\t// TODO: Clean up error code linter violations repo-wide.\n\n\t\tassert(counterOp.type === \"increment\", 0x3ec /* Op type is not increment */);\n\n\t\tthis.increment(counterOp.incrementAmount);\n\t}\n}\n"]}
1
+ {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,kEAA6D;AAM7D,0EAA0E;AAC1E,oEAAqE;AAQrE,0EAGqD;AAgCrD,MAAM,gBAAgB,GAAG,QAAQ,CAAC;AAElC;;;GAGG;AACH,MAAa,aACZ,SAAQ,uBAAkC;IAG1C,YACC,EAAU,EACV,OAA+B,EAC/B,UAA8B;QAE9B,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC;QAG1C,WAAM,GAAW,CAAC,CAAC;QAE3B;;WAEG;QACc,eAAU,GAAwB,EAAE,CAAC;QAEtD;;WAEG;QACK,yBAAoB,GAAW,CAAC,CAAC;IAZzC,CAAC;IAcD;;OAEG;IACH,IAAW,KAAK;QACf,OAAO,IAAI,CAAC,MAAM,CAAC;IACpB,CAAC;IAED;;OAEG;IACI,SAAS,CAAC,eAAuB;QACvC,uGAAuG;QACvG,wGAAwG;QACxG,IAAI,eAAe,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACrD,CAAC;QAED,MAAM,EAAE,GAAwB;YAC/B,IAAI,EAAE,WAAW;YACjB,eAAe;SACf,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAE9C,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;QACpC,2DAA2D;QAC3D,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;YACvB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;YAC3C,IAAI,CAAC,kBAAkB,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IAEO,aAAa,CAAC,eAAuB;QAC5C,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACxD,CAAC;IAED;;;;OAIG;IACO,aAAa,CAAC,UAA4B;QACnD,kCAAkC;QAClC,MAAM,OAAO,GAA2B;YACvC,KAAK,EAAE,IAAI,CAAC,KAAK;SACjB,CAAC;QAEF,wCAAwC;QACxC,OAAO,IAAA,kCAAuB,EAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED;;OAEG;IACO,KAAK,CAAC,QAAQ,CAAC,OAA+B;QACvD,MAAM,OAAO,GAAG,MAAM,IAAA,uBAAY,EAAyB,OAAO,EAAE,gBAAgB,CAAC,CAAC;QAEtF,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAC7B,CAAC;IAED;;OAEG;IACO,YAAY,KAAU,CAAC;IAEjC;;OAEG;IACO,mBAAmB,CAAC,kBAA6C;QAC1E,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,kBAAkB,CAAC;QAChE,KAAK,MAAM,cAAc,IAAI,eAAe,EAAE,CAAC;YAC9C,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;IAEO,cAAc,CACrB,eAA0C,EAC1C,cAAuC,EACvC,KAAc;QAEd,wEAAwE;QACxE,IAAI,eAAe,CAAC,IAAI,KAAK,sBAAW,CAAC,SAAS,EAAE,CAAC;YACpD,MAAM,EAAE,GAAG,cAAc,CAAC,QAA+B,CAAC;YAE1D,mEAAmE;YACnE,oDAAoD;YACpD,gEAAgE;YAChE,IAAI,KAAK,EAAE,CAAC;gBACX,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;gBAC1C,MAAM,SAAS,GAAG,cAAc,CAAC,eAAe,CAAC;gBACjD,IAAA,iBAAM,EAAC,OAAO,SAAS,KAAK,QAAQ,EAAE,KAAK,CAAC,wCAAwC,CAAC,CAAC;gBACtF,IAAA,iBAAM,EACL,SAAS,KAAK,SAAS;oBACtB,SAAS,CAAC,SAAS,KAAK,SAAS;oBACjC,SAAS,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI;oBAC1B,SAAS,CAAC,eAAe,KAAK,EAAE,CAAC,eAAe,EACjD,KAAK,CAAC,uBAAuB,CAC7B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACP,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;oBACjB,KAAK,WAAW,CAAC,CAAC,CAAC;wBAClB,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC;wBACvC,MAAM;oBACP,CAAC;oBAED,OAAO,CAAC,CAAC,CAAC;wBACT,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;oBACtC,CAAC;gBACF,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAED;;OAEG;IACO,cAAc,CAAC,EAAW;QACnC,MAAM,SAAS,GAAG,EAAyB,CAAC;QAE5C,yDAAyD;QAEzD,IAAA,iBAAM,EAAC,SAAS,CAAC,IAAI,KAAK,WAAW,EAAE,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAE7E,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACO,QAAQ,CAAC,OAAgB,EAAE,eAAwB;QAC5D,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAA,iBAAM,EACL,OAAO,eAAe,KAAK,QAAQ,EACnC,KAAK,CAAC,wCAAwC,CAC9C,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC;QACxC,IAAA,iBAAM,EACL,SAAS,KAAK,SAAS;YACtB,SAAS,CAAC,SAAS,KAAK,eAAe;YACvC,SAAS,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI;YAC/B,SAAS,CAAC,eAAe,KAAK,OAAO,CAAC,eAAe,EACtD,KAAK,CAAC,6CAA6C,CACnD,CAAC;QACF,gFAAgF;QAChF,0EAA0E;QAC1E,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9C,CAAC;CACD;AA5KD,sCA4KC;AAED,SAAS,mBAAmB,CAAC,EAAW;IACvC,IAAA,iBAAM,EACL,OAAO,EAAE,KAAK,QAAQ;QACrB,EAAE,KAAK,IAAI;QACX,MAAM,IAAI,EAAE;QACZ,iBAAiB,IAAI,EAAE;QACvB,EAAE,CAAC,IAAI,KAAK,WAAW;QACvB,OAAO,EAAE,CAAC,eAAe,KAAK,QAAQ,EACvC,KAAK,CAAC,iCAAiC,CACvC,CAAC;AACH,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { assert } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIChannelAttributes,\n\tIFluidDataStoreRuntime,\n\tIChannelStorageService,\n} from \"@fluidframework/datastore-definitions/internal\";\nimport { MessageType } from \"@fluidframework/driver-definitions/internal\";\nimport { readAndParse } from \"@fluidframework/driver-utils/internal\";\nimport type {\n\tISummaryTreeWithStats,\n\tIRuntimeMessageCollection,\n\tIRuntimeMessagesContent,\n\tISequencedMessageEnvelope,\n} from \"@fluidframework/runtime-definitions/internal\";\nimport type { IFluidSerializer } from \"@fluidframework/shared-object-base/internal\";\nimport {\n\tSharedObject,\n\tcreateSingleBlobSummary,\n} from \"@fluidframework/shared-object-base/internal\";\n\nimport type { ISharedCounter, ISharedCounterEvents } from \"./interfaces.js\";\n\n/**\n * Describes the operation (op) format for incrementing the {@link SharedCounter}.\n */\nexport interface IIncrementOperation {\n\ttype: \"increment\";\n\tincrementAmount: number;\n}\n\n/**\n * Represents a pending op that has been submitted but not yet ack'd.\n * Includes the messageId that was used when submitting the op.\n */\ninterface IPendingOperation {\n\ttype: \"increment\";\n\tincrementAmount: number;\n\tmessageId: number;\n}\n\n/**\n * @remarks Used in snapshotting.\n */\ninterface ICounterSnapshotFormat {\n\t/**\n\t * The value of the counter.\n\t */\n\tvalue: number;\n}\n\nconst snapshotFileName = \"header\";\n\n/**\n * {@inheritDoc ISharedCounter}\n * @legacy @beta\n */\nexport class SharedCounter\n\textends SharedObject<ISharedCounterEvents>\n\timplements ISharedCounter\n{\n\tpublic constructor(\n\t\tid: string,\n\t\truntime: IFluidDataStoreRuntime,\n\t\tattributes: IChannelAttributes,\n\t) {\n\t\tsuper(id, runtime, attributes, \"fluid_counter_\");\n\t}\n\n\tprivate _value: number = 0;\n\n\t/**\n\t * Tracks pending local ops that have not been ack'd yet.\n\t */\n\tprivate readonly pendingOps: IPendingOperation[] = [];\n\n\t/**\n\t * The next message id to be used when submitting an op.\n\t */\n\tprivate nextPendingMessageId: number = 0;\n\n\t/**\n\t * {@inheritDoc ISharedCounter.value}\n\t */\n\tpublic get value(): number {\n\t\treturn this._value;\n\t}\n\n\t/**\n\t * {@inheritDoc ISharedCounter.increment}\n\t */\n\tpublic increment(incrementAmount: number): void {\n\t\t// Incrementing by floating point numbers will be eventually inconsistent, since the order in which the\n\t\t// increments are applied affects the result. A more-robust solution would be required to support this.\n\t\tif (incrementAmount % 1 !== 0) {\n\t\t\tthrow new Error(\"Must increment by a whole number\");\n\t\t}\n\n\t\tconst op: IIncrementOperation = {\n\t\t\ttype: \"increment\",\n\t\t\tincrementAmount,\n\t\t};\n\t\tconst messageId = this.nextPendingMessageId++;\n\n\t\tthis.incrementCore(incrementAmount);\n\t\t// We don't need to send the op if we are not attached yet.\n\t\tif (this.isAttached()) {\n\t\t\tthis.pendingOps.push({ ...op, messageId });\n\t\t\tthis.submitLocalMessage(op, messageId);\n\t\t}\n\t}\n\n\tprivate incrementCore(incrementAmount: number): void {\n\t\tthis._value += incrementAmount;\n\t\tthis.emit(\"incremented\", incrementAmount, this._value);\n\t}\n\n\t/**\n\t * Create a summary for the counter.\n\t *\n\t * @returns The summary of the current state of the counter.\n\t */\n\tprotected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {\n\t\t// Get a serializable form of data\n\t\tconst content: ICounterSnapshotFormat = {\n\t\t\tvalue: this.value,\n\t\t};\n\n\t\t// And then construct the summary for it\n\t\treturn createSingleBlobSummary(snapshotFileName, JSON.stringify(content));\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}\n\t */\n\tprotected async loadCore(storage: IChannelStorageService): Promise<void> {\n\t\tconst content = await readAndParse<ICounterSnapshotFormat>(storage, snapshotFileName);\n\n\t\tthis._value = content.value;\n\t}\n\n\t/**\n\t * Called when the object has disconnected from the delta stream.\n\t */\n\tprotected onDisconnect(): void {}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processMessagesCore}\n\t */\n\tprotected processMessagesCore(messagesCollection: IRuntimeMessageCollection): void {\n\t\tconst { envelope, local, messagesContent } = messagesCollection;\n\t\tfor (const messageContent of messagesContent) {\n\t\t\tthis.processMessage(envelope, messageContent, local);\n\t\t}\n\t}\n\n\tprivate processMessage(\n\t\tmessageEnvelope: ISequencedMessageEnvelope,\n\t\tmessageContent: IRuntimeMessagesContent,\n\t\tlocal: boolean,\n\t): void {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison\n\t\tif (messageEnvelope.type === MessageType.Operation) {\n\t\t\tconst op = messageContent.contents as IIncrementOperation;\n\n\t\t\t// If the message is local we have already optimistically processed\n\t\t\t// and we should now remove it from this.pendingOps.\n\t\t\t// If the message is from a remote client, we should process it.\n\t\t\tif (local) {\n\t\t\t\tconst pendingOp = this.pendingOps.shift();\n\t\t\t\tconst messageId = messageContent.localOpMetadata;\n\t\t\t\tassert(typeof messageId === \"number\", 0xc8e /* localOpMetadata should be a number */);\n\t\t\t\tassert(\n\t\t\t\t\tpendingOp !== undefined &&\n\t\t\t\t\t\tpendingOp.messageId === messageId &&\n\t\t\t\t\t\tpendingOp.type === op.type &&\n\t\t\t\t\t\tpendingOp.incrementAmount === op.incrementAmount,\n\t\t\t\t\t0xc8f /* local op mismatch */,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tswitch (op.type) {\n\t\t\t\t\tcase \"increment\": {\n\t\t\t\t\t\tthis.incrementCore(op.incrementAmount);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tthrow new Error(\"Unknown operation\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * {@inheritdoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}\n\t */\n\tprotected applyStashedOp(op: unknown): void {\n\t\tconst counterOp = op as IIncrementOperation;\n\n\t\t// TODO: Clean up error code linter violations repo-wide.\n\n\t\tassert(counterOp.type === \"increment\", 0x3ec /* Op type is not increment */);\n\n\t\tthis.increment(counterOp.incrementAmount);\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}\n\t * @sealed\n\t */\n\tprotected rollback(content: unknown, localOpMetadata: unknown): void {\n\t\tassertIsIncrementOp(content);\n\t\tassert(\n\t\t\ttypeof localOpMetadata === \"number\",\n\t\t\t0xc90 /* localOpMetadata should be a number */,\n\t\t);\n\t\tconst pendingOp = this.pendingOps.pop();\n\t\tassert(\n\t\t\tpendingOp !== undefined &&\n\t\t\t\tpendingOp.messageId === localOpMetadata &&\n\t\t\t\tpendingOp.type === content.type &&\n\t\t\t\tpendingOp.incrementAmount === content.incrementAmount,\n\t\t\t0xc91 /* op to rollback mismatch with pending op */,\n\t\t);\n\t\t// To rollback the optimistic increment we can increment by the opposite amount.\n\t\t// This will also emit another incremented event with the opposite amount.\n\t\tthis.incrementCore(-content.incrementAmount);\n\t}\n}\n\nfunction assertIsIncrementOp(op: unknown): asserts op is IIncrementOperation {\n\tassert(\n\t\ttypeof op === \"object\" &&\n\t\t\top !== null &&\n\t\t\t\"type\" in op &&\n\t\t\t\"incrementAmount\" in op &&\n\t\t\top.type === \"increment\" &&\n\t\t\ttypeof op.incrementAmount === \"number\",\n\t\t0xc92 /* invalid increment op format */,\n\t);\n}\n"]}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/counter";
8
- export declare const pkgVersion = "2.70.0";
8
+ export declare const pkgVersion = "2.71.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -8,5 +8,5 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.pkgVersion = exports.pkgName = void 0;
10
10
  exports.pkgName = "@fluidframework/counter";
11
- exports.pkgVersion = "2.70.0";
11
+ exports.pkgVersion = "2.71.0";
12
12
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,OAAO,GAAG,yBAAyB,CAAC;AACpC,QAAA,UAAU,GAAG,QAAQ,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/counter\";\nexport const pkgVersion = \"2.70.0\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,OAAO,GAAG,yBAAyB,CAAC;AACpC,QAAA,UAAU,GAAG,QAAQ,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/counter\";\nexport const pkgVersion = \"2.71.0\";\n"]}
package/lib/counter.d.ts CHANGED
@@ -7,6 +7,13 @@ import type { ISummaryTreeWithStats, IRuntimeMessageCollection } from "@fluidfra
7
7
  import type { IFluidSerializer } from "@fluidframework/shared-object-base/internal";
8
8
  import { SharedObject } from "@fluidframework/shared-object-base/internal";
9
9
  import type { ISharedCounter, ISharedCounterEvents } from "./interfaces.js";
10
+ /**
11
+ * Describes the operation (op) format for incrementing the {@link SharedCounter}.
12
+ */
13
+ export interface IIncrementOperation {
14
+ type: "increment";
15
+ incrementAmount: number;
16
+ }
10
17
  /**
11
18
  * {@inheritDoc ISharedCounter}
12
19
  * @legacy @beta
@@ -14,6 +21,14 @@ import type { ISharedCounter, ISharedCounterEvents } from "./interfaces.js";
14
21
  export declare class SharedCounter extends SharedObject<ISharedCounterEvents> implements ISharedCounter {
15
22
  constructor(id: string, runtime: IFluidDataStoreRuntime, attributes: IChannelAttributes);
16
23
  private _value;
24
+ /**
25
+ * Tracks pending local ops that have not been ack'd yet.
26
+ */
27
+ private readonly pendingOps;
28
+ /**
29
+ * The next message id to be used when submitting an op.
30
+ */
31
+ private nextPendingMessageId;
17
32
  /**
18
33
  * {@inheritDoc ISharedCounter.value}
19
34
  */
@@ -46,5 +61,10 @@ export declare class SharedCounter extends SharedObject<ISharedCounterEvents> im
46
61
  * {@inheritdoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
47
62
  */
48
63
  protected applyStashedOp(op: unknown): void;
64
+ /**
65
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
66
+ * @sealed
67
+ */
68
+ protected rollback(content: unknown, localOpMetadata: unknown): void;
49
69
  }
50
70
  //# sourceMappingURL=counter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,MAAM,gDAAgD,CAAC;AAGxD,OAAO,KAAK,EACX,qBAAqB,EACrB,yBAAyB,EAGzB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,EACN,YAAY,EAEZ,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAsB5E;;;GAGG;AACH,qBAAa,aACZ,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,cAAc;gBAGxB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,sBAAsB,EAC/B,UAAU,EAAE,kBAAkB;IAK/B,OAAO,CAAC,MAAM,CAAa;IAE3B;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED;;OAEG;IACI,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAgB/C,OAAO,CAAC,aAAa;IAKrB;;;;OAIG;IACH,SAAS,CAAC,aAAa,CAAC,UAAU,EAAE,gBAAgB,GAAG,qBAAqB;IAU5E;;OAEG;cACa,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxE;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAE9B;;OAEG;IACH,SAAS,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,yBAAyB,GAAG,IAAI;IAOlF,OAAO,CAAC,cAAc;IAsBtB;;OAEG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;CAS3C"}
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,MAAM,gDAAgD,CAAC;AAGxD,OAAO,KAAK,EACX,qBAAqB,EACrB,yBAAyB,EAGzB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,EACN,YAAY,EAEZ,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACxB;AAwBD;;;GAGG;AACH,qBAAa,aACZ,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,cAAc;gBAGxB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,sBAAsB,EAC/B,UAAU,EAAE,kBAAkB;IAK/B,OAAO,CAAC,MAAM,CAAa;IAE3B;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IAEtD;;OAEG;IACH,OAAO,CAAC,oBAAoB,CAAa;IAEzC;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED;;OAEG;IACI,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAqB/C,OAAO,CAAC,aAAa;IAKrB;;;;OAIG;IACH,SAAS,CAAC,aAAa,CAAC,UAAU,EAAE,gBAAgB,GAAG,qBAAqB;IAU5E;;OAEG;cACa,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxE;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAE9B;;OAEG;IACH,SAAS,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,yBAAyB,GAAG,IAAI;IAOlF,OAAO,CAAC,cAAc;IAsCtB;;OAEG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAU3C;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,GAAG,IAAI;CAkBpE"}
package/lib/counter.js CHANGED
@@ -15,6 +15,14 @@ export class SharedCounter extends SharedObject {
15
15
  constructor(id, runtime, attributes) {
16
16
  super(id, runtime, attributes, "fluid_counter_");
17
17
  this._value = 0;
18
+ /**
19
+ * Tracks pending local ops that have not been ack'd yet.
20
+ */
21
+ this.pendingOps = [];
22
+ /**
23
+ * The next message id to be used when submitting an op.
24
+ */
25
+ this.nextPendingMessageId = 0;
18
26
  }
19
27
  /**
20
28
  * {@inheritDoc ISharedCounter.value}
@@ -35,8 +43,13 @@ export class SharedCounter extends SharedObject {
35
43
  type: "increment",
36
44
  incrementAmount,
37
45
  };
46
+ const messageId = this.nextPendingMessageId++;
38
47
  this.incrementCore(incrementAmount);
39
- this.submitLocalMessage(op);
48
+ // We don't need to send the op if we are not attached yet.
49
+ if (this.isAttached()) {
50
+ this.pendingOps.push({ ...op, messageId });
51
+ this.submitLocalMessage(op, messageId);
52
+ }
40
53
  }
41
54
  incrementCore(incrementAmount) {
42
55
  this._value += incrementAmount;
@@ -77,15 +90,29 @@ export class SharedCounter extends SharedObject {
77
90
  }
78
91
  processMessage(messageEnvelope, messageContent, local) {
79
92
  // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
80
- if (messageEnvelope.type === MessageType.Operation && !local) {
93
+ if (messageEnvelope.type === MessageType.Operation) {
81
94
  const op = messageContent.contents;
82
- switch (op.type) {
83
- case "increment": {
84
- this.incrementCore(op.incrementAmount);
85
- break;
86
- }
87
- default: {
88
- throw new Error("Unknown operation");
95
+ // If the message is local we have already optimistically processed
96
+ // and we should now remove it from this.pendingOps.
97
+ // If the message is from a remote client, we should process it.
98
+ if (local) {
99
+ const pendingOp = this.pendingOps.shift();
100
+ const messageId = messageContent.localOpMetadata;
101
+ assert(typeof messageId === "number", 0xc8e /* localOpMetadata should be a number */);
102
+ assert(pendingOp !== undefined &&
103
+ pendingOp.messageId === messageId &&
104
+ pendingOp.type === op.type &&
105
+ pendingOp.incrementAmount === op.incrementAmount, 0xc8f /* local op mismatch */);
106
+ }
107
+ else {
108
+ switch (op.type) {
109
+ case "increment": {
110
+ this.incrementCore(op.incrementAmount);
111
+ break;
112
+ }
113
+ default: {
114
+ throw new Error("Unknown operation");
115
+ }
89
116
  }
90
117
  }
91
118
  }
@@ -99,5 +126,29 @@ export class SharedCounter extends SharedObject {
99
126
  assert(counterOp.type === "increment", 0x3ec /* Op type is not increment */);
100
127
  this.increment(counterOp.incrementAmount);
101
128
  }
129
+ /**
130
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
131
+ * @sealed
132
+ */
133
+ rollback(content, localOpMetadata) {
134
+ assertIsIncrementOp(content);
135
+ assert(typeof localOpMetadata === "number", 0xc90 /* localOpMetadata should be a number */);
136
+ const pendingOp = this.pendingOps.pop();
137
+ assert(pendingOp !== undefined &&
138
+ pendingOp.messageId === localOpMetadata &&
139
+ pendingOp.type === content.type &&
140
+ pendingOp.incrementAmount === content.incrementAmount, 0xc91 /* op to rollback mismatch with pending op */);
141
+ // To rollback the optimistic increment we can increment by the opposite amount.
142
+ // This will also emit another incremented event with the opposite amount.
143
+ this.incrementCore(-content.incrementAmount);
144
+ }
145
+ }
146
+ function assertIsIncrementOp(op) {
147
+ assert(typeof op === "object" &&
148
+ op !== null &&
149
+ "type" in op &&
150
+ "incrementAmount" in op &&
151
+ op.type === "increment" &&
152
+ typeof op.incrementAmount === "number", 0xc92 /* invalid increment op format */);
102
153
  }
103
154
  //# sourceMappingURL=counter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { assert } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIChannelAttributes,\n\tIFluidDataStoreRuntime,\n\tIChannelStorageService,\n} from \"@fluidframework/datastore-definitions/internal\";\nimport { MessageType } from \"@fluidframework/driver-definitions/internal\";\nimport { readAndParse } from \"@fluidframework/driver-utils/internal\";\nimport type {\n\tISummaryTreeWithStats,\n\tIRuntimeMessageCollection,\n\tIRuntimeMessagesContent,\n\tISequencedMessageEnvelope,\n} from \"@fluidframework/runtime-definitions/internal\";\nimport type { IFluidSerializer } from \"@fluidframework/shared-object-base/internal\";\nimport {\n\tSharedObject,\n\tcreateSingleBlobSummary,\n} from \"@fluidframework/shared-object-base/internal\";\n\nimport type { ISharedCounter, ISharedCounterEvents } from \"./interfaces.js\";\n\n/**\n * Describes the operation (op) format for incrementing the {@link SharedCounter}.\n */\ninterface IIncrementOperation {\n\ttype: \"increment\";\n\tincrementAmount: number;\n}\n\n/**\n * @remarks Used in snapshotting.\n */\ninterface ICounterSnapshotFormat {\n\t/**\n\t * The value of the counter.\n\t */\n\tvalue: number;\n}\n\nconst snapshotFileName = \"header\";\n\n/**\n * {@inheritDoc ISharedCounter}\n * @legacy @beta\n */\nexport class SharedCounter\n\textends SharedObject<ISharedCounterEvents>\n\timplements ISharedCounter\n{\n\tpublic constructor(\n\t\tid: string,\n\t\truntime: IFluidDataStoreRuntime,\n\t\tattributes: IChannelAttributes,\n\t) {\n\t\tsuper(id, runtime, attributes, \"fluid_counter_\");\n\t}\n\n\tprivate _value: number = 0;\n\n\t/**\n\t * {@inheritDoc ISharedCounter.value}\n\t */\n\tpublic get value(): number {\n\t\treturn this._value;\n\t}\n\n\t/**\n\t * {@inheritDoc ISharedCounter.increment}\n\t */\n\tpublic increment(incrementAmount: number): void {\n\t\t// Incrementing by floating point numbers will be eventually inconsistent, since the order in which the\n\t\t// increments are applied affects the result. A more-robust solution would be required to support this.\n\t\tif (incrementAmount % 1 !== 0) {\n\t\t\tthrow new Error(\"Must increment by a whole number\");\n\t\t}\n\n\t\tconst op: IIncrementOperation = {\n\t\t\ttype: \"increment\",\n\t\t\tincrementAmount,\n\t\t};\n\n\t\tthis.incrementCore(incrementAmount);\n\t\tthis.submitLocalMessage(op);\n\t}\n\n\tprivate incrementCore(incrementAmount: number): void {\n\t\tthis._value += incrementAmount;\n\t\tthis.emit(\"incremented\", incrementAmount, this._value);\n\t}\n\n\t/**\n\t * Create a summary for the counter.\n\t *\n\t * @returns The summary of the current state of the counter.\n\t */\n\tprotected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {\n\t\t// Get a serializable form of data\n\t\tconst content: ICounterSnapshotFormat = {\n\t\t\tvalue: this.value,\n\t\t};\n\n\t\t// And then construct the summary for it\n\t\treturn createSingleBlobSummary(snapshotFileName, JSON.stringify(content));\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}\n\t */\n\tprotected async loadCore(storage: IChannelStorageService): Promise<void> {\n\t\tconst content = await readAndParse<ICounterSnapshotFormat>(storage, snapshotFileName);\n\n\t\tthis._value = content.value;\n\t}\n\n\t/**\n\t * Called when the object has disconnected from the delta stream.\n\t */\n\tprotected onDisconnect(): void {}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processMessagesCore}\n\t */\n\tprotected processMessagesCore(messagesCollection: IRuntimeMessageCollection): void {\n\t\tconst { envelope, local, messagesContent } = messagesCollection;\n\t\tfor (const messageContent of messagesContent) {\n\t\t\tthis.processMessage(envelope, messageContent, local);\n\t\t}\n\t}\n\n\tprivate processMessage(\n\t\tmessageEnvelope: ISequencedMessageEnvelope,\n\t\tmessageContent: IRuntimeMessagesContent,\n\t\tlocal: boolean,\n\t): void {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison\n\t\tif (messageEnvelope.type === MessageType.Operation && !local) {\n\t\t\tconst op = messageContent.contents as IIncrementOperation;\n\n\t\t\tswitch (op.type) {\n\t\t\t\tcase \"increment\": {\n\t\t\t\t\tthis.incrementCore(op.incrementAmount);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tdefault: {\n\t\t\t\t\tthrow new Error(\"Unknown operation\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * {@inheritdoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}\n\t */\n\tprotected applyStashedOp(op: unknown): void {\n\t\tconst counterOp = op as IIncrementOperation;\n\n\t\t// TODO: Clean up error code linter violations repo-wide.\n\n\t\tassert(counterOp.type === \"increment\", 0x3ec /* Op type is not increment */);\n\n\t\tthis.increment(counterOp.incrementAmount);\n\t}\n}\n"]}
1
+ {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { assert } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIChannelAttributes,\n\tIFluidDataStoreRuntime,\n\tIChannelStorageService,\n} from \"@fluidframework/datastore-definitions/internal\";\nimport { MessageType } from \"@fluidframework/driver-definitions/internal\";\nimport { readAndParse } from \"@fluidframework/driver-utils/internal\";\nimport type {\n\tISummaryTreeWithStats,\n\tIRuntimeMessageCollection,\n\tIRuntimeMessagesContent,\n\tISequencedMessageEnvelope,\n} from \"@fluidframework/runtime-definitions/internal\";\nimport type { IFluidSerializer } from \"@fluidframework/shared-object-base/internal\";\nimport {\n\tSharedObject,\n\tcreateSingleBlobSummary,\n} from \"@fluidframework/shared-object-base/internal\";\n\nimport type { ISharedCounter, ISharedCounterEvents } from \"./interfaces.js\";\n\n/**\n * Describes the operation (op) format for incrementing the {@link SharedCounter}.\n */\nexport interface IIncrementOperation {\n\ttype: \"increment\";\n\tincrementAmount: number;\n}\n\n/**\n * Represents a pending op that has been submitted but not yet ack'd.\n * Includes the messageId that was used when submitting the op.\n */\ninterface IPendingOperation {\n\ttype: \"increment\";\n\tincrementAmount: number;\n\tmessageId: number;\n}\n\n/**\n * @remarks Used in snapshotting.\n */\ninterface ICounterSnapshotFormat {\n\t/**\n\t * The value of the counter.\n\t */\n\tvalue: number;\n}\n\nconst snapshotFileName = \"header\";\n\n/**\n * {@inheritDoc ISharedCounter}\n * @legacy @beta\n */\nexport class SharedCounter\n\textends SharedObject<ISharedCounterEvents>\n\timplements ISharedCounter\n{\n\tpublic constructor(\n\t\tid: string,\n\t\truntime: IFluidDataStoreRuntime,\n\t\tattributes: IChannelAttributes,\n\t) {\n\t\tsuper(id, runtime, attributes, \"fluid_counter_\");\n\t}\n\n\tprivate _value: number = 0;\n\n\t/**\n\t * Tracks pending local ops that have not been ack'd yet.\n\t */\n\tprivate readonly pendingOps: IPendingOperation[] = [];\n\n\t/**\n\t * The next message id to be used when submitting an op.\n\t */\n\tprivate nextPendingMessageId: number = 0;\n\n\t/**\n\t * {@inheritDoc ISharedCounter.value}\n\t */\n\tpublic get value(): number {\n\t\treturn this._value;\n\t}\n\n\t/**\n\t * {@inheritDoc ISharedCounter.increment}\n\t */\n\tpublic increment(incrementAmount: number): void {\n\t\t// Incrementing by floating point numbers will be eventually inconsistent, since the order in which the\n\t\t// increments are applied affects the result. A more-robust solution would be required to support this.\n\t\tif (incrementAmount % 1 !== 0) {\n\t\t\tthrow new Error(\"Must increment by a whole number\");\n\t\t}\n\n\t\tconst op: IIncrementOperation = {\n\t\t\ttype: \"increment\",\n\t\t\tincrementAmount,\n\t\t};\n\t\tconst messageId = this.nextPendingMessageId++;\n\n\t\tthis.incrementCore(incrementAmount);\n\t\t// We don't need to send the op if we are not attached yet.\n\t\tif (this.isAttached()) {\n\t\t\tthis.pendingOps.push({ ...op, messageId });\n\t\t\tthis.submitLocalMessage(op, messageId);\n\t\t}\n\t}\n\n\tprivate incrementCore(incrementAmount: number): void {\n\t\tthis._value += incrementAmount;\n\t\tthis.emit(\"incremented\", incrementAmount, this._value);\n\t}\n\n\t/**\n\t * Create a summary for the counter.\n\t *\n\t * @returns The summary of the current state of the counter.\n\t */\n\tprotected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {\n\t\t// Get a serializable form of data\n\t\tconst content: ICounterSnapshotFormat = {\n\t\t\tvalue: this.value,\n\t\t};\n\n\t\t// And then construct the summary for it\n\t\treturn createSingleBlobSummary(snapshotFileName, JSON.stringify(content));\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}\n\t */\n\tprotected async loadCore(storage: IChannelStorageService): Promise<void> {\n\t\tconst content = await readAndParse<ICounterSnapshotFormat>(storage, snapshotFileName);\n\n\t\tthis._value = content.value;\n\t}\n\n\t/**\n\t * Called when the object has disconnected from the delta stream.\n\t */\n\tprotected onDisconnect(): void {}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processMessagesCore}\n\t */\n\tprotected processMessagesCore(messagesCollection: IRuntimeMessageCollection): void {\n\t\tconst { envelope, local, messagesContent } = messagesCollection;\n\t\tfor (const messageContent of messagesContent) {\n\t\t\tthis.processMessage(envelope, messageContent, local);\n\t\t}\n\t}\n\n\tprivate processMessage(\n\t\tmessageEnvelope: ISequencedMessageEnvelope,\n\t\tmessageContent: IRuntimeMessagesContent,\n\t\tlocal: boolean,\n\t): void {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison\n\t\tif (messageEnvelope.type === MessageType.Operation) {\n\t\t\tconst op = messageContent.contents as IIncrementOperation;\n\n\t\t\t// If the message is local we have already optimistically processed\n\t\t\t// and we should now remove it from this.pendingOps.\n\t\t\t// If the message is from a remote client, we should process it.\n\t\t\tif (local) {\n\t\t\t\tconst pendingOp = this.pendingOps.shift();\n\t\t\t\tconst messageId = messageContent.localOpMetadata;\n\t\t\t\tassert(typeof messageId === \"number\", 0xc8e /* localOpMetadata should be a number */);\n\t\t\t\tassert(\n\t\t\t\t\tpendingOp !== undefined &&\n\t\t\t\t\t\tpendingOp.messageId === messageId &&\n\t\t\t\t\t\tpendingOp.type === op.type &&\n\t\t\t\t\t\tpendingOp.incrementAmount === op.incrementAmount,\n\t\t\t\t\t0xc8f /* local op mismatch */,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tswitch (op.type) {\n\t\t\t\t\tcase \"increment\": {\n\t\t\t\t\t\tthis.incrementCore(op.incrementAmount);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tthrow new Error(\"Unknown operation\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * {@inheritdoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}\n\t */\n\tprotected applyStashedOp(op: unknown): void {\n\t\tconst counterOp = op as IIncrementOperation;\n\n\t\t// TODO: Clean up error code linter violations repo-wide.\n\n\t\tassert(counterOp.type === \"increment\", 0x3ec /* Op type is not increment */);\n\n\t\tthis.increment(counterOp.incrementAmount);\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}\n\t * @sealed\n\t */\n\tprotected rollback(content: unknown, localOpMetadata: unknown): void {\n\t\tassertIsIncrementOp(content);\n\t\tassert(\n\t\t\ttypeof localOpMetadata === \"number\",\n\t\t\t0xc90 /* localOpMetadata should be a number */,\n\t\t);\n\t\tconst pendingOp = this.pendingOps.pop();\n\t\tassert(\n\t\t\tpendingOp !== undefined &&\n\t\t\t\tpendingOp.messageId === localOpMetadata &&\n\t\t\t\tpendingOp.type === content.type &&\n\t\t\t\tpendingOp.incrementAmount === content.incrementAmount,\n\t\t\t0xc91 /* op to rollback mismatch with pending op */,\n\t\t);\n\t\t// To rollback the optimistic increment we can increment by the opposite amount.\n\t\t// This will also emit another incremented event with the opposite amount.\n\t\tthis.incrementCore(-content.incrementAmount);\n\t}\n}\n\nfunction assertIsIncrementOp(op: unknown): asserts op is IIncrementOperation {\n\tassert(\n\t\ttypeof op === \"object\" &&\n\t\t\top !== null &&\n\t\t\t\"type\" in op &&\n\t\t\t\"incrementAmount\" in op &&\n\t\t\top.type === \"increment\" &&\n\t\t\ttypeof op.incrementAmount === \"number\",\n\t\t0xc92 /* invalid increment op format */,\n\t);\n}\n"]}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/counter";
8
- export declare const pkgVersion = "2.70.0";
8
+ export declare const pkgVersion = "2.71.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export const pkgName = "@fluidframework/counter";
8
- export const pkgVersion = "2.70.0";
8
+ export const pkgVersion = "2.71.0";
9
9
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,yBAAyB,CAAC;AACjD,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/counter\";\nexport const pkgVersion = \"2.70.0\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,yBAAyB,CAAC;AACjD,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/counter\";\nexport const pkgVersion = \"2.71.0\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/counter",
3
- "version": "2.70.0",
3
+ "version": "2.71.0",
4
4
  "description": "Counter DDS",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -42,6 +42,18 @@
42
42
  "types": "./dist/index.d.ts",
43
43
  "default": "./dist/index.js"
44
44
  }
45
+ },
46
+ "./internal/test": {
47
+ "allow-ff-test-exports": {
48
+ "import": {
49
+ "types": "./lib/test/index.d.ts",
50
+ "default": "./lib/test/index.js"
51
+ },
52
+ "require": {
53
+ "types": "./dist/test/index.d.ts",
54
+ "default": "./dist/test/index.js"
55
+ }
56
+ }
45
57
  }
46
58
  },
47
59
  "main": "lib/index.js",
@@ -69,25 +81,27 @@
69
81
  "temp-directory": "nyc/.nyc_output"
70
82
  },
71
83
  "dependencies": {
72
- "@fluidframework/core-interfaces": "~2.70.0",
73
- "@fluidframework/core-utils": "~2.70.0",
74
- "@fluidframework/datastore-definitions": "~2.70.0",
75
- "@fluidframework/driver-definitions": "~2.70.0",
76
- "@fluidframework/driver-utils": "~2.70.0",
77
- "@fluidframework/runtime-definitions": "~2.70.0",
78
- "@fluidframework/shared-object-base": "~2.70.0"
84
+ "@fluidframework/core-interfaces": "~2.71.0",
85
+ "@fluidframework/core-utils": "~2.71.0",
86
+ "@fluidframework/datastore-definitions": "~2.71.0",
87
+ "@fluidframework/driver-definitions": "~2.71.0",
88
+ "@fluidframework/driver-utils": "~2.71.0",
89
+ "@fluidframework/runtime-definitions": "~2.71.0",
90
+ "@fluidframework/shared-object-base": "~2.71.0"
79
91
  },
80
92
  "devDependencies": {
81
93
  "@arethetypeswrong/cli": "^0.17.1",
82
94
  "@biomejs/biome": "~1.9.3",
83
- "@fluid-internal/mocha-test-setup": "~2.70.0",
95
+ "@fluid-internal/mocha-test-setup": "~2.71.0",
96
+ "@fluid-private/stochastic-test-utils": "~2.71.0",
97
+ "@fluid-private/test-dds-utils": "~2.71.0",
84
98
  "@fluid-tools/build-cli": "^0.58.3",
85
99
  "@fluidframework/build-common": "^2.0.3",
86
100
  "@fluidframework/build-tools": "^0.58.3",
87
- "@fluidframework/container-definitions": "~2.70.0",
88
- "@fluidframework/counter-previous": "npm:@fluidframework/counter@2.63.0",
89
- "@fluidframework/eslint-config-fluid": "^6.1.0",
90
- "@fluidframework/test-runtime-utils": "~2.70.0",
101
+ "@fluidframework/container-definitions": "~2.71.0",
102
+ "@fluidframework/counter-previous": "npm:@fluidframework/counter@2.70.0",
103
+ "@fluidframework/eslint-config-fluid": "^7.0.0",
104
+ "@fluidframework/test-runtime-utils": "~2.71.0",
91
105
  "@microsoft/api-extractor": "7.52.11",
92
106
  "@types/mocha": "^10.0.10",
93
107
  "@types/node": "^18.19.0",
@@ -121,7 +135,7 @@
121
135
  "build:test": "npm run build:test:esm && npm run build:test:cjs",
122
136
  "build:test:cjs": "fluid-tsc commonjs --project ./src/test/tsconfig.cjs.json",
123
137
  "build:test:esm": "tsc --project ./src/test/tsconfig.json",
124
- "check:are-the-types-wrong": "attw --pack .",
138
+ "check:are-the-types-wrong": "attw --pack . --exclude-entrypoints ./internal/test",
125
139
  "check:biome": "biome check .",
126
140
  "check:exports": "concurrently \"npm:check:exports:*\"",
127
141
  "check:exports:bundle-release-tags": "api-extractor run --config api-extractor/api-extractor-lint-bundle.json",
@@ -135,8 +149,8 @@
135
149
  "ci:build:api-reports:legacy": "api-extractor run --config api-extractor/api-extractor.legacy.json",
136
150
  "ci:build:docs": "api-extractor run",
137
151
  "clean": "rimraf --glob dist lib {alpha,beta,internal,legacy}.d.ts \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp nyc",
138
- "eslint": "eslint --format stylish src",
139
- "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
152
+ "eslint": "eslint --quiet --format stylish src",
153
+ "eslint:fix": "eslint --quiet --format stylish src --fix --fix-type problem,suggestion,layout",
140
154
  "format": "npm run format:biome",
141
155
  "format:biome": "biome check . --write",
142
156
  "lint": "fluid-build . --task lint",
package/src/counter.ts CHANGED
@@ -28,11 +28,21 @@ import type { ISharedCounter, ISharedCounterEvents } from "./interfaces.js";
28
28
  /**
29
29
  * Describes the operation (op) format for incrementing the {@link SharedCounter}.
30
30
  */
31
- interface IIncrementOperation {
31
+ export interface IIncrementOperation {
32
32
  type: "increment";
33
33
  incrementAmount: number;
34
34
  }
35
35
 
36
+ /**
37
+ * Represents a pending op that has been submitted but not yet ack'd.
38
+ * Includes the messageId that was used when submitting the op.
39
+ */
40
+ interface IPendingOperation {
41
+ type: "increment";
42
+ incrementAmount: number;
43
+ messageId: number;
44
+ }
45
+
36
46
  /**
37
47
  * @remarks Used in snapshotting.
38
48
  */
@@ -63,6 +73,16 @@ export class SharedCounter
63
73
 
64
74
  private _value: number = 0;
65
75
 
76
+ /**
77
+ * Tracks pending local ops that have not been ack'd yet.
78
+ */
79
+ private readonly pendingOps: IPendingOperation[] = [];
80
+
81
+ /**
82
+ * The next message id to be used when submitting an op.
83
+ */
84
+ private nextPendingMessageId: number = 0;
85
+
66
86
  /**
67
87
  * {@inheritDoc ISharedCounter.value}
68
88
  */
@@ -84,9 +104,14 @@ export class SharedCounter
84
104
  type: "increment",
85
105
  incrementAmount,
86
106
  };
107
+ const messageId = this.nextPendingMessageId++;
87
108
 
88
109
  this.incrementCore(incrementAmount);
89
- this.submitLocalMessage(op);
110
+ // We don't need to send the op if we are not attached yet.
111
+ if (this.isAttached()) {
112
+ this.pendingOps.push({ ...op, messageId });
113
+ this.submitLocalMessage(op, messageId);
114
+ }
90
115
  }
91
116
 
92
117
  private incrementCore(incrementAmount: number): void {
@@ -139,17 +164,33 @@ export class SharedCounter
139
164
  local: boolean,
140
165
  ): void {
141
166
  // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
142
- if (messageEnvelope.type === MessageType.Operation && !local) {
167
+ if (messageEnvelope.type === MessageType.Operation) {
143
168
  const op = messageContent.contents as IIncrementOperation;
144
169
 
145
- switch (op.type) {
146
- case "increment": {
147
- this.incrementCore(op.incrementAmount);
148
- break;
149
- }
150
-
151
- default: {
152
- throw new Error("Unknown operation");
170
+ // If the message is local we have already optimistically processed
171
+ // and we should now remove it from this.pendingOps.
172
+ // If the message is from a remote client, we should process it.
173
+ if (local) {
174
+ const pendingOp = this.pendingOps.shift();
175
+ const messageId = messageContent.localOpMetadata;
176
+ assert(typeof messageId === "number", 0xc8e /* localOpMetadata should be a number */);
177
+ assert(
178
+ pendingOp !== undefined &&
179
+ pendingOp.messageId === messageId &&
180
+ pendingOp.type === op.type &&
181
+ pendingOp.incrementAmount === op.incrementAmount,
182
+ 0xc8f /* local op mismatch */,
183
+ );
184
+ } else {
185
+ switch (op.type) {
186
+ case "increment": {
187
+ this.incrementCore(op.incrementAmount);
188
+ break;
189
+ }
190
+
191
+ default: {
192
+ throw new Error("Unknown operation");
193
+ }
153
194
  }
154
195
  }
155
196
  }
@@ -167,4 +208,39 @@ export class SharedCounter
167
208
 
168
209
  this.increment(counterOp.incrementAmount);
169
210
  }
211
+
212
+ /**
213
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
214
+ * @sealed
215
+ */
216
+ protected rollback(content: unknown, localOpMetadata: unknown): void {
217
+ assertIsIncrementOp(content);
218
+ assert(
219
+ typeof localOpMetadata === "number",
220
+ 0xc90 /* localOpMetadata should be a number */,
221
+ );
222
+ const pendingOp = this.pendingOps.pop();
223
+ assert(
224
+ pendingOp !== undefined &&
225
+ pendingOp.messageId === localOpMetadata &&
226
+ pendingOp.type === content.type &&
227
+ pendingOp.incrementAmount === content.incrementAmount,
228
+ 0xc91 /* op to rollback mismatch with pending op */,
229
+ );
230
+ // To rollback the optimistic increment we can increment by the opposite amount.
231
+ // This will also emit another incremented event with the opposite amount.
232
+ this.incrementCore(-content.incrementAmount);
233
+ }
234
+ }
235
+
236
+ function assertIsIncrementOp(op: unknown): asserts op is IIncrementOperation {
237
+ assert(
238
+ typeof op === "object" &&
239
+ op !== null &&
240
+ "type" in op &&
241
+ "incrementAmount" in op &&
242
+ op.type === "increment" &&
243
+ typeof op.incrementAmount === "number",
244
+ 0xc92 /* invalid increment op format */,
245
+ );
170
246
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/counter";
9
- export const pkgVersion = "2.70.0";
9
+ export const pkgVersion = "2.71.0";