@evolu/common 8.0.0-next.2 → 8.0.0-next.4
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/dist/src/Assert.d.ts +5 -5
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +4 -4
- package/dist/src/Identicon.js +4 -4
- package/dist/src/LockManager.d.ts +74 -0
- package/dist/src/LockManager.d.ts.map +1 -0
- package/dist/src/LockManager.js +103 -0
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +90 -26
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +153 -68
- package/dist/src/Resource.d.ts +6 -6
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +211 -162
- package/dist/src/Schedule.d.ts +5 -5
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +53 -44
- package/dist/src/Sqlite.d.ts +2 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +64 -40
- package/dist/src/Task.d.ts +6 -33
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +213 -204
- package/dist/src/Test.d.ts +4 -4
- package/dist/src/Time.d.ts +5 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +5 -0
- package/dist/src/Type.d.ts +7 -0
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +7 -0
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +7 -6
- package/dist/src/Worker.d.ts +42 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +298 -75
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/local-first/Db.d.ts +5 -4
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +44 -38
- package/dist/src/local-first/Evolu.d.ts +3 -2
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +90 -80
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +65 -54
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +387 -373
- package/package.json +1 -1
- package/src/Array.ts +8 -8
- package/src/Assert.ts +5 -5
- package/src/Identicon.ts +4 -4
- package/src/LockManager.ts +181 -0
- package/src/Microtask.ts +17 -11
- package/src/Object.ts +3 -3
- package/src/RefCount.ts +25 -21
- package/src/Resource.ts +63 -56
- package/src/Schedule.ts +70 -48
- package/src/Set.ts +4 -4
- package/src/Sqlite.ts +41 -30
- package/src/Task.ts +77 -120
- package/src/Test.ts +4 -4
- package/src/Time.ts +7 -0
- package/src/Type.ts +9 -0
- package/src/WebSocket.ts +7 -6
- package/src/Worker.ts +240 -49
- package/src/index.ts +1 -0
- package/src/local-first/Db.ts +69 -46
- package/src/local-first/Evolu.ts +74 -68
- package/src/local-first/Owner.ts +1 -1
- package/src/local-first/Query.ts +1 -1
- package/src/local-first/Shared.ts +472 -464
package/package.json
CHANGED
package/src/Array.ts
CHANGED
|
@@ -326,7 +326,7 @@ export function mapArray<T, U>(
|
|
|
326
326
|
const length = array.length;
|
|
327
327
|
const result = new Array<U>(length);
|
|
328
328
|
for (let i = 0; i < length; i++) result[i] = mapper(array[i], i);
|
|
329
|
-
return result
|
|
329
|
+
return result;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
/**
|
|
@@ -394,7 +394,7 @@ export function flatMapArray<T, U>(
|
|
|
394
394
|
index: number,
|
|
395
395
|
) => ReadonlyArray<U> | Array<U>,
|
|
396
396
|
): ReadonlyArray<U> {
|
|
397
|
-
return array.flatMap(mapper)
|
|
397
|
+
return array.flatMap(mapper);
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
/**
|
|
@@ -475,7 +475,7 @@ export function filterArray<T>(
|
|
|
475
475
|
array: ReadonlyArray<T>,
|
|
476
476
|
predicate: PredicateWithIndex<T>,
|
|
477
477
|
): ReadonlyArray<T> {
|
|
478
|
-
return array.filter(predicate)
|
|
478
|
+
return array.filter(predicate);
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
/**
|
|
@@ -519,7 +519,7 @@ export function dedupeArray<T>(
|
|
|
519
519
|
by?: (item: T) => unknown,
|
|
520
520
|
): ReadonlyArray<T> {
|
|
521
521
|
if (by == null) {
|
|
522
|
-
return Array.from(new Set(array))
|
|
522
|
+
return Array.from(new Set(array));
|
|
523
523
|
}
|
|
524
524
|
|
|
525
525
|
const seen = new Set<unknown>();
|
|
@@ -528,7 +528,7 @@ export function dedupeArray<T>(
|
|
|
528
528
|
if (seen.has(key)) return false;
|
|
529
529
|
seen.add(key);
|
|
530
530
|
return true;
|
|
531
|
-
})
|
|
531
|
+
});
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
/**
|
|
@@ -593,7 +593,7 @@ export function partitionArray<T>(
|
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
return [trueArray
|
|
596
|
+
return [trueArray, falseArray];
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
/**
|
|
@@ -622,7 +622,7 @@ export function sortArray<T>(
|
|
|
622
622
|
array: ReadonlyArray<T>,
|
|
623
623
|
compareFn?: (a: T, b: T) => number,
|
|
624
624
|
): ReadonlyArray<T> {
|
|
625
|
-
return array.toSorted(compareFn)
|
|
625
|
+
return array.toSorted(compareFn);
|
|
626
626
|
}
|
|
627
627
|
|
|
628
628
|
/**
|
|
@@ -644,7 +644,7 @@ export function reverseArray<T>(
|
|
|
644
644
|
/** Possibly empty array. */
|
|
645
645
|
export function reverseArray<T>(array: ReadonlyArray<T>): ReadonlyArray<T>;
|
|
646
646
|
export function reverseArray<T>(array: ReadonlyArray<T>): ReadonlyArray<T> {
|
|
647
|
-
return array.toReversed()
|
|
647
|
+
return array.toReversed();
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
/**
|
package/src/Assert.ts
CHANGED
|
@@ -146,14 +146,14 @@ export function assertNotAborted<T, E>(
|
|
|
146
146
|
* ### Example
|
|
147
147
|
*
|
|
148
148
|
* ```ts
|
|
149
|
-
*
|
|
150
|
-
* assertNotDisposed(
|
|
151
|
-
* await
|
|
152
|
-
* assertNotDisposed(
|
|
149
|
+
* using disposer = new globalThis.AsyncDisposableStack();
|
|
150
|
+
* assertNotDisposed(disposer); // no-op
|
|
151
|
+
* await disposer.disposeAsync();
|
|
152
|
+
* assertNotDisposed(disposer); // throws Error
|
|
153
153
|
* ```
|
|
154
154
|
*/
|
|
155
155
|
export const assertNotDisposed = (
|
|
156
|
-
value:
|
|
156
|
+
value: DisposableStack | AsyncDisposableStack,
|
|
157
157
|
): void => {
|
|
158
158
|
assert(!value.disposed, "Expected value to not be disposed.");
|
|
159
159
|
};
|
package/src/Identicon.ts
CHANGED
|
@@ -147,8 +147,8 @@ export const createIdenticon = (
|
|
|
147
147
|
case "sutnar": {
|
|
148
148
|
// Three compositional variants with adaptive colors.
|
|
149
149
|
const hue = (bytes[0] / 255) * 360;
|
|
150
|
-
const saturation =
|
|
151
|
-
const lightness =
|
|
150
|
+
const saturation = 60 + (bytes[1] / 255) * 30;
|
|
151
|
+
const lightness = 35 + (bytes[2] / 255) * 20;
|
|
152
152
|
|
|
153
153
|
// Generate palette from base hue with variations
|
|
154
154
|
const toHsl = (h: number, s: number, l: number) =>
|
|
@@ -157,11 +157,11 @@ export const createIdenticon = (
|
|
|
157
157
|
const color1 = toHsl(hue, saturation, lightness);
|
|
158
158
|
const color2 = toHsl((hue + 120) % 360, saturation, lightness);
|
|
159
159
|
const color3 = toHsl((hue + 240) % 360, saturation, lightness);
|
|
160
|
-
const color4 = toHsl(hue, saturation * 0.
|
|
160
|
+
const color4 = toHsl(hue, saturation * 0.5, lightness * 0.6);
|
|
161
161
|
const color5 = toHsl(
|
|
162
162
|
hue,
|
|
163
163
|
saturation * 0.5,
|
|
164
|
-
Math.min(lightness * 1.3,
|
|
164
|
+
Math.min(lightness * 1.3, 60),
|
|
165
165
|
);
|
|
166
166
|
|
|
167
167
|
const palette = [color1, color2, color3, color4, color5] as const;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link LockManager} helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRandomBytes } from "./Crypto.js";
|
|
8
|
+
import { lazyVoid } from "./Function.js";
|
|
9
|
+
import { tryAsync } from "./Result.js";
|
|
10
|
+
import { AbortError, type Task } from "./Task.js";
|
|
11
|
+
import { createId } from "./Type.js";
|
|
12
|
+
import type { Callback } from "./Types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dependency wrapper for {@link LockManager}.
|
|
16
|
+
*
|
|
17
|
+
* For React Native, use the `lockManager` ponyfill from `@evolu/react-native`.
|
|
18
|
+
* For tests, use {@link testCreateLockManager}.
|
|
19
|
+
*
|
|
20
|
+
* ### Example
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* // Web or Node.js 24+
|
|
24
|
+
* import { testCreateLockManager } from "@evolu/common";
|
|
25
|
+
*
|
|
26
|
+
* const deps: LockManagerDep = {
|
|
27
|
+
* lockManager: testCreateLockManager(),
|
|
28
|
+
* };
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* ### Example
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* // React Native
|
|
35
|
+
* import { lockManager } from "@evolu/react-native";
|
|
36
|
+
*
|
|
37
|
+
* const deps: LockManagerDep = { lockManager };
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export interface LockManagerDep {
|
|
42
|
+
readonly lockManager: LockManager;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a test {@link LockManager} backed by the native platform
|
|
47
|
+
* {@link LockManager}.
|
|
48
|
+
*
|
|
49
|
+
* Native backing is intentional: tests must observe the same locking behavior
|
|
50
|
+
* as the platform runtime, and a userland implementation cannot reproduce it.
|
|
51
|
+
*
|
|
52
|
+
* The platform `LockManager` cannot be instantiated, so tests cannot create an
|
|
53
|
+
* isolated native lock per case. This helper isolates lock usage per instance
|
|
54
|
+
* via internal namespacing, so tests can reuse the same lock names without
|
|
55
|
+
* contending through the global Web Locks. Query results are filtered to that
|
|
56
|
+
* private namespace and returned with the original visible names.
|
|
57
|
+
*/
|
|
58
|
+
export const testCreateLockManager = (
|
|
59
|
+
nativeLockManager: LockManager = globalThis.navigator.locks,
|
|
60
|
+
): LockManager => {
|
|
61
|
+
const namespace = createId({ randomBytes: createRandomBytes() });
|
|
62
|
+
|
|
63
|
+
const filterLockInfosByNamespace = <T extends LockInfo>(
|
|
64
|
+
locks: ReadonlyArray<T>,
|
|
65
|
+
): Array<T> =>
|
|
66
|
+
locks.flatMap((lock) =>
|
|
67
|
+
lock.name?.endsWith(namespace) === true
|
|
68
|
+
? [{ ...lock, name: lock.name.slice(0, -namespace.length) }]
|
|
69
|
+
: [],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
request: <T>(
|
|
74
|
+
name: string,
|
|
75
|
+
optionsOrCallback: LockOptions | LockGrantedCallback<T>,
|
|
76
|
+
maybeCallback?: LockGrantedCallback<T>,
|
|
77
|
+
): Promise<Awaited<T>> => {
|
|
78
|
+
const [options, callback] =
|
|
79
|
+
typeof optionsOrCallback === "function"
|
|
80
|
+
? [{}, optionsOrCallback]
|
|
81
|
+
: [optionsOrCallback, maybeCallback!];
|
|
82
|
+
|
|
83
|
+
return nativeLockManager.request(`${name}${namespace}`, options, (lock) =>
|
|
84
|
+
callback(lock && { mode: lock.mode, name }),
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
query: async () => {
|
|
89
|
+
const locks = await nativeLockManager.query();
|
|
90
|
+
return {
|
|
91
|
+
...(locks.held && {
|
|
92
|
+
held: filterLockInfosByNamespace(locks.held),
|
|
93
|
+
}),
|
|
94
|
+
...(locks.pending && {
|
|
95
|
+
pending: filterLockInfosByNamespace(locks.pending),
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Competes to become the current leader for `name` using {@link LockManager}.
|
|
104
|
+
*
|
|
105
|
+
* Only one caller can lead for a given name at a time, and the returned
|
|
106
|
+
* {@link AsyncDisposable} represents that leadership.
|
|
107
|
+
*
|
|
108
|
+
* Leadership is held until the returned handle is disposed. Once released,
|
|
109
|
+
* another waiting caller may become the next leader. Waiting for leadership is
|
|
110
|
+
* abortable via the calling {@link Task}'s signal.
|
|
111
|
+
*/
|
|
112
|
+
export const acquireLeaderLock =
|
|
113
|
+
(name: string): Task<AsyncDisposable, AbortError, LockManagerDep> =>
|
|
114
|
+
(run) =>
|
|
115
|
+
tryAsync(
|
|
116
|
+
async () => {
|
|
117
|
+
const acquisition = Promise.withResolvers<void>();
|
|
118
|
+
const released = Promise.withResolvers<void>();
|
|
119
|
+
|
|
120
|
+
const request = run.deps.lockManager.request(
|
|
121
|
+
createLeaderLockName(name),
|
|
122
|
+
{ mode: "exclusive", signal: run.signal },
|
|
123
|
+
async () => {
|
|
124
|
+
acquisition.resolve();
|
|
125
|
+
await released.promise;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
void request.catch(acquisition.reject);
|
|
129
|
+
|
|
130
|
+
await acquisition.promise;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
[Symbol.asyncDispose]: async () => {
|
|
134
|
+
released.resolve();
|
|
135
|
+
await request;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
(error): AbortError =>
|
|
140
|
+
AbortError.is(error) ? error : { type: "AbortError", reason: error },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Competes to become the current leader for `name` using {@link LockManager}.
|
|
145
|
+
*
|
|
146
|
+
* Only one caller can lead for a given name at a time. The callback runs when
|
|
147
|
+
* leadership is acquired, and the returned {@link Disposable} represents that
|
|
148
|
+
* leadership request.
|
|
149
|
+
*
|
|
150
|
+
* Leadership is held until the returned handle is disposed. Once released,
|
|
151
|
+
* another waiting caller may become the next leader. Waiting for leadership is
|
|
152
|
+
* abortable by disposing the returned handle.
|
|
153
|
+
*/
|
|
154
|
+
export const acquireLeaderLockCallback =
|
|
155
|
+
(deps: LockManagerDep) =>
|
|
156
|
+
(name: string, callback: Callback<void>): Disposable => {
|
|
157
|
+
const abortController = new AbortController();
|
|
158
|
+
let release: (() => void) | null = null;
|
|
159
|
+
|
|
160
|
+
const request = deps.lockManager.request(
|
|
161
|
+
createLeaderLockName(name),
|
|
162
|
+
{ mode: "exclusive", signal: abortController.signal },
|
|
163
|
+
async () => {
|
|
164
|
+
const released = Promise.withResolvers<void>();
|
|
165
|
+
release = released.resolve;
|
|
166
|
+
callback();
|
|
167
|
+
await released.promise;
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
void request.catch(lazyVoid);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
[Symbol.dispose]: () => {
|
|
174
|
+
abortController.abort();
|
|
175
|
+
release?.();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const createLeaderLockName = (name: string): string =>
|
|
181
|
+
`evolu-leaderlock-${name}`;
|
package/src/Microtask.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { appendToArray, type NonEmptyReadonlyArray } from "./Array.js";
|
|
8
|
+
import { assertNotDisposed } from "./Assert.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Batches values and flushes them in a single microtask.
|
|
@@ -24,11 +25,17 @@ export interface MicrotaskBatch<T> extends Disposable {
|
|
|
24
25
|
export const createMicrotaskBatch = <T>(
|
|
25
26
|
onFlush: (items: NonEmptyReadonlyArray<T>) => void,
|
|
26
27
|
): MicrotaskBatch<T> => {
|
|
28
|
+
using disposer = new DisposableStack();
|
|
27
29
|
let queue: NonEmptyReadonlyArray<T> | null = null;
|
|
28
|
-
let disposed = false;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
disposer.defer(() => {
|
|
32
|
+
queue = null;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const disposables = disposer.move();
|
|
36
|
+
|
|
37
|
+
const flushQueuedItems = () => {
|
|
38
|
+
if (queue == null) return;
|
|
32
39
|
const queuedItems = queue;
|
|
33
40
|
queue = null;
|
|
34
41
|
onFlush(queuedItems);
|
|
@@ -36,23 +43,22 @@ export const createMicrotaskBatch = <T>(
|
|
|
36
43
|
|
|
37
44
|
return {
|
|
38
45
|
push: (item) => {
|
|
39
|
-
|
|
46
|
+
assertNotDisposed(disposables);
|
|
40
47
|
|
|
41
48
|
if (queue == null) {
|
|
42
49
|
queue = [item];
|
|
43
|
-
queueMicrotask(
|
|
50
|
+
queueMicrotask(flushQueuedItems);
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
queue = appendToArray(queue, item);
|
|
48
55
|
},
|
|
49
56
|
|
|
50
|
-
flushNow
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (disposed) return;
|
|
54
|
-
disposed = true;
|
|
55
|
-
queue = null;
|
|
57
|
+
flushNow: () => {
|
|
58
|
+
assertNotDisposed(disposables);
|
|
59
|
+
flushQueuedItems();
|
|
56
60
|
},
|
|
61
|
+
|
|
62
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
57
63
|
};
|
|
58
64
|
};
|
package/src/Object.ts
CHANGED
|
@@ -89,7 +89,7 @@ export const objectToEntries = <T extends Record<string, any>>(
|
|
|
89
89
|
): ReadonlyArray<[StringKeyOf<T>, T[StringKeyOf<T>]]> =>
|
|
90
90
|
Object.entries(record) as Array<
|
|
91
91
|
[StringKeyOf<T>, T[StringKeyOf<T>]]
|
|
92
|
-
|
|
92
|
+
>;
|
|
93
93
|
|
|
94
94
|
// A helper type to remove symbol keys (e.g for branded objects).
|
|
95
95
|
type StringKeyOf<T> = Extract<keyof T, string>;
|
|
@@ -148,10 +148,10 @@ export const mapObject = <K extends string, V, U>(
|
|
|
148
148
|
const out = Object.create(null) as Record<K, U>;
|
|
149
149
|
|
|
150
150
|
for (const key in record) {
|
|
151
|
-
out[key as K] = fn(record[key as K], key
|
|
151
|
+
out[key as K] = fn(record[key as K], key);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
return out
|
|
154
|
+
return out;
|
|
155
155
|
};
|
|
156
156
|
|
|
157
157
|
/** Conditionally excludes a property from an object. */
|
package/src/RefCount.ts
CHANGED
|
@@ -34,34 +34,32 @@ export interface RefCount extends Disposable {
|
|
|
34
34
|
|
|
35
35
|
/** Creates {@link RefCount}. */
|
|
36
36
|
export const createRefCount = (): RefCount => {
|
|
37
|
+
// We use DisposableStack only because of assertNotDisposed.
|
|
38
|
+
using disposer = new DisposableStack();
|
|
37
39
|
let count = zeroNonNegativeInt;
|
|
38
|
-
const
|
|
39
|
-
stack.defer(() => {
|
|
40
|
-
count = zeroNonNegativeInt;
|
|
41
|
-
});
|
|
42
|
-
const moved = stack.move();
|
|
40
|
+
const disposables = disposer.move();
|
|
43
41
|
|
|
44
42
|
return {
|
|
45
43
|
increment: () => {
|
|
46
|
-
assertNotDisposed(
|
|
44
|
+
assertNotDisposed(disposables);
|
|
47
45
|
const nextCount = PositiveInt.orThrow(count + 1);
|
|
48
46
|
count = nextCount;
|
|
49
47
|
return nextCount;
|
|
50
48
|
},
|
|
51
49
|
|
|
52
50
|
decrement: () => {
|
|
53
|
-
assertNotDisposed(
|
|
51
|
+
assertNotDisposed(disposables);
|
|
54
52
|
assert(count > 0, "RefCount must not be decremented below zero.");
|
|
55
53
|
count = NonNegativeInt.orThrow(count - 1);
|
|
56
54
|
return count;
|
|
57
55
|
},
|
|
58
56
|
|
|
59
57
|
getCount: () => {
|
|
60
|
-
assertNotDisposed(
|
|
58
|
+
assertNotDisposed(disposables);
|
|
61
59
|
return count;
|
|
62
60
|
},
|
|
63
61
|
|
|
64
|
-
[Symbol.dispose]: () =>
|
|
62
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
65
63
|
};
|
|
66
64
|
};
|
|
67
65
|
|
|
@@ -112,17 +110,23 @@ export function createRefCountByKey<TKey, L>(
|
|
|
112
110
|
export function createRefCountByKey<TKey, L = TKey>({
|
|
113
111
|
lookup = identity as Lookup<TKey, L>,
|
|
114
112
|
}: CreateRefCountByKeyOptions<TKey, L> = {}): RefCountByKey<TKey> {
|
|
115
|
-
|
|
113
|
+
// We use DisposableStack only because of assertNotDisposed.
|
|
114
|
+
using disposer = new DisposableStack();
|
|
116
115
|
|
|
117
|
-
const refCountByKey =
|
|
116
|
+
const refCountByKey = disposer.adopt(
|
|
118
117
|
createLookupMap<TKey, RefCount, L>({ lookup }),
|
|
119
118
|
(refCountByKey) => {
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
using disposer = new DisposableStack();
|
|
120
|
+
disposer.defer(() => {
|
|
121
|
+
refCountByKey.clear();
|
|
122
|
+
});
|
|
123
|
+
for (const refCount of refCountByKey.values()) {
|
|
124
|
+
disposer.use(refCount);
|
|
125
|
+
}
|
|
122
126
|
},
|
|
123
127
|
);
|
|
124
128
|
|
|
125
|
-
const
|
|
129
|
+
const disposables = disposer.move();
|
|
126
130
|
|
|
127
131
|
const getRefCount = (key: TKey): RefCount => {
|
|
128
132
|
let refCount = refCountByKey.get(key);
|
|
@@ -135,36 +139,36 @@ export function createRefCountByKey<TKey, L = TKey>({
|
|
|
135
139
|
|
|
136
140
|
return {
|
|
137
141
|
increment: (key) => {
|
|
138
|
-
assertNotDisposed(
|
|
142
|
+
assertNotDisposed(disposables);
|
|
139
143
|
return getRefCount(key).increment();
|
|
140
144
|
},
|
|
141
145
|
|
|
142
146
|
decrement: (key) => {
|
|
143
|
-
assertNotDisposed(
|
|
147
|
+
assertNotDisposed(disposables);
|
|
144
148
|
const refCount = getRefCount(key);
|
|
145
149
|
const nextCount = refCount.decrement();
|
|
146
150
|
if (nextCount === 0) {
|
|
147
|
-
refCount[Symbol.dispose]();
|
|
148
151
|
refCountByKey.delete(key);
|
|
152
|
+
refCount[Symbol.dispose]();
|
|
149
153
|
}
|
|
150
154
|
return nextCount;
|
|
151
155
|
},
|
|
152
156
|
|
|
153
157
|
getCount: (key) => {
|
|
154
|
-
assertNotDisposed(
|
|
158
|
+
assertNotDisposed(disposables);
|
|
155
159
|
return refCountByKey.get(key)?.getCount() ?? zeroNonNegativeInt;
|
|
156
160
|
},
|
|
157
161
|
|
|
158
162
|
has: (key) => {
|
|
159
|
-
assertNotDisposed(
|
|
163
|
+
assertNotDisposed(disposables);
|
|
160
164
|
return refCountByKey.has(key);
|
|
161
165
|
},
|
|
162
166
|
|
|
163
167
|
keys: () => {
|
|
164
|
-
assertNotDisposed(
|
|
168
|
+
assertNotDisposed(disposables);
|
|
165
169
|
return new Set(refCountByKey.keys());
|
|
166
170
|
},
|
|
167
171
|
|
|
168
|
-
[Symbol.dispose]: () =>
|
|
172
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
169
173
|
};
|
|
170
174
|
}
|