@fluidframework/map 2.74.0 → 2.81.0-374083

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.74.0",
3
+ "version": "2.81.0-374083",
4
4
  "description": "Distributed map",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -81,45 +81,45 @@
81
81
  "temp-directory": "nyc/.nyc_output"
82
82
  },
83
83
  "dependencies": {
84
- "@fluid-internal/client-utils": "~2.74.0",
85
- "@fluidframework/core-interfaces": "~2.74.0",
86
- "@fluidframework/core-utils": "~2.74.0",
87
- "@fluidframework/datastore-definitions": "~2.74.0",
88
- "@fluidframework/driver-definitions": "~2.74.0",
89
- "@fluidframework/driver-utils": "~2.74.0",
90
- "@fluidframework/runtime-definitions": "~2.74.0",
91
- "@fluidframework/runtime-utils": "~2.74.0",
92
- "@fluidframework/shared-object-base": "~2.74.0",
93
- "@fluidframework/telemetry-utils": "~2.74.0",
84
+ "@fluid-internal/client-utils": "2.81.0-374083",
85
+ "@fluidframework/core-interfaces": "2.81.0-374083",
86
+ "@fluidframework/core-utils": "2.81.0-374083",
87
+ "@fluidframework/datastore-definitions": "2.81.0-374083",
88
+ "@fluidframework/driver-definitions": "2.81.0-374083",
89
+ "@fluidframework/driver-utils": "2.81.0-374083",
90
+ "@fluidframework/runtime-definitions": "2.81.0-374083",
91
+ "@fluidframework/runtime-utils": "2.81.0-374083",
92
+ "@fluidframework/shared-object-base": "2.81.0-374083",
93
+ "@fluidframework/telemetry-utils": "2.81.0-374083",
94
94
  "path-browserify": "^1.0.1"
95
95
  },
96
96
  "devDependencies": {
97
- "@arethetypeswrong/cli": "^0.17.1",
97
+ "@arethetypeswrong/cli": "^0.18.2",
98
98
  "@biomejs/biome": "~1.9.3",
99
- "@fluid-internal/mocha-test-setup": "~2.74.0",
100
- "@fluid-private/stochastic-test-utils": "~2.74.0",
101
- "@fluid-private/test-dds-utils": "~2.74.0",
102
- "@fluid-tools/benchmark": "^0.51.0",
103
- "@fluid-tools/build-cli": "^0.61.0",
99
+ "@fluid-internal/mocha-test-setup": "2.81.0-374083",
100
+ "@fluid-private/stochastic-test-utils": "2.81.0-374083",
101
+ "@fluid-private/test-dds-utils": "2.81.0-374083",
102
+ "@fluid-tools/benchmark": "^0.52.0",
103
+ "@fluid-tools/build-cli": "^0.63.0",
104
104
  "@fluidframework/build-common": "^2.0.3",
105
- "@fluidframework/build-tools": "^0.61.0",
106
- "@fluidframework/container-definitions": "~2.74.0",
107
- "@fluidframework/eslint-config-fluid": "~2.74.0",
108
- "@fluidframework/map-previous": "npm:@fluidframework/map@2.73.0",
109
- "@fluidframework/test-runtime-utils": "~2.74.0",
105
+ "@fluidframework/build-tools": "^0.63.0",
106
+ "@fluidframework/container-definitions": "2.81.0-374083",
107
+ "@fluidframework/eslint-config-fluid": "2.81.0-374083",
108
+ "@fluidframework/map-previous": "npm:@fluidframework/map@2.80.0",
109
+ "@fluidframework/test-runtime-utils": "2.81.0-374083",
110
110
  "@microsoft/api-extractor": "7.52.11",
111
111
  "@types/mocha": "^10.0.10",
112
112
  "@types/node": "^18.19.0",
113
113
  "@types/path-browserify": "^1.0.0",
114
114
  "c8": "^10.1.3",
115
- "concurrently": "^8.2.1",
115
+ "concurrently": "^9.2.1",
116
116
  "copyfiles": "^2.4.1",
117
- "cross-env": "^7.0.3",
118
- "eslint": "~8.57.1",
117
+ "cross-env": "^10.1.0",
118
+ "eslint": "~9.39.1",
119
119
  "jiti": "^2.6.1",
120
120
  "mocha": "^10.8.2",
121
121
  "mocha-multi-reporters": "^1.5.1",
122
- "rimraf": "^4.4.0",
122
+ "rimraf": "^6.1.2",
123
123
  "typescript": "~5.4.5"
124
124
  },
125
125
  "typeValidation": {
package/src/directory.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ // TODO: Fix prefer-nullish-coalescing and prefer-optional-chain lint violations
7
+ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/prefer-optional-chain */
8
+
6
9
  import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
10
  import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
8
11
  import type {
@@ -40,7 +43,7 @@ import type {
40
43
  IDirectoryEvents,
41
44
  IDirectoryValueChanged,
42
45
  ISharedDirectory,
43
- ISharedDirectoryEventsInternal,
46
+ ISharedDirectoryEvents,
44
47
  IValueChanged,
45
48
  } from "./interfaces.js";
46
49
  import type {
@@ -402,7 +405,7 @@ interface SequenceData {
402
405
  * @sealed
403
406
  */
404
407
  export class SharedDirectory
405
- extends SharedObject<ISharedDirectoryEventsInternal>
408
+ extends SharedObject<ISharedDirectoryEvents>
406
409
  implements ISharedDirectory
407
410
  {
408
411
  /**
@@ -525,7 +528,7 @@ export class SharedDirectory
525
528
  // TODO: Use `unknown` instead (breaking change).
526
529
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
527
530
  public forEach(callback: (value: any, key: string, map: Map<string, any>) => void): void {
528
- // eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
531
+ // eslint-disable-next-line unicorn/no-array-for-each
529
532
  this.root.forEach(callback);
530
533
  }
531
534
 
@@ -1512,7 +1515,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1512
1515
  if (!this.directory.isAttached()) {
1513
1516
  const successfullyRemoved = this.sequencedStorageData.delete(key);
1514
1517
  // Only emit if we actually deleted something.
1515
- if (previousOptimisticLocalValue !== undefined && successfullyRemoved) {
1518
+ if (
1519
+ this.isNotDisposedAndReachable() &&
1520
+ previousOptimisticLocalValue !== undefined &&
1521
+ successfullyRemoved
1522
+ ) {
1516
1523
  const event: IDirectoryValueChanged = {
1517
1524
  key,
1518
1525
  path: this.absolutePath,
@@ -1545,7 +1552,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1545
1552
  // Only emit if we locally believe we deleted something. Otherwise we still send the op
1546
1553
  // (permitting speculative deletion even if we don't see anything locally) but don't emit
1547
1554
  // a valueChanged since we in fact did not locally observe a value change.
1548
- if (previousOptimisticLocalValue !== undefined) {
1555
+ if (this.isNotDisposedAndReachable() && previousOptimisticLocalValue !== undefined) {
1549
1556
  const event: IDirectoryValueChanged = {
1550
1557
  key,
1551
1558
  path: this.absolutePath,
@@ -1570,7 +1577,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1570
1577
  if (!this.directory.isAttached()) {
1571
1578
  this.sequencedStorageData.clear();
1572
1579
  this.directory.emit("clear", true, this.directory);
1573
- this.directory.emit("clearInternal", this.absolutePath, true, this.directory);
1580
+ this.directory.emit("cleared", this.absolutePath, true, this.directory);
1574
1581
  return;
1575
1582
  }
1576
1583
 
@@ -1582,7 +1589,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1582
1589
  this.pendingStorageData.push(pendingClear);
1583
1590
 
1584
1591
  this.directory.emit("clear", true, this.directory);
1585
- this.directory.emit("clearInternal", this.absolutePath, true, this.directory);
1592
+ this.directory.emit("cleared", this.absolutePath, true, this.directory);
1586
1593
  const op: IDirectoryOperation = {
1587
1594
  type: "clear",
1588
1595
  path: this.absolutePath,
@@ -1859,6 +1866,29 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1859
1866
  return subdir;
1860
1867
  };
1861
1868
 
1869
+ /**
1870
+ * Checks if this directory should be considered visible in the optimistic view.
1871
+ * This requires both:
1872
+ * 1. The directory object must not be disposed (disposed = true means fully deleted)
1873
+ * 2. The directory must be reachable via getWorkingDirectory (respects pending deletes)
1874
+ *
1875
+ * There's a timing window where a directory has a pending delete but is not yet disposed:
1876
+ * - When deleteSubDirectory is called locally, it adds a pending delete entry and emits a
1877
+ * "dispose" event, but doesn't set disposed = true yet.
1878
+ * - The directory only gets fully disposed when the delete message is sequenced.
1879
+ * - During this window, !disposed is true but getWorkingDirectory returns undefined.
1880
+ *
1881
+ * This method should be used before emitting events during remote message processing to ensure
1882
+ * events aren't emitted for directories that are invisible in the optimistic view.
1883
+ *
1884
+ * @returns true if this directory is visible in the optimistic view, false otherwise
1885
+ */
1886
+ private isNotDisposedAndReachable(): boolean {
1887
+ return (
1888
+ !this.disposed && this.directory.getWorkingDirectory(this.absolutePath) !== undefined
1889
+ );
1890
+ }
1891
+
1862
1892
  public get sequencedSubdirectories(): ReadonlyMap<string, SubDirectory> {
1863
1893
  this.throwIfDisposed();
1864
1894
  return this._sequencedSubdirectories;
@@ -1907,14 +1937,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1907
1937
 
1908
1938
  // Only emit for remote ops, we would have already emitted for local ops. Only emit if there
1909
1939
  // is no optimistically-applied local pending clear that would supersede this remote clear.
1940
+ // Don't emit events if this directory has been disposed or no longer exists
1910
1941
  if (!this.pendingStorageData.some((entry) => entry.type === "clear")) {
1911
1942
  this.directory.emit("clear", local, this.directory);
1912
- this.directory.emit("clearInternal", this.absolutePath, local, this.directory);
1943
+ this.directory.emit("cleared", this.absolutePath, local, this.directory);
1913
1944
  }
1914
1945
 
1915
1946
  // For pending set operations, emit valueChanged events
1916
1947
  // Include 'path' so listeners can identify which subdirectory the change occurred in
1917
1948
  for (const { key, previousValue } of pendingSets) {
1949
+ // Stop emitting events if this directory has been disposed or is no longer reachable
1950
+ if (!this.isNotDisposedAndReachable()) {
1951
+ break;
1952
+ }
1918
1953
  this.directory.emit(
1919
1954
  "valueChanged",
1920
1955
  {
@@ -1967,6 +2002,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1967
2002
  this.sequencedStorageData.delete(op.key);
1968
2003
  // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1969
2004
  if (
2005
+ this.isNotDisposedAndReachable() &&
1970
2006
  !this.pendingStorageData.some(
1971
2007
  (entry) => entry.type === "clear" || entry.key === op.key,
1972
2008
  )
@@ -2032,6 +2068,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2032
2068
 
2033
2069
  // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
2034
2070
  if (
2071
+ this.isNotDisposedAndReachable() &&
2035
2072
  !this.pendingStorageData.some((entry) => entry.type === "clear" || entry.key === key)
2036
2073
  ) {
2037
2074
  const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
@@ -2121,7 +2158,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2121
2158
  this._sequencedSubdirectories.set(op.subdirName, subDir);
2122
2159
 
2123
2160
  // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
2124
- if (!this.pendingSubDirectoryData.some((entry) => entry.subdirName === op.subdirName)) {
2161
+ if (
2162
+ !this.pendingSubDirectoryData.some((entry) => entry.subdirName === op.subdirName) &&
2163
+ subDir.isNotDisposedAndReachable()
2164
+ ) {
2125
2165
  this.emit("subDirectoryCreated", op.subdirName, local, this);
2126
2166
  }
2127
2167
  }
package/src/interfaces.ts CHANGED
@@ -153,6 +153,8 @@ export interface ISharedDirectoryEvents extends ISharedObjectEvents {
153
153
  /**
154
154
  * Emitted when the {@link ISharedDirectory} is cleared.
155
155
  *
156
+ * @deprecated Use the "cleared" event instead which provides the path that was cleared.
157
+ *
156
158
  * @remarks Listener parameters:
157
159
  *
158
160
  * - `local` - Whether the clear originated from this client.
@@ -161,6 +163,22 @@ export interface ISharedDirectoryEvents extends ISharedObjectEvents {
161
163
  */
162
164
  (event: "clear", listener: (local: boolean, target: IEventThisPlaceHolder) => void);
163
165
 
166
+ /**
167
+ * Emitted when the {@link ISharedDirectory} is cleared.
168
+ *
169
+ * @remarks Listener parameters:
170
+ *
171
+ * - `path` - The absolute path to the directory that was cleared.
172
+ *
173
+ * - `local` - Whether the clear originated from this client.
174
+ *
175
+ * - `target` - The {@link ISharedDirectory} itself.
176
+ */
177
+ (
178
+ event: "cleared",
179
+ listener: (path: string, local: boolean, target: IEventThisPlaceHolder) => void,
180
+ );
181
+
164
182
  /**
165
183
  * Emitted when a subdirectory is created.
166
184
  *
@@ -277,28 +295,6 @@ export interface IDirectoryEvents extends IEvent {
277
295
  (event: "undisposed", listener: (target: IEventThisPlaceHolder) => void);
278
296
  }
279
297
 
280
- /**
281
- * Internal events for {@link ISharedDirectory}.
282
- * @internal
283
- */
284
- export interface ISharedDirectoryEventsInternal extends ISharedDirectoryEvents {
285
- /**
286
- * Emitted when the {@link ISharedDirectory} is cleared.
287
- *
288
- * @remarks Listener parameters:
289
- *
290
- * - `path` - The absolute path to the directory that was cleared.
291
- *
292
- * - `local` - Whether the clear originated from this client.
293
- *
294
- * - `target` - The {@link ISharedDirectory} itself.
295
- */
296
- (
297
- event: "clearInternal",
298
- listener: (path: string, local: boolean, target: IEventThisPlaceHolder) => void,
299
- );
300
- }
301
-
302
298
  /**
303
299
  * Provides a hierarchical organization of map-like data structures as SubDirectories.
304
300
  * The values stored within can be accessed like a map, and the hierarchy can be navigated using path syntax.
package/src/map.ts CHANGED
@@ -124,7 +124,7 @@ export class SharedMap extends SharedObject<ISharedMapEvents> implements IShared
124
124
  // TODO: Use `unknown` instead (breaking change).
125
125
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
126
  public forEach(callbackFn: (value: any, key: string, map: Map<string, any>) => void): void {
127
- // eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
127
+ // eslint-disable-next-line unicorn/no-array-for-each
128
128
  this.kernel.forEach(callbackFn);
129
129
  }
130
130
 
package/src/mapKernel.ts CHANGED
@@ -495,11 +495,32 @@ export class MapKernel {
495
495
  */
496
496
  public clear(): void {
497
497
  if (!this.isAttached()) {
498
+ // Collect keys to delete before clearing
499
+ // eslint-disable-next-line @typescript-eslint/no-shadow
500
+ const keysToDelete: { key: string; previousValue: unknown }[] = [];
501
+ for (const [key, value] of this.sequencedData) {
502
+ keysToDelete.push({ key, previousValue: value.value });
503
+ }
498
504
  this.sequencedData.clear();
499
505
  this.eventEmitter.emit("clear", true, this.eventEmitter);
506
+ // Emit delete-like valueChanged events for keys that were removed
507
+ for (const { key, previousValue } of keysToDelete) {
508
+ this.eventEmitter.emit(
509
+ "valueChanged",
510
+ { key, previousValue },
511
+ true,
512
+ this.eventEmitter,
513
+ );
514
+ }
500
515
  return;
501
516
  }
502
517
 
518
+ // Collect keys that will be deleted (those without pending ops that supersede the clear)
519
+ const keysToDelete: { key: string; previousValue: unknown }[] = [];
520
+ for (const [key, value] of this.internalIterator()) {
521
+ keysToDelete.push({ key, previousValue: value.value });
522
+ }
523
+
503
524
  const pendingClear: PendingClear = {
504
525
  type: "clear",
505
526
  };
@@ -510,6 +531,10 @@ export class MapKernel {
510
531
  };
511
532
  this.submitMessage(op, pendingClear);
512
533
  this.eventEmitter.emit("clear", true, this.eventEmitter);
534
+ // Emit delete-like valueChanged events for keys that were removed
535
+ for (const { key, previousValue } of keysToDelete) {
536
+ this.eventEmitter.emit("valueChanged", { key, previousValue }, true, this.eventEmitter);
537
+ }
513
538
  }
514
539
 
515
540
  /**
@@ -616,6 +641,7 @@ export class MapKernel {
616
641
  // A pending clear will be last in the list, since it terminates all prior lifetimes.
617
642
  const pendingClear = this.pendingData.pop();
618
643
  assert(
644
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- using ?. could change behavior
619
645
  pendingClear !== undefined &&
620
646
  pendingClear.type === "clear" &&
621
647
  pendingClear === typedLocalOpMetadata,
@@ -689,6 +715,7 @@ export class MapKernel {
689
715
  this.sequencedData.clear();
690
716
  const pendingClear = this.pendingData.shift();
691
717
  assert(
718
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- using ?. could change behavior
692
719
  pendingClear !== undefined &&
693
720
  pendingClear.type === "clear" &&
694
721
  pendingClear === localOpMetadata,
@@ -745,6 +772,7 @@ export class MapKernel {
745
772
  );
746
773
  const pendingEntry = this.pendingData[pendingEntryIndex];
747
774
  assert(
775
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- using ?. could change behavior
748
776
  pendingEntry !== undefined &&
749
777
  pendingEntry.type === "delete" &&
750
778
  pendingEntry === localOpMetadata,
@@ -785,6 +813,7 @@ export class MapKernel {
785
813
  );
786
814
  const pendingEntry = this.pendingData[pendingEntryIndex];
787
815
  assert(
816
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- using ?. could change behavior
788
817
  pendingEntry !== undefined && pendingEntry.type === "lifetime",
789
818
  0xbf8 /* Couldn't match local set message to pending lifetime */,
790
819
  );
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/map";
9
- export const pkgVersion = "2.74.0";
9
+ export const pkgVersion = "2.81.0-374083";
package/.eslintrc.cjs DELETED
@@ -1,28 +0,0 @@
1
- /*!
2
- * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
- * Licensed under the MIT License.
4
- */
5
-
6
- module.exports = {
7
- extends: [require.resolve("@fluidframework/eslint-config-fluid/strict"), "prettier"],
8
- parserOptions: {
9
- project: ["./tsconfig.json", "./src/test/tsconfig.json"],
10
- },
11
- rules: {
12
- "@typescript-eslint/no-use-before-define": "off",
13
- "@typescript-eslint/strict-boolean-expressions": "off",
14
-
15
- // TODO: consider re-enabling once we have addressed how this rule conflicts with our error codes.
16
- "unicorn/numeric-separators-style": "off",
17
- "@fluid-internal/fluid/no-unchecked-record-access": "warn",
18
- },
19
- overrides: [
20
- {
21
- files: ["src/test/**"],
22
- rules: {
23
- // Allow tests (which only run in Node.js) use `__dirname`
24
- "unicorn/prefer-module": "off",
25
- },
26
- },
27
- ],
28
- };