File size: 2,300 Bytes
8a37e0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 * A Map that allows for subscribing to changes and getting a snapshot of the current state.
 *
 * It can be used with the `useSyncExternalStore` hook to sync the state of the map with a React component.
 *
 * Reactivity is shallow, so changes to nested objects will not trigger a re-render.
 */
export class SyncableMap<K, V> extends Map<K, V> {
  private subscriptions = new Set<() => void>();
  private lastSnapshot: Map<K, V> | null = null;

  constructor(entries?: readonly (readonly [K, V])[] | null) {
    super(entries);
  }

  set = (key: K, value: V): this => {
    super.set(key, value);
    this.notifySubscribers();
    return this;
  };

  delete = (key: K): boolean => {
    const result = super.delete(key);
    this.notifySubscribers();
    return result;
  };

  clear = (): void => {
    super.clear();
    this.notifySubscribers();
  };

  /**
   * Notify all subscribers that the map has changed.
   */
  private notifySubscribers = () => {
    for (const callback of this.subscriptions) {
      callback();
    }
  };

  /**
   * Subscribe to changes to the map.
   * @param callback A function to call when the map changes
   * @returns A function to unsubscribe from changes
   */
  subscribe = (callback: () => void): (() => void) => {
    this.subscriptions.add(callback);
    return () => {
      this.subscriptions.delete(callback);
    };
  };

  /**
   * Get a snapshot of the current state of the map.
   * @returns A snapshot of the current state of the map
   */
  getSnapshot = (): Map<K, V> => {
    const currentSnapshot = new Map(this);
    if (!this.lastSnapshot || !this.areSnapshotsEqual(this.lastSnapshot, currentSnapshot)) {
      this.lastSnapshot = currentSnapshot;
    }

    return this.lastSnapshot;
  };

  /**
   * Compare two snapshots to determine if they are equal.
   * @param snapshotA The first snapshot to compare
   * @param snapshotB The second snapshot to compare
   * @returns Whether the two snapshots are equal
   */
  private areSnapshotsEqual = (snapshotA: Map<K, V>, snapshotB: Map<K, V>): boolean => {
    if (snapshotA.size !== snapshotB.size) {
      return false;
    }

    for (const [key, value] of snapshotA) {
      if (!Object.is(value, snapshotB.get(key))) {
        return false;
      }
    }

    return true;
  };
}