@evolu/common 8.0.0-next.4 → 8.0.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/dist/src/Array.d.ts +279 -173
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +105 -101
- package/dist/src/Assert.d.ts +44 -44
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +44 -32
- package/dist/src/BigInt.d.ts +6 -3
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -2
- package/dist/src/Brand.d.ts +27 -40
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Buffer.d.ts +27 -11
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Cache.d.ts +9 -4
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +8 -3
- package/dist/src/Callbacks.d.ts +30 -19
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Console.d.ts +145 -66
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +91 -43
- package/dist/src/Crypto.d.ts +85 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +44 -20
- package/dist/src/Eq.d.ts +87 -21
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +67 -19
- package/dist/src/Error.d.ts +5 -3
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +16 -0
- package/dist/src/Function.d.ts +119 -58
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +86 -43
- package/dist/src/Http.d.ts +394 -0
- package/dist/src/Http.d.ts.map +1 -0
- package/dist/src/Http.js +102 -0
- package/dist/src/Identicon.d.ts +16 -7
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +14 -5
- package/dist/src/LeakDetector.d.ts +88 -0
- package/dist/src/LeakDetector.d.ts.map +1 -0
- package/dist/src/LeakDetector.js +86 -0
- package/dist/src/LockManager.d.ts +18 -9
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +7 -8
- package/dist/src/Lookup.d.ts +43 -13
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +28 -8
- package/dist/src/Microtask.d.ts +1 -1
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +3 -7
- package/dist/src/Number.d.ts +49 -13
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +34 -5
- package/dist/src/Object.d.ts +160 -40
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +92 -46
- package/dist/src/Option.d.ts +10 -7
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +9 -6
- package/dist/src/Order.d.ts +21 -21
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +18 -21
- package/dist/src/Platform.d.ts +31 -0
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +98 -0
- package/dist/src/Random.d.ts +32 -17
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +12 -8
- package/dist/src/Redacted.d.ts +43 -29
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +14 -3
- package/dist/src/Ref.d.ts +11 -3
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +2 -2
- package/dist/src/RefCount.d.ts +2 -6
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +33 -90
- package/dist/src/Relation.d.ts +70 -5
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +77 -2
- package/dist/src/Resource.d.ts +590 -155
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +592 -343
- package/dist/src/Result.d.ts +838 -191
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +163 -102
- package/dist/src/Schedule.d.ts +684 -231
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +766 -295
- package/dist/src/Set.d.ts +89 -40
- package/dist/src/Set.d.ts.map +1 -1
- package/dist/src/Set.js +50 -29
- package/dist/src/Sqlite.d.ts +29 -72
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +41 -46
- package/dist/src/StackTrace.d.ts +40 -0
- package/dist/src/StackTrace.d.ts.map +1 -0
- package/dist/src/StackTrace.js +140 -0
- package/dist/src/Store.d.ts +5 -4
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +5 -4
- package/dist/src/String.d.ts +7 -1
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +20 -7
- package/dist/src/Task.d.ts +3572 -1770
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1879 -1274
- package/dist/src/Test.d.ts +17 -125
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +17 -68
- package/dist/src/Time.d.ts +108 -49
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +172 -73
- package/dist/src/Type.d.ts +3542 -1926
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +3070 -1784
- package/dist/src/Types.d.ts +200 -108
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +39 -8
- package/dist/src/WebSocket.d.ts +48 -23
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +15 -6
- package/dist/src/Worker.d.ts +94 -36
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +58 -24
- package/dist/src/index.d.ts +55 -52
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/cs.d.ts +94 -0
- package/dist/src/intl/cs.d.ts.map +1 -0
- package/dist/src/intl/cs.js +220 -0
- package/dist/src/intl/index.d.ts +7 -0
- package/dist/src/intl/index.d.ts.map +1 -0
- package/dist/src/intl/index.js +6 -0
- package/dist/src/local-first/Db.d.ts +12 -8
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -132
- package/dist/src/local-first/Error.d.ts +4 -4
- package/dist/src/local-first/Evolu.d.ts +338 -92
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +47 -55
- package/dist/src/local-first/LocalAuth.d.ts +4 -4
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/Owner.d.ts +61 -24
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +45 -8
- package/dist/src/local-first/Protocol.d.ts +44 -31
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +36 -13
- package/dist/src/local-first/Query.d.ts +147 -62
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +112 -58
- package/dist/src/local-first/Relay.d.ts +44 -14
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +92 -41
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +25 -8
- package/dist/src/local-first/Shared.d.ts +23 -21
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +100 -57
- package/dist/src/local-first/Storage.d.ts +53 -32
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +194 -234
- package/dist/src/local-first/Timestamp.d.ts +21 -21
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +2 -2
- package/dist/src/local-first/index.d.ts +11 -11
- package/package.json +14 -13
- package/src/Array.ts +306 -205
- package/src/Assert.ts +54 -69
- package/src/BigInt.ts +6 -3
- package/src/Brand.ts +27 -40
- package/src/Buffer.ts +28 -12
- package/src/Cache.ts +9 -4
- package/src/Callbacks.ts +30 -19
- package/src/Console.ts +156 -84
- package/src/Crypto.ts +83 -41
- package/src/Eq.ts +95 -22
- package/src/Error.ts +8 -4
- package/src/Function.ts +167 -59
- package/src/Http.ts +549 -0
- package/src/Identicon.ts +16 -7
- package/src/LeakDetector.ts +182 -0
- package/src/LockManager.ts +43 -39
- package/src/Lookup.ts +62 -28
- package/src/Microtask.ts +19 -23
- package/src/Number.ts +64 -12
- package/src/Object.ts +173 -49
- package/src/Option.ts +10 -7
- package/src/Order.ts +23 -21
- package/src/Platform.ts +136 -0
- package/src/Polyfills.ts +6 -12
- package/src/Random.ts +32 -19
- package/src/Redacted.ts +44 -30
- package/src/Ref.ts +14 -6
- package/src/RefCount.ts +38 -80
- package/src/Relation.ts +202 -10
- package/src/Resource.ts +1199 -591
- package/src/Result.ts +930 -271
- package/src/Schedule.ts +859 -343
- package/src/Set.ts +89 -40
- package/src/Sqlite.ts +144 -138
- package/src/StackTrace.ts +230 -0
- package/src/Store.ts +8 -7
- package/src/String.ts +22 -5
- package/src/Task.ts +5101 -2832
- package/src/Test.ts +20 -168
- package/src/Time.ts +293 -90
- package/src/Type.ts +10034 -4117
- package/src/Types.ts +227 -110
- package/src/WebSocket.ts +64 -36
- package/src/Worker.ts +180 -72
- package/src/index.ts +55 -52
- package/src/intl/cs.ts +388 -0
- package/src/intl/index.ts +7 -0
- package/src/local-first/Db.ts +152 -179
- package/src/local-first/Error.ts +4 -4
- package/src/local-first/Evolu.ts +495 -250
- package/src/local-first/LocalAuth.ts +6 -12
- package/src/local-first/Owner.ts +58 -21
- package/src/local-first/Protocol.ts +49 -31
- package/src/local-first/Query.ts +146 -61
- package/src/local-first/Relay.ts +51 -21
- package/src/local-first/Schema.ts +109 -51
- package/src/local-first/Shared.ts +266 -226
- package/src/local-first/Storage.ts +266 -264
- package/src/local-first/Timestamp.ts +25 -18
- package/src/local-first/index.ts +11 -11
- package/dist/src/Skiplist.d.ts +0 -28
- package/dist/src/Skiplist.d.ts.map +0 -1
- package/dist/src/Skiplist.js +0 -64
- package/dist/src/Tracer.d.ts +0 -48
- package/dist/src/Tracer.d.ts.map +0 -1
- package/dist/src/Tracer.js +0 -6
- package/src/Skiplist.ts +0 -108
- package/src/Tracer.ts +0 -54
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development-time detection of leaked handles via garbage collection.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type ConsoleDep } from "./Console.ts";
|
|
8
|
+
import { constVoid } from "./Function.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detects handles that are garbage-collected while still considered held.
|
|
12
|
+
*
|
|
13
|
+
* A handle such as a Lease must be explicitly cleaned up. Nothing in JavaScript
|
|
14
|
+
* enforces that, so a handle whose last reference is dropped without cleanup
|
|
15
|
+
* leaks silently. Garbage collection proves the leak: once the handle is
|
|
16
|
+
* unreachable, no code can ever clean it up. A LeakDetector observes collection
|
|
17
|
+
* with `FinalizationRegistry` and warns when the collected handle was still
|
|
18
|
+
* held — a proven leak with zero false positives, pointing at the tracking
|
|
19
|
+
* site.
|
|
20
|
+
*
|
|
21
|
+
* Detection is best-effort: garbage collection timing is not deterministic, so
|
|
22
|
+
* a warning may come late or, in short-lived processes, never. It is a
|
|
23
|
+
* development canary, not a guarantee. Production uses
|
|
24
|
+
* {@link noopLeakDetector}.
|
|
25
|
+
*/
|
|
26
|
+
export interface LeakDetector {
|
|
27
|
+
/**
|
|
28
|
+
* Starts tracking `target` until {@link LeakDetector.untrack | untrack} or
|
|
29
|
+
* garbage collection.
|
|
30
|
+
*
|
|
31
|
+
* When `target` is garbage-collected and {@link Leak.isLeaked} returns `true`,
|
|
32
|
+
* the leak is reported with the stack captured at this call. The leak must
|
|
33
|
+
* not reference `target`, or the target would never become unreachable.
|
|
34
|
+
*/
|
|
35
|
+
readonly track: (target: object, leak: Leak, unregisterToken: object) => void;
|
|
36
|
+
|
|
37
|
+
/** Stops tracking the target registered with `unregisterToken`. */
|
|
38
|
+
readonly untrack: (unregisterToken: object) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Describes a tracked handle for {@link LeakDetector.track}. */
|
|
42
|
+
export interface Leak {
|
|
43
|
+
/** Handle name used in the warning, for example `"Lease"`. */
|
|
44
|
+
readonly name: string;
|
|
45
|
+
|
|
46
|
+
/** Returns whether the collected handle was still considered held. */
|
|
47
|
+
readonly isLeaked: () => boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dependency wrapper for {@link LeakDetector}.
|
|
52
|
+
*
|
|
53
|
+
* @see {@link LeakDetector}
|
|
54
|
+
*/
|
|
55
|
+
export interface LeakDetectorDep {
|
|
56
|
+
readonly leakDetector: LeakDetector;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates {@link LeakDetector} backed by `FinalizationRegistry`.
|
|
61
|
+
*
|
|
62
|
+
* Capturing a stack per track call is too expensive for production; use
|
|
63
|
+
* {@link noopLeakDetector} there.
|
|
64
|
+
*/
|
|
65
|
+
export const createLeakDetector = (deps: ConsoleDep): LeakDetector => {
|
|
66
|
+
if (typeof globalThis.FinalizationRegistry !== "function")
|
|
67
|
+
return noopLeakDetector;
|
|
68
|
+
|
|
69
|
+
const registry = new globalThis.FinalizationRegistry<TrackedLeak>(
|
|
70
|
+
reportLeak(deps),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
track: (target, leak, unregisterToken) => {
|
|
75
|
+
registry.register(
|
|
76
|
+
target,
|
|
77
|
+
{ ...leak, stack: new Error().stack },
|
|
78
|
+
unregisterToken,
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
untrack: (unregisterToken) => {
|
|
82
|
+
registry.unregister(unregisterToken);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** No-op {@link LeakDetector} for production. */
|
|
88
|
+
export const noopLeakDetector: LeakDetector = {
|
|
89
|
+
track: constVoid,
|
|
90
|
+
untrack: constVoid,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
interface TrackedLeak extends Leak {
|
|
94
|
+
readonly stack: string | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reports a collected handle that was still held.
|
|
99
|
+
*
|
|
100
|
+
* Shared by {@link createLeakDetector} as the `FinalizationRegistry` callback
|
|
101
|
+
* and by {@link testCreateLeakDetector.collect | collect}, so tests cover the
|
|
102
|
+
* same reporting path without forcing garbage collection. Returns whether the
|
|
103
|
+
* leak was reported.
|
|
104
|
+
*/
|
|
105
|
+
const reportLeak =
|
|
106
|
+
(deps: ConsoleDep) =>
|
|
107
|
+
(leak: TrackedLeak): boolean => {
|
|
108
|
+
if (!leak.isLeaked()) return false;
|
|
109
|
+
deps.console.warn(
|
|
110
|
+
`${leak.name} was garbage-collected without cleanup. Tracked at:`,
|
|
111
|
+
leak.stack,
|
|
112
|
+
);
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Test {@link LeakDetector} with deterministic collection.
|
|
118
|
+
*
|
|
119
|
+
* @see {@link testCreateLeakDetector}
|
|
120
|
+
*/
|
|
121
|
+
export interface TestLeakDetector extends LeakDetector {
|
|
122
|
+
/**
|
|
123
|
+
* Simulates garbage collection of all tracked targets.
|
|
124
|
+
*
|
|
125
|
+
* Reports every tracked handle that is still held and clears tracking.
|
|
126
|
+
* Returns the number of reported leaks.
|
|
127
|
+
*/
|
|
128
|
+
readonly collect: () => number;
|
|
129
|
+
|
|
130
|
+
/** Returns the number of currently tracked targets. */
|
|
131
|
+
readonly getTrackedCount: (options?: { name?: string }) => number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Dependency wrapper for {@link TestLeakDetector}.
|
|
136
|
+
*
|
|
137
|
+
* @see {@link TestLeakDetector}
|
|
138
|
+
*/
|
|
139
|
+
export interface TestLeakDetectorDep extends LeakDetectorDep {
|
|
140
|
+
readonly leakDetector: TestLeakDetector;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Creates {@link TestLeakDetector}. */
|
|
144
|
+
export const testCreateLeakDetector = (deps: ConsoleDep): TestLeakDetector => {
|
|
145
|
+
const trackedLeaksByToken = new Map<object, ReadonlyArray<TrackedLeak>>();
|
|
146
|
+
const report = reportLeak(deps);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
track: (_target, leak, unregisterToken) => {
|
|
150
|
+
const trackedLeaks = trackedLeaksByToken.get(unregisterToken) ?? [];
|
|
151
|
+
trackedLeaksByToken.set(unregisterToken, [
|
|
152
|
+
...trackedLeaks,
|
|
153
|
+
{
|
|
154
|
+
...leak,
|
|
155
|
+
stack: new Error().stack,
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
},
|
|
159
|
+
untrack: (unregisterToken) => {
|
|
160
|
+
trackedLeaksByToken.delete(unregisterToken);
|
|
161
|
+
},
|
|
162
|
+
collect: () => {
|
|
163
|
+
let leakCount = 0;
|
|
164
|
+
for (const trackedLeaks of trackedLeaksByToken.values()) {
|
|
165
|
+
for (const leak of trackedLeaks) {
|
|
166
|
+
if (report(leak)) leakCount++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
trackedLeaksByToken.clear();
|
|
170
|
+
return leakCount;
|
|
171
|
+
},
|
|
172
|
+
getTrackedCount: ({ name } = {}) => {
|
|
173
|
+
let count = 0;
|
|
174
|
+
for (const trackedLeaks of trackedLeaksByToken.values()) {
|
|
175
|
+
for (const leak of trackedLeaks) {
|
|
176
|
+
if (name === undefined || leak.name === name) count++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return count;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|
package/src/LockManager.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createRandomBytes } from "./Crypto.
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { createId } from "./Type.
|
|
12
|
-
import type { Callback } from "./Types.
|
|
7
|
+
import { createRandomBytes } from "./Crypto.ts";
|
|
8
|
+
import { constVoid } from "./Function.ts";
|
|
9
|
+
import { ok } from "./Result.ts";
|
|
10
|
+
import type { Task } from "./Task.ts";
|
|
11
|
+
import { createId } from "./Type.ts";
|
|
12
|
+
import type { Callback } from "./Types.ts";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Dependency wrapper for {@link LockManager}.
|
|
@@ -17,24 +17,33 @@ import type { Callback } from "./Types.js";
|
|
|
17
17
|
* For React Native, use the `lockManager` ponyfill from `@evolu/react-native`.
|
|
18
18
|
* For tests, use {@link testCreateLockManager}.
|
|
19
19
|
*
|
|
20
|
-
* ###
|
|
20
|
+
* ### Web and Node.js 24+
|
|
21
21
|
*
|
|
22
22
|
* ```ts
|
|
23
|
-
*
|
|
24
|
-
* import { testCreateLockManager } from "@evolu/common";
|
|
23
|
+
* import type { LockManagerDep } from "@evolu/common";
|
|
25
24
|
*
|
|
26
25
|
* const deps: LockManagerDep = {
|
|
27
|
-
* lockManager:
|
|
26
|
+
* lockManager: globalThis.navigator.locks,
|
|
28
27
|
* };
|
|
28
|
+
*
|
|
29
|
+
* const result = await deps.lockManager.request("example", (lock) => {
|
|
30
|
+
* return lock?.name;
|
|
31
|
+
* });
|
|
32
|
+
* expect(result).toBe("example");
|
|
29
33
|
* ```
|
|
30
34
|
*
|
|
31
|
-
* ###
|
|
35
|
+
* ### React Native
|
|
32
36
|
*
|
|
33
37
|
* ```ts
|
|
34
|
-
*
|
|
38
|
+
* import type { LockManagerDep } from "@evolu/common";
|
|
35
39
|
* import { lockManager } from "@evolu/react-native";
|
|
36
40
|
*
|
|
37
41
|
* const deps: LockManagerDep = { lockManager };
|
|
42
|
+
*
|
|
43
|
+
* const result = await deps.lockManager.request("example", (lock) => {
|
|
44
|
+
* return lock?.name;
|
|
45
|
+
* });
|
|
46
|
+
* expect(result).toBe("example");
|
|
38
47
|
* ```
|
|
39
48
|
*/
|
|
40
49
|
|
|
@@ -110,35 +119,30 @@ export const testCreateLockManager = (
|
|
|
110
119
|
* abortable via the calling {@link Task}'s signal.
|
|
111
120
|
*/
|
|
112
121
|
export const acquireLeaderLock =
|
|
113
|
-
(name: string): Task<AsyncDisposable,
|
|
114
|
-
(run) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const acquisition = Promise.withResolvers<void>();
|
|
118
|
-
const released = Promise.withResolvers<void>();
|
|
122
|
+
(name: string): Task<AsyncDisposable, never, LockManagerDep> =>
|
|
123
|
+
async (run) => {
|
|
124
|
+
const acquisition = Promise.withResolvers<void>();
|
|
125
|
+
const released = Promise.withResolvers<void>();
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
};
|
|
127
|
+
const request = run.deps.lockManager.request(
|
|
128
|
+
createLeaderLockName(name),
|
|
129
|
+
{ mode: "exclusive", signal: run.signal },
|
|
130
|
+
async () => {
|
|
131
|
+
acquisition.resolve();
|
|
132
|
+
await released.promise;
|
|
138
133
|
},
|
|
139
|
-
(error): AbortError =>
|
|
140
|
-
AbortError.is(error) ? error : { type: "AbortError", reason: error },
|
|
141
134
|
);
|
|
135
|
+
void request.catch(acquisition.reject);
|
|
136
|
+
|
|
137
|
+
await acquisition.promise;
|
|
138
|
+
|
|
139
|
+
return ok({
|
|
140
|
+
[Symbol.asyncDispose]: async () => {
|
|
141
|
+
released.resolve();
|
|
142
|
+
await request;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
};
|
|
142
146
|
|
|
143
147
|
/**
|
|
144
148
|
* Competes to become the current leader for `name` using {@link LockManager}.
|
|
@@ -167,7 +171,7 @@ export const acquireLeaderLockCallback =
|
|
|
167
171
|
await released.promise;
|
|
168
172
|
},
|
|
169
173
|
);
|
|
170
|
-
void request.catch(
|
|
174
|
+
void request.catch(constVoid);
|
|
171
175
|
|
|
172
176
|
return {
|
|
173
177
|
[Symbol.dispose]: () => {
|
package/src/Lookup.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { assert } from "./Assert.
|
|
8
|
-
import type { Brand } from "./Brand.
|
|
9
|
-
import { isPlainObject } from "./Object.
|
|
10
|
-
import { Uint8Array, uint8ArrayToBase64Url } from "./Type.
|
|
7
|
+
import { assert } from "./Assert.ts";
|
|
8
|
+
import type { Brand } from "./Brand.ts";
|
|
9
|
+
import { isPlainObject } from "./Object.ts";
|
|
10
|
+
import { Uint8Array, uint8ArrayToBase64Url } from "./Type.ts";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Derives the native lookup key used by lookup-based collections.
|
|
@@ -23,6 +23,16 @@ import { Uint8Array, uint8ArrayToBase64Url } from "./Type.js";
|
|
|
23
23
|
* Use this when callers provide keys in one form but logical identity should be
|
|
24
24
|
* based on another stable key, such as a natural id or {@link StructuralKey}.
|
|
25
25
|
*
|
|
26
|
+
* Lookup functions must be pure and stable. The same logical key must derive
|
|
27
|
+
* the same native `Map` key while an entry is stored, so use lookup-based
|
|
28
|
+
* collections with immutable key data, preferably readonly values. Mutating key
|
|
29
|
+
* data after insertion can make later `get`, `has`, and `delete` calls disagree
|
|
30
|
+
* with the inserted entry.
|
|
31
|
+
*
|
|
32
|
+
* The returned lookup key uses native `Map` equality. Return primitives or
|
|
33
|
+
* stable object references; returning a new object for each call prevents equal
|
|
34
|
+
* logical keys from matching.
|
|
35
|
+
*
|
|
26
36
|
* @see {@link createLookupMap}
|
|
27
37
|
* @see {@link createLookupSet}
|
|
28
38
|
*/
|
|
@@ -74,12 +84,12 @@ export const createLookupMap = <K, V, L>({
|
|
|
74
84
|
lookup,
|
|
75
85
|
entries,
|
|
76
86
|
}: CreateLookupMapOptions<K, V, L>): LookupMap<K, V> => {
|
|
77
|
-
interface LookupEntry
|
|
87
|
+
interface LookupEntry {
|
|
78
88
|
readonly key: K;
|
|
79
89
|
readonly value: V;
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
const entriesByLookupKey = new Map<L, LookupEntry
|
|
92
|
+
const entriesByLookupKey = new Map<L, LookupEntry>();
|
|
83
93
|
|
|
84
94
|
const map: LookupMap<K, V> = {
|
|
85
95
|
get size() {
|
|
@@ -278,8 +288,10 @@ export type StructuralKey =
|
|
|
278
288
|
*
|
|
279
289
|
* `Structural<T>` checks a concrete type recursively at compile time instead:
|
|
280
290
|
* scalars pass through, arrays recurse, object properties recurse, and
|
|
281
|
-
* function-valued properties are rejected.
|
|
282
|
-
*
|
|
291
|
+
* function-valued and symbol-keyed properties are rejected. Symbol-keyed
|
|
292
|
+
* properties are rejected because runtime serialization reads only enumerable
|
|
293
|
+
* string keys, so they would be silently ignored. This keeps public structural
|
|
294
|
+
* APIs ergonomic for interface-based inputs while preserving the same runtime
|
|
283
295
|
* constraints as structural lookup serialization.
|
|
284
296
|
*
|
|
285
297
|
* @see {@link StructuralKey}
|
|
@@ -293,19 +305,21 @@ export type Structural<T> = T extends StructuralScalar
|
|
|
293
305
|
? never
|
|
294
306
|
: T extends object
|
|
295
307
|
? {
|
|
296
|
-
readonly [
|
|
297
|
-
T
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
308
|
+
readonly [
|
|
309
|
+
K in keyof T as K extends symbol
|
|
310
|
+
? never
|
|
311
|
+
: Extract<T[K], StructuralFunction> extends never
|
|
312
|
+
? K
|
|
313
|
+
: never
|
|
314
|
+
]: Structural<Exclude<T[K], StructuralFunction>>;
|
|
302
315
|
} & {
|
|
303
|
-
readonly [
|
|
304
|
-
T
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
316
|
+
readonly [
|
|
317
|
+
K in keyof T as K extends symbol
|
|
318
|
+
? K
|
|
319
|
+
: Extract<T[K], StructuralFunction> extends never
|
|
320
|
+
? never
|
|
321
|
+
: K
|
|
322
|
+
]?: never;
|
|
309
323
|
}
|
|
310
324
|
: never;
|
|
311
325
|
|
|
@@ -317,7 +331,8 @@ export type StructuralFunction = (...args: ReadonlyArray<unknown>) => unknown;
|
|
|
317
331
|
*
|
|
318
332
|
* Structural lookup keys are derived from JSON-like values plus `Uint8Array`.
|
|
319
333
|
* Equal structures produce the same lookup key even when they are different
|
|
320
|
-
* JavaScript instances.
|
|
334
|
+
* JavaScript instances. `undefined` values and sparse array holes are rejected
|
|
335
|
+
* rather than skipped.
|
|
321
336
|
*
|
|
322
337
|
* The derived key is memoized by non-null object identity in a module-scoped
|
|
323
338
|
* `WeakMap` shared by all callers, so keys must be immutable.
|
|
@@ -328,16 +343,33 @@ export type StructuralFunction = (...args: ReadonlyArray<unknown>) => unknown;
|
|
|
328
343
|
* ### Example
|
|
329
344
|
*
|
|
330
345
|
* ```ts
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* StructuralLookupKey
|
|
335
|
-
*
|
|
346
|
+
* import {
|
|
347
|
+
* createLookupMap,
|
|
348
|
+
* structuralLookup,
|
|
349
|
+
* type StructuralLookupKey,
|
|
350
|
+
* } from "@evolu/common";
|
|
351
|
+
*
|
|
352
|
+
* interface Filter {
|
|
353
|
+
* readonly table: string;
|
|
354
|
+
* readonly where: readonly [string, string];
|
|
355
|
+
* }
|
|
356
|
+
*
|
|
357
|
+
* const byFilter = createLookupMap<Filter, string, StructuralLookupKey>({
|
|
336
358
|
* lookup: structuralLookup,
|
|
337
359
|
* });
|
|
338
360
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
361
|
+
* const cachedFilter: Filter = {
|
|
362
|
+
* table: "todo",
|
|
363
|
+
* where: ["owner", "ada"],
|
|
364
|
+
* };
|
|
365
|
+
* const equivalentFilter: Filter = {
|
|
366
|
+
* where: ["owner", "ada"],
|
|
367
|
+
* table: "todo",
|
|
368
|
+
* };
|
|
369
|
+
* byFilter.set(cachedFilter, "cached");
|
|
370
|
+
*
|
|
371
|
+
* expect(byFilter.get(equivalentFilter)).toBe("cached");
|
|
372
|
+
* expect(byFilter.getKey(equivalentFilter)).toBe(cachedFilter);
|
|
341
373
|
* ```
|
|
342
374
|
*
|
|
343
375
|
* @see {@link StructuralKey}
|
|
@@ -395,6 +427,8 @@ const structuralLookupInternal = (
|
|
|
395
427
|
);
|
|
396
428
|
}
|
|
397
429
|
|
|
430
|
+
// Cache only after traversal succeeds so cyclic values never leave a
|
|
431
|
+
// partial lookup key in the memo.
|
|
398
432
|
structuralLookupKeyByValue.set(value, lookupKey);
|
|
399
433
|
return lookupKey;
|
|
400
434
|
}
|
package/src/Microtask.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { appendToArray, type NonEmptyReadonlyArray } from "./Array.
|
|
8
|
-
import {
|
|
7
|
+
import { appendToArray, type NonEmptyReadonlyArray } from "./Array.ts";
|
|
8
|
+
import { disposable } from "./Function.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Batches values and flushes them in a single microtask.
|
|
@@ -32,8 +32,6 @@ export const createMicrotaskBatch = <T>(
|
|
|
32
32
|
queue = null;
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
const disposables = disposer.move();
|
|
36
|
-
|
|
37
35
|
const flushQueuedItems = () => {
|
|
38
36
|
if (queue == null) return;
|
|
39
37
|
const queuedItems = queue;
|
|
@@ -41,24 +39,22 @@ export const createMicrotaskBatch = <T>(
|
|
|
41
39
|
onFlush(queuedItems);
|
|
42
40
|
};
|
|
43
41
|
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
return disposable<MicrotaskBatch<T>>(
|
|
43
|
+
{
|
|
44
|
+
push: (item) => {
|
|
45
|
+
if (queue == null) {
|
|
46
|
+
queue = [item];
|
|
47
|
+
queueMicrotask(flushQueuedItems);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
queue = appendToArray(queue, item);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
flushNow: () => {
|
|
55
|
+
flushQueuedItems();
|
|
56
|
+
},
|
|
55
57
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
assertNotDisposed(disposables);
|
|
59
|
-
flushQueuedItems();
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
63
|
-
};
|
|
58
|
+
disposer,
|
|
59
|
+
);
|
|
64
60
|
};
|
package/src/Number.ts
CHANGED
|
@@ -4,22 +4,51 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { NonEmptyReadonlyArray } from "./Array.
|
|
8
|
-
import { assertNonEmptyReadonlyArray } from "./Assert.
|
|
9
|
-
import type { IsBranded } from "./Brand.
|
|
10
|
-
import type { Result } from "./Result.
|
|
11
|
-
import { err, ok } from "./Result.
|
|
7
|
+
import type { NonEmptyReadonlyArray } from "./Array.ts";
|
|
8
|
+
import { assertNonEmptyReadonlyArray } from "./Assert.ts";
|
|
9
|
+
import type { IsBranded } from "./Brand.ts";
|
|
10
|
+
import type { Result } from "./Result.ts";
|
|
11
|
+
import { err, ok } from "./Result.ts";
|
|
12
12
|
import {
|
|
13
13
|
brand,
|
|
14
14
|
lessThanOrEqualTo,
|
|
15
15
|
onePositiveInt,
|
|
16
16
|
NonNegativeInt,
|
|
17
17
|
PositiveInt,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Ratio,
|
|
19
|
+
} from "./Type.ts";
|
|
20
|
+
import type {
|
|
21
|
+
Digit,
|
|
22
|
+
Digit1To9,
|
|
23
|
+
Digit1To99,
|
|
24
|
+
Predicate,
|
|
25
|
+
WidenLiteral,
|
|
26
|
+
} from "./Types.ts";
|
|
27
|
+
|
|
28
|
+
/** Percentage represented as a readable literal or a validated ratio. */
|
|
29
|
+
export type Percentage = PercentageLiteral | Ratio;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Percentage literal from `"0%"` to `"100%"`.
|
|
33
|
+
*
|
|
34
|
+
* Decimal literals support one decimal place. Use {@link Ratio} for computed
|
|
35
|
+
* values or greater precision.
|
|
36
|
+
*/
|
|
37
|
+
export type PercentageLiteral =
|
|
38
|
+
"0%" | "100%" | `${Digit1To99}%` | `${Digit | Digit1To99}.${Digit1To9}%`;
|
|
39
|
+
|
|
40
|
+
/** Converts a {@link Percentage} to its numeric {@link Ratio}. */
|
|
41
|
+
export const percentageToRatio = (percentage: Percentage): Ratio =>
|
|
42
|
+
typeof percentage === "number"
|
|
43
|
+
? percentage
|
|
44
|
+
: Ratio.orThrow(globalThis.Number.parseFloat(percentage) / 100);
|
|
20
45
|
|
|
21
46
|
export const increment = (n: number): number => n + 1;
|
|
22
47
|
|
|
48
|
+
/** Increments a positive integer, saturating at the maximum safe integer. */
|
|
49
|
+
export const incrementPositiveInt = (n: PositiveInt): PositiveInt =>
|
|
50
|
+
PositiveInt.orNull(increment(n)) ?? n;
|
|
51
|
+
|
|
23
52
|
export const decrement = (n: number): number => n - 1;
|
|
24
53
|
|
|
25
54
|
/** Clamps a number within a given range. */
|
|
@@ -34,9 +63,12 @@ export const clamp =
|
|
|
34
63
|
* ### Example
|
|
35
64
|
*
|
|
36
65
|
* ```ts
|
|
66
|
+
* import { isBetween } from "@evolu/common";
|
|
67
|
+
*
|
|
37
68
|
* const isBetween10And20 = isBetween(10, 20);
|
|
38
|
-
*
|
|
39
|
-
*
|
|
69
|
+
*
|
|
70
|
+
* expect(isBetween10And20(20)).toBe(true);
|
|
71
|
+
* expect(isBetween10And20(25)).toBe(false);
|
|
40
72
|
* ```
|
|
41
73
|
*/
|
|
42
74
|
export const isBetween =
|
|
@@ -64,8 +96,28 @@ export const max = <T extends number>(
|
|
|
64
96
|
* ### Example
|
|
65
97
|
*
|
|
66
98
|
* ```ts
|
|
67
|
-
*
|
|
68
|
-
*
|
|
99
|
+
* import {
|
|
100
|
+
* computeBalancedBuckets,
|
|
101
|
+
* NonNegativeInt,
|
|
102
|
+
* PositiveInt,
|
|
103
|
+
* } from "@evolu/common";
|
|
104
|
+
*
|
|
105
|
+
* expectOk(
|
|
106
|
+
* computeBalancedBuckets(
|
|
107
|
+
* NonNegativeInt.orThrow(10),
|
|
108
|
+
* PositiveInt.orThrow(3),
|
|
109
|
+
* PositiveInt.orThrow(2),
|
|
110
|
+
* ),
|
|
111
|
+
* [4, 7, 10],
|
|
112
|
+
* );
|
|
113
|
+
* expectErr(
|
|
114
|
+
* computeBalancedBuckets(
|
|
115
|
+
* NonNegativeInt.orThrow(5),
|
|
116
|
+
* PositiveInt.orThrow(3),
|
|
117
|
+
* PositiveInt.orThrow(2),
|
|
118
|
+
* ),
|
|
119
|
+
* 6,
|
|
120
|
+
* );
|
|
69
121
|
* ```
|
|
70
122
|
*/
|
|
71
123
|
export const computeBalancedBuckets = (
|
|
@@ -107,7 +159,7 @@ export const FibonacciIndex = /*#__PURE__*/ brand(
|
|
|
107
159
|
"FibonacciIndex",
|
|
108
160
|
/*#__PURE__*/ lessThanOrEqualTo(78)(PositiveInt),
|
|
109
161
|
);
|
|
110
|
-
export type FibonacciIndex = typeof FibonacciIndex.
|
|
162
|
+
export type FibonacciIndex = typeof FibonacciIndex.Output;
|
|
111
163
|
|
|
112
164
|
/** Returns the Fibonacci number at the given index (1-indexed: 1,1,2,3,5,8,...). */
|
|
113
165
|
export const fibonacciAt = (index: FibonacciIndex): PositiveInt => {
|