@fluidframework/map 2.43.0 → 2.50.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/map",
3
- "version": "2.43.0",
3
+ "version": "2.50.0",
4
4
  "description": "Distributed map",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -81,33 +81,33 @@
81
81
  "temp-directory": "nyc/.nyc_output"
82
82
  },
83
83
  "dependencies": {
84
- "@fluid-internal/client-utils": "~2.43.0",
85
- "@fluidframework/core-interfaces": "~2.43.0",
86
- "@fluidframework/core-utils": "~2.43.0",
87
- "@fluidframework/datastore-definitions": "~2.43.0",
88
- "@fluidframework/driver-definitions": "~2.43.0",
89
- "@fluidframework/driver-utils": "~2.43.0",
90
- "@fluidframework/merge-tree": "~2.43.0",
91
- "@fluidframework/runtime-definitions": "~2.43.0",
92
- "@fluidframework/runtime-utils": "~2.43.0",
93
- "@fluidframework/shared-object-base": "~2.43.0",
94
- "@fluidframework/telemetry-utils": "~2.43.0",
84
+ "@fluid-internal/client-utils": "~2.50.0",
85
+ "@fluidframework/core-interfaces": "~2.50.0",
86
+ "@fluidframework/core-utils": "~2.50.0",
87
+ "@fluidframework/datastore-definitions": "~2.50.0",
88
+ "@fluidframework/driver-definitions": "~2.50.0",
89
+ "@fluidframework/driver-utils": "~2.50.0",
90
+ "@fluidframework/merge-tree": "~2.50.0",
91
+ "@fluidframework/runtime-definitions": "~2.50.0",
92
+ "@fluidframework/runtime-utils": "~2.50.0",
93
+ "@fluidframework/shared-object-base": "~2.50.0",
94
+ "@fluidframework/telemetry-utils": "~2.50.0",
95
95
  "path-browserify": "^1.0.1"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@arethetypeswrong/cli": "^0.17.1",
99
99
  "@biomejs/biome": "~1.9.3",
100
- "@fluid-internal/mocha-test-setup": "~2.43.0",
101
- "@fluid-private/stochastic-test-utils": "~2.43.0",
102
- "@fluid-private/test-dds-utils": "~2.43.0",
100
+ "@fluid-internal/mocha-test-setup": "~2.50.0",
101
+ "@fluid-private/stochastic-test-utils": "~2.50.0",
102
+ "@fluid-private/test-dds-utils": "~2.50.0",
103
103
  "@fluid-tools/benchmark": "^0.51.0",
104
104
  "@fluid-tools/build-cli": "^0.56.0",
105
105
  "@fluidframework/build-common": "^2.0.3",
106
106
  "@fluidframework/build-tools": "^0.56.0",
107
- "@fluidframework/container-definitions": "~2.43.0",
107
+ "@fluidframework/container-definitions": "~2.50.0",
108
108
  "@fluidframework/eslint-config-fluid": "^5.7.4",
109
- "@fluidframework/map-previous": "npm:@fluidframework/map@2.42.0",
110
- "@fluidframework/test-runtime-utils": "~2.43.0",
109
+ "@fluidframework/map-previous": "npm:@fluidframework/map@2.43.0",
110
+ "@fluidframework/test-runtime-utils": "~2.50.0",
111
111
  "@microsoft/api-extractor": "7.52.8",
112
112
  "@types/mocha": "^10.0.10",
113
113
  "@types/node": "^18.19.0",
package/src/map.ts CHANGED
@@ -275,7 +275,7 @@ export class SharedMap extends SharedObject<ISharedMapEvents> implements IShared
275
275
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
276
276
  */
277
277
  protected override reSubmitCore(content: unknown, localOpMetadata: unknown): void {
278
- this.kernel.trySubmitMessage(content as IMapOperation, localOpMetadata);
278
+ this.kernel.tryResubmitMessage(content as IMapOperation, localOpMetadata);
279
279
  }
280
280
 
281
281
  /**
package/src/mapKernel.ts CHANGED
@@ -5,7 +5,12 @@
5
5
 
6
6
  import type { TypedEventEmitter } from "@fluid-internal/client-utils";
7
7
  import type { IFluidHandle } from "@fluidframework/core-interfaces";
8
- import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
8
+ import {
9
+ assert,
10
+ DoublyLinkedList,
11
+ type ListNode,
12
+ unreachableCase,
13
+ } from "@fluidframework/core-utils/internal";
9
14
  import type { IFluidSerializer } from "@fluidframework/shared-object-base/internal";
10
15
  import { ValueType } from "@fluidframework/shared-object-base/internal";
11
16
 
@@ -28,7 +33,7 @@ import {
28
33
  } from "./localValues.js";
29
34
 
30
35
  /**
31
- * Defines the means to process and submit a given op on a map.
36
+ * Defines the means to process and resubmit a given op on a map.
32
37
  */
33
38
  interface IMapMessageHandler {
34
39
  /**
@@ -38,14 +43,18 @@ interface IMapMessageHandler {
38
43
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
39
44
  * For messages from a remote client, this will be undefined.
40
45
  */
41
- process(op: IMapOperation, local: boolean, localOpMetadata: MapLocalOpMetadata): void;
46
+ process(
47
+ op: IMapOperation,
48
+ local: boolean,
49
+ localOpMetadata: ListNode<MapLocalOpMetadata> | undefined,
50
+ ): void;
42
51
 
43
52
  /**
44
- * Communicate the operation to remote clients.
45
- * @param op - The map operation to submit
46
- * @param localOpMetadata - The metadata to be submitted with the message.
53
+ * Resubmit a previously submitted operation that was not delivered.
54
+ * @param op - The map operation to resubmit
55
+ * @param localOpMetadata - The metadata that was originally submitted with the message.
47
56
  */
48
- submit(op: IMapOperation, localOpMetadata: MapLocalOpMetadata): void;
57
+ resubmit(op: IMapOperation, localOpMetadata: ListNode<MapLocalOpMetadata>): void;
49
58
  }
50
59
 
51
60
  /**
@@ -95,14 +104,6 @@ function isClearLocalOpMetadata(metadata: any): metadata is IMapClearLocalOpMeta
95
104
  );
96
105
  }
97
106
 
98
- function isMapLocalOpMetadata(metadata: any): metadata is MapLocalOpMetadata {
99
- return (
100
- metadata !== undefined &&
101
- typeof metadata.pendingMessageId === "number" &&
102
- (metadata.type === "add" || metadata.type === "edit" || metadata.type === "clear")
103
- );
104
- }
105
-
106
107
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
107
108
 
108
109
  function createClearLocalOpMetadata(
@@ -160,6 +161,12 @@ export class MapKernel {
160
161
  */
161
162
  private nextPendingMessageId: number = 0;
162
163
 
164
+ /**
165
+ * The pending metadata for any local operations that have not yet been ack'd from the server, in order.
166
+ */
167
+ private readonly pendingMapLocalOpMetadata: DoublyLinkedList<MapLocalOpMetadata> =
168
+ new DoublyLinkedList<MapLocalOpMetadata>();
169
+
163
170
  /**
164
171
  * The pending ids of any clears that have been performed locally but not yet ack'd from the server
165
172
  */
@@ -370,19 +377,19 @@ export class MapKernel {
370
377
  }
371
378
 
372
379
  /**
373
- * Submit the given op if a handler is registered.
380
+ * Resubmit the given op if a handler is registered.
374
381
  * @param op - The operation to attempt to submit
375
382
  * @param localOpMetadata - The local metadata associated with the op. This is kept locally by the runtime
376
383
  * and not sent to the server. This will be sent back when this message is received back from the server. This is
377
384
  * also sent if we are asked to resubmit the message.
378
385
  * @returns True if the operation was submitted, false otherwise.
379
386
  */
380
- public trySubmitMessage(op: IMapOperation, localOpMetadata: unknown): boolean {
387
+ public tryResubmitMessage(op: IMapOperation, localOpMetadata: unknown): boolean {
381
388
  const handler = this.messageHandlers.get(op.type);
382
389
  if (handler === undefined) {
383
390
  return false;
384
391
  }
385
- handler.submit(op, localOpMetadata as MapLocalOpMetadata);
392
+ handler.resubmit(op, localOpMetadata as ListNode<MapLocalOpMetadata>);
386
393
  return true;
387
394
  }
388
395
 
@@ -431,65 +438,68 @@ export class MapKernel {
431
438
  if (handler === undefined) {
432
439
  return false;
433
440
  }
434
- handler.process(op, local, localOpMetadata as MapLocalOpMetadata);
441
+ handler.process(op, local, localOpMetadata as ListNode<MapLocalOpMetadata> | undefined);
435
442
  return true;
436
443
  }
437
444
 
438
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
439
-
440
445
  /**
441
446
  * Rollback a local op
442
447
  * @param op - The operation to rollback
443
448
  * @param localOpMetadata - The local metadata associated with the op.
444
449
  */
445
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
446
- public rollback(op: any, localOpMetadata: unknown): void {
447
- if (!isMapLocalOpMetadata(localOpMetadata)) {
448
- throw new Error("Invalid localOpMetadata");
449
- }
450
+ public rollback(op: unknown, localOpMetadata: unknown): void {
451
+ const mapOp: IMapOperation = op as IMapOperation;
452
+ const listNodeLocalOpMetadata = localOpMetadata as ListNode<MapLocalOpMetadata>;
453
+ const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.pop();
454
+ assert(
455
+ removedLocalOpMetadata !== undefined &&
456
+ removedLocalOpMetadata === listNodeLocalOpMetadata,
457
+ 0xbcb /* Rolling back unexpected op */,
458
+ );
450
459
 
451
- if (op.type === "clear" && localOpMetadata.type === "clear") {
452
- if (localOpMetadata.previousMap === undefined) {
460
+ if (mapOp.type === "clear" && listNodeLocalOpMetadata.data.type === "clear") {
461
+ if (listNodeLocalOpMetadata.data.previousMap === undefined) {
453
462
  throw new Error("Cannot rollback without previous map");
454
463
  }
455
- for (const [key, localValue] of localOpMetadata.previousMap.entries()) {
464
+ for (const [key, localValue] of listNodeLocalOpMetadata.data.previousMap.entries()) {
456
465
  this.setCore(key, localValue, true);
457
466
  }
458
467
 
459
468
  const lastPendingClearId = this.pendingClearMessageIds.pop();
460
469
  if (
461
470
  lastPendingClearId === undefined ||
462
- lastPendingClearId !== localOpMetadata.pendingMessageId
471
+ lastPendingClearId !== listNodeLocalOpMetadata.data.pendingMessageId
463
472
  ) {
464
473
  throw new Error("Rollback op does match last clear");
465
474
  }
466
- } else if (op.type === "delete" || op.type === "set") {
467
- if (localOpMetadata.type === "add") {
468
- this.deleteCore(op.key as string, true);
475
+ } else if (mapOp.type === "delete" || mapOp.type === "set") {
476
+ if (listNodeLocalOpMetadata.data.type === "add") {
477
+ this.deleteCore(mapOp.key, true);
469
478
  } else if (
470
- localOpMetadata.type === "edit" &&
471
- localOpMetadata.previousValue !== undefined
479
+ listNodeLocalOpMetadata.data.type === "edit" &&
480
+ listNodeLocalOpMetadata.data.previousValue !== undefined
472
481
  ) {
473
- this.setCore(op.key as string, localOpMetadata.previousValue, true);
482
+ this.setCore(mapOp.key, listNodeLocalOpMetadata.data.previousValue, true);
474
483
  } else {
475
484
  throw new Error("Cannot rollback without previous value");
476
485
  }
477
486
 
478
- const pendingMessageIds = this.pendingKeys.get(op.key as string);
487
+ const pendingMessageIds = this.pendingKeys.get(mapOp.key);
479
488
  const lastPendingMessageId = pendingMessageIds?.pop();
480
- if (!pendingMessageIds || lastPendingMessageId !== localOpMetadata.pendingMessageId) {
489
+ if (
490
+ !pendingMessageIds ||
491
+ lastPendingMessageId !== listNodeLocalOpMetadata.data.pendingMessageId
492
+ ) {
481
493
  throw new Error("Rollback op does not match last pending");
482
494
  }
483
495
  if (pendingMessageIds.length === 0) {
484
- this.pendingKeys.delete(op.key as string);
496
+ this.pendingKeys.delete(mapOp.key);
485
497
  }
486
498
  } else {
487
499
  throw new Error("Unsupported op for rollback");
488
500
  }
489
501
  }
490
502
 
491
- /* eslint-enable @typescript-eslint/no-unsafe-member-access */
492
-
493
503
  /**
494
504
  * Set implementation used for both locally sourced sets as well as incoming remote sets.
495
505
  * @param key - The key being set
@@ -562,7 +572,7 @@ export class MapKernel {
562
572
  private needProcessKeyOperation(
563
573
  op: IMapKeyOperation,
564
574
  local: boolean,
565
- localOpMetadata: MapLocalOpMetadata,
575
+ localOpMetadata: MapLocalOpMetadata | undefined,
566
576
  ): boolean {
567
577
  if (this.pendingClearMessageIds[0] !== undefined) {
568
578
  if (local) {
@@ -609,15 +619,24 @@ export class MapKernel {
609
619
  private getMessageHandlers(): Map<string, IMapMessageHandler> {
610
620
  const messageHandlers = new Map<string, IMapMessageHandler>();
611
621
  messageHandlers.set("clear", {
612
- process: (op: IMapClearOperation, local, localOpMetadata) => {
622
+ process: (
623
+ op: IMapClearOperation,
624
+ local: boolean,
625
+ localOpMetadata: ListNode<MapLocalOpMetadata> | undefined,
626
+ ) => {
613
627
  if (local) {
628
+ const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.shift();
629
+ assert(
630
+ removedLocalOpMetadata !== undefined && removedLocalOpMetadata === localOpMetadata,
631
+ 0xbcc /* Processing unexpected local clear op */,
632
+ );
614
633
  assert(
615
- isClearLocalOpMetadata(localOpMetadata),
634
+ isClearLocalOpMetadata(localOpMetadata.data),
616
635
  0x015 /* "pendingMessageId is missing from the local client's clear operation" */,
617
636
  );
618
637
  const pendingClearMessageId = this.pendingClearMessageIds.shift();
619
638
  assert(
620
- pendingClearMessageId === localOpMetadata.pendingMessageId,
639
+ pendingClearMessageId === localOpMetadata.data.pendingMessageId,
621
640
  0x2fb /* pendingMessageId does not match */,
622
641
  );
623
642
  return;
@@ -628,34 +647,66 @@ export class MapKernel {
628
647
  }
629
648
  this.clearCore(local);
630
649
  },
631
- submit: (op: IMapClearOperation, localOpMetadata: IMapClearLocalOpMetadata) => {
650
+ resubmit: (op: IMapClearOperation, localOpMetadata: ListNode<MapLocalOpMetadata>) => {
651
+ const removedLocalOpMetadata = localOpMetadata.remove()?.data;
652
+ assert(
653
+ removedLocalOpMetadata !== undefined,
654
+ 0xbcd /* Resubmitting unexpected local clear op */,
655
+ );
632
656
  assert(
633
- isClearLocalOpMetadata(localOpMetadata),
657
+ isClearLocalOpMetadata(localOpMetadata.data),
634
658
  0x2fc /* Invalid localOpMetadata for clear */,
635
659
  );
636
660
  // We don't reuse the metadata pendingMessageId but send a new one on each submit.
637
661
  const pendingClearMessageId = this.pendingClearMessageIds.shift();
638
662
  assert(
639
- pendingClearMessageId === localOpMetadata.pendingMessageId,
663
+ pendingClearMessageId === localOpMetadata.data.pendingMessageId,
640
664
  0x2fd /* pendingMessageId does not match */,
641
665
  );
642
- this.submitMapClearMessage(op, localOpMetadata.previousMap);
666
+ this.submitMapClearMessage(op, localOpMetadata.data.previousMap);
643
667
  },
644
668
  });
645
669
  messageHandlers.set("delete", {
646
- process: (op: IMapDeleteOperation, local, localOpMetadata) => {
647
- if (!this.needProcessKeyOperation(op, local, localOpMetadata)) {
670
+ process: (
671
+ op: IMapDeleteOperation,
672
+ local: boolean,
673
+ localOpMetadata: ListNode<MapLocalOpMetadata> | undefined,
674
+ ) => {
675
+ if (local) {
676
+ const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.shift();
677
+ assert(
678
+ removedLocalOpMetadata !== undefined && removedLocalOpMetadata === localOpMetadata,
679
+ 0xbce /* Processing unexpected local delete op */,
680
+ );
681
+ }
682
+ if (!this.needProcessKeyOperation(op, local, localOpMetadata?.data)) {
648
683
  return;
649
684
  }
650
685
  this.deleteCore(op.key, local);
651
686
  },
652
- submit: (op: IMapDeleteOperation, localOpMetadata: MapKeyLocalOpMetadata) => {
653
- this.resubmitMapKeyMessage(op, localOpMetadata);
687
+ resubmit: (op: IMapDeleteOperation, localOpMetadata: ListNode<MapLocalOpMetadata>) => {
688
+ const removedLocalOpMetadata = localOpMetadata.remove()?.data;
689
+ assert(
690
+ removedLocalOpMetadata !== undefined,
691
+ 0xbcf /* Resubmitting unexpected local delete op */,
692
+ );
693
+ this.resubmitMapKeyMessage(op, localOpMetadata.data);
654
694
  },
655
695
  });
656
696
  messageHandlers.set("set", {
657
- process: (op: IMapSetOperation, local, localOpMetadata) => {
658
- if (!this.needProcessKeyOperation(op, local, localOpMetadata)) {
697
+ process: (
698
+ op: IMapSetOperation,
699
+ local: boolean,
700
+ localOpMetadata: ListNode<MapLocalOpMetadata> | undefined,
701
+ ) => {
702
+ if (local) {
703
+ const removedLocalOpMetadata = this.pendingMapLocalOpMetadata.shift();
704
+ assert(
705
+ removedLocalOpMetadata !== undefined && removedLocalOpMetadata === localOpMetadata,
706
+ 0xbd0 /* Processing unexpected local set op */,
707
+ );
708
+ }
709
+ if (!this.needProcessKeyOperation(op, local, localOpMetadata?.data)) {
659
710
  return;
660
711
  }
661
712
 
@@ -663,8 +714,13 @@ export class MapKernel {
663
714
  migrateIfSharedSerializable(op.value, this.serializer, this.handle);
664
715
  this.setCore(op.key, { value: op.value.value }, local);
665
716
  },
666
- submit: (op: IMapSetOperation, localOpMetadata: MapKeyLocalOpMetadata) => {
667
- this.resubmitMapKeyMessage(op, localOpMetadata);
717
+ resubmit: (op: IMapSetOperation, localOpMetadata: ListNode<MapLocalOpMetadata>) => {
718
+ const removedLocalOpMetadata = localOpMetadata.remove()?.data;
719
+ assert(
720
+ removedLocalOpMetadata !== undefined,
721
+ 0xbd1 /* Resubmitting unexpected local set op */,
722
+ );
723
+ this.resubmitMapKeyMessage(op, localOpMetadata.data);
668
724
  },
669
725
  });
670
726
 
@@ -685,8 +741,10 @@ export class MapKernel {
685
741
  op: IMapClearOperation,
686
742
  previousMap?: Map<string, ILocalValue>,
687
743
  ): void {
688
- const metadata = createClearLocalOpMetadata(op, this.getMapClearMessageId(), previousMap);
689
- this.submitMessage(op, metadata);
744
+ const pendingMessageId = this.getMapClearMessageId();
745
+ const localMetadata = createClearLocalOpMetadata(op, pendingMessageId, previousMap);
746
+ const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
747
+ this.submitMessage(op, listNode);
690
748
  }
691
749
 
692
750
  private getMapKeyMessageId(op: IMapKeyOperation): number {
@@ -706,12 +764,10 @@ export class MapKernel {
706
764
  * @param previousValue - The value of the key before this op
707
765
  */
708
766
  private submitMapKeyMessage(op: IMapKeyOperation, previousValue?: ILocalValue): void {
709
- const localMetadata = createKeyLocalOpMetadata(
710
- op,
711
- this.getMapKeyMessageId(op),
712
- previousValue,
713
- );
714
- this.submitMessage(op, localMetadata);
767
+ const pendingMessageId = this.getMapKeyMessageId(op);
768
+ const localMetadata = createKeyLocalOpMetadata(op, pendingMessageId, previousValue);
769
+ const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
770
+ this.submitMessage(op, listNode);
715
771
  }
716
772
 
717
773
  /**
@@ -746,10 +802,11 @@ export class MapKernel {
746
802
 
747
803
  // We don't reuse the metadata pendingMessageId but send a new one on each submit.
748
804
  const pendingMessageId = this.getMapKeyMessageId(op);
749
- const localMetadata =
805
+ const localMetadata: MapKeyLocalOpMetadata =
750
806
  localOpMetadata.type === "edit"
751
807
  ? { type: "edit", pendingMessageId, previousValue: localOpMetadata.previousValue }
752
808
  : { type: "add", pendingMessageId };
753
- this.submitMessage(op, localMetadata);
809
+ const listNode = this.pendingMapLocalOpMetadata.push(localMetadata).first;
810
+ this.submitMessage(op, listNode);
754
811
  }
755
812
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/map";
9
- export const pkgVersion = "2.43.0";
9
+ export const pkgVersion = "2.50.0";