-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathStoreManager.ts
More file actions
91 lines (80 loc) · 2.56 KB
/
Copy pathStoreManager.ts
File metadata and controls
91 lines (80 loc) · 2.56 KB
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
88
89
90
91
import Emitter from "./Emitter";
import { Store } from "./Store";
type RefCountedSubscription = {
count: number;
unsubscribe: () => void;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyStore = Store<any, never>;
type StoresSnapshot = Map<AnyStore, unknown>;
/**
* StoreManager tracks all actively rendered stores in the tree and maintains a
* reference-counted subscription to each one. This allows the <CommitTracker />
* component to observe every state update and record each store's committed
* state.
*/
export class StoreManager extends Emitter<[]> {
_storeRefCounts: Map<AnyStore, RefCountedSubscription> = new Map();
getAllCommittedStates(): StoresSnapshot {
return new Map(
Array.from(this._storeRefCounts.keys()).map((store) => [
store,
store.getCommittedState(),
]),
);
}
getAllStates(): StoresSnapshot {
return new Map(
Array.from(this._storeRefCounts.keys()).map((store) => [
store,
store.getState(),
]),
);
}
addStore(store: AnyStore) {
const prev = this._storeRefCounts.get(store);
if (prev == null) {
this._storeRefCounts.set(store, {
unsubscribe: store.subscribe(() => {
this.notify();
}),
count: 1,
});
} else {
this._storeRefCounts.set(store, { ...prev, count: prev.count + 1 });
}
}
commitAllStates(state: StoresSnapshot) {
for (const [store, committedState] of state) {
store.commit(committedState);
}
this.sweep();
}
removeStore(store: AnyStore) {
const prev = this._storeRefCounts.get(store);
if (prev == null) {
throw new Error(
"Imbalance in concurrent-safe store reference counting. This is a bug in react-use-store, please report it.",
);
}
// We decrement the count here, but don't actually do the cleanup. This is
// because a state update could cause the last store subscriber to unmount
// while also mounting a new subscriber. In this case we need to ensure we
// don't lose the currently commited state in the moment between when the
// clean-up of the unmounting component is run and the useLayoutEffect of
// the mounting component is run.
// So, we cleanup unreferenced stores after each commit.
this._storeRefCounts.set(store, {
unsubscribe: prev.unsubscribe,
count: prev.count - 1,
});
}
sweep() {
for (const [store, refs] of this._storeRefCounts) {
if (refs.count < 1) {
refs.unsubscribe();
this._storeRefCounts.delete(store);
}
}
}
}