@evolu/common 7.4.0 → 8.0.0-next.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/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
package/src/Resource.ts
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrency-safe helpers for efficient reuse of disposable resources.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { assert, assertNotAborted } from "./Assert.js";
|
|
8
|
+
import { identity } from "./Function.js";
|
|
9
|
+
import {
|
|
10
|
+
createLookupMap,
|
|
11
|
+
createLookupSet,
|
|
12
|
+
type Lookup,
|
|
13
|
+
type LookupOption,
|
|
14
|
+
} from "./Lookup.js";
|
|
15
|
+
import {
|
|
16
|
+
createRefCount,
|
|
17
|
+
createRefCountByKey,
|
|
18
|
+
type RefCountByKey,
|
|
19
|
+
} from "./RefCount.js";
|
|
20
|
+
import { createRelation } from "./Relation.js";
|
|
21
|
+
import { ok } from "./Result.js";
|
|
22
|
+
import {
|
|
23
|
+
createMutex,
|
|
24
|
+
createMutexByKey,
|
|
25
|
+
sleep,
|
|
26
|
+
unabortable,
|
|
27
|
+
type AbortError,
|
|
28
|
+
type Fiber,
|
|
29
|
+
type MutexRef,
|
|
30
|
+
type SemaphoreSnapshot,
|
|
31
|
+
type Task,
|
|
32
|
+
} from "./Task.js";
|
|
33
|
+
import { type Duration } from "./Time.js";
|
|
34
|
+
import { NonNegativeInt, zeroNonNegativeInt } from "./Type.js";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
createRefCount,
|
|
38
|
+
createRefCountByKey,
|
|
39
|
+
type CreateRefCountByKeyOptions,
|
|
40
|
+
type RefCount,
|
|
41
|
+
type RefCountByKey,
|
|
42
|
+
} from "./RefCount.js";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Disposable resource.
|
|
46
|
+
*
|
|
47
|
+
* A resource is any object that implements {@link Disposable} or
|
|
48
|
+
* {@link AsyncDisposable}.
|
|
49
|
+
*
|
|
50
|
+
* Disposal must succeed. A disposer that throws indicates an unrecoverable
|
|
51
|
+
* invariant violation, not a recoverable domain error, so resource lifecycle
|
|
52
|
+
* APIs let that error propagate. Disposal failures shall surface at the app
|
|
53
|
+
* boundary, where they are shown to the user and logged. The purpose of
|
|
54
|
+
* resource helpers is to guarantee cleanup and prevent leaks.
|
|
55
|
+
*
|
|
56
|
+
* @see {@link ResourceRef}
|
|
57
|
+
* @see {@link createResourceRef}
|
|
58
|
+
* @see {@link SharedResource}
|
|
59
|
+
* @see {@link createSharedResource}
|
|
60
|
+
* @see {@link SharedResourceByKey}
|
|
61
|
+
* @see {@link createSharedResourceByKey}
|
|
62
|
+
*/
|
|
63
|
+
export type Resource = Disposable | AsyncDisposable;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Borrowed {@link Resource}.
|
|
67
|
+
*
|
|
68
|
+
* A borrowed resource is a {@link Resource} without disposal methods.
|
|
69
|
+
*
|
|
70
|
+
* Another abstraction owns the resource and controls its lifecycle. Exposing
|
|
71
|
+
* disposal would break that ownership and allow callers to dispose a resource
|
|
72
|
+
* they do not own.
|
|
73
|
+
*/
|
|
74
|
+
export type BorrowedResource<T extends Resource> = Omit<
|
|
75
|
+
T,
|
|
76
|
+
typeof Symbol.dispose | typeof Symbol.asyncDispose
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* {@link Resource} reference.
|
|
81
|
+
*
|
|
82
|
+
* A {@link MutexRef}-like reference for resources. `ResourceRef` controls the
|
|
83
|
+
* resource lifecycle.
|
|
84
|
+
*
|
|
85
|
+
* Callers get the current resource as {@link BorrowedResource} to ensure only
|
|
86
|
+
* the `ResourceRef` can dispose it.
|
|
87
|
+
*
|
|
88
|
+
* Setting a new resource first disposes the current one and then sets the next.
|
|
89
|
+
* Calling abort on the returned Fiber does not roll that change back once it
|
|
90
|
+
* has started. The create Task must not fail. If it could fail, the current
|
|
91
|
+
* resource would be disposed without the next resource installed.
|
|
92
|
+
*/
|
|
93
|
+
export interface ResourceRef<
|
|
94
|
+
T extends Resource,
|
|
95
|
+
D = unknown,
|
|
96
|
+
> extends AsyncDisposable {
|
|
97
|
+
/** Returns the current resource. */
|
|
98
|
+
readonly get: Task<BorrowedResource<T>, never, D>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Disposes the current resource and then creates and sets the next.
|
|
102
|
+
*
|
|
103
|
+
* Once started, this operation runs to completion even if the caller aborts
|
|
104
|
+
* its Fiber. Always await the result instead of treating abort as rollback.
|
|
105
|
+
*/
|
|
106
|
+
readonly set: (create: Task<T, never, D>) => Task<void, never, D>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Creates {@link ResourceRef}. */
|
|
110
|
+
export const createResourceRef = <T extends Resource, D>(
|
|
111
|
+
create: Task<T, never, D>,
|
|
112
|
+
): Task<ResourceRef<T, D>, never, D> =>
|
|
113
|
+
unabortable<ResourceRef<T, D>, never, D>(async (run) => {
|
|
114
|
+
const resourceRefRun = run.create();
|
|
115
|
+
|
|
116
|
+
await using stack = new AsyncDisposableStack();
|
|
117
|
+
stack.use(resourceRefRun);
|
|
118
|
+
|
|
119
|
+
const initial = await resourceRefRun(create);
|
|
120
|
+
if (!initial.ok) return initial;
|
|
121
|
+
|
|
122
|
+
let current = createOwnedResource(initial.value);
|
|
123
|
+
|
|
124
|
+
const mutex = stack.use(createMutex());
|
|
125
|
+
stack.defer(() => current.stack.disposeAsync());
|
|
126
|
+
// Register as the last so disposal aborts further calls first.
|
|
127
|
+
// Repeated registration is safe because disposal is idempotent.
|
|
128
|
+
stack.use(resourceRefRun);
|
|
129
|
+
|
|
130
|
+
const moved = stack.move();
|
|
131
|
+
|
|
132
|
+
return ok({
|
|
133
|
+
get: () => resourceRefRun(mutex.withLock(() => ok(current.resource))),
|
|
134
|
+
|
|
135
|
+
set: (create: Task<T, never, D>): Task<void, never, D> =>
|
|
136
|
+
unabortable(() =>
|
|
137
|
+
resourceRefRun(
|
|
138
|
+
mutex.withLock(async (run) => {
|
|
139
|
+
await current.stack.disposeAsync();
|
|
140
|
+
const next = await run(create);
|
|
141
|
+
if (!next.ok) return next;
|
|
142
|
+
current = createOwnedResource(next.value);
|
|
143
|
+
return ok();
|
|
144
|
+
}),
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
|
|
148
|
+
[Symbol.asyncDispose]: () => moved.disposeAsync(),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Shared {@link Resource}.
|
|
154
|
+
*
|
|
155
|
+
* Lazily acquires the underlying resource on the first
|
|
156
|
+
* {@link SharedResource.acquire | acquire} call, shares it across callers, and
|
|
157
|
+
* disposes it when the last caller {@link SharedResource.release | releases}
|
|
158
|
+
* it.
|
|
159
|
+
*
|
|
160
|
+
* Calls to {@link SharedResource.release | release} must be balanced with
|
|
161
|
+
* successful calls to {@link SharedResource.acquire | acquire}. Releasing more
|
|
162
|
+
* times than acquired is a programmer error checked with {@link assert}.
|
|
163
|
+
*/
|
|
164
|
+
export interface SharedResource<
|
|
165
|
+
T extends Resource,
|
|
166
|
+
D = unknown,
|
|
167
|
+
> extends AsyncDisposable {
|
|
168
|
+
/** Returns the current shared-resource state for monitoring/debugging. */
|
|
169
|
+
readonly snapshot: () => SharedResourceSnapshot;
|
|
170
|
+
|
|
171
|
+
/** Returns the current resource, or `undefined` if absent. */
|
|
172
|
+
readonly get: () => BorrowedResource<T> | undefined;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Acquires a shared reference.
|
|
176
|
+
*
|
|
177
|
+
* The first call lazily creates the resource. Later calls reuse the same
|
|
178
|
+
* resource until the final {@link SharedResource.release | release} starts the
|
|
179
|
+
* final disposal path. Disposal happens immediately by default, or after
|
|
180
|
+
* {@link SharedResourceOptions.idleDisposeAfter | idleDisposeAfter} elapses
|
|
181
|
+
* when configured.
|
|
182
|
+
*
|
|
183
|
+
* Once started, acquire runs to completion even if the caller aborts its
|
|
184
|
+
* Fiber. Always await the result. A successful result still counts as an
|
|
185
|
+
* acquired lease and must later be balanced with
|
|
186
|
+
* {@link SharedResource.release | release}.
|
|
187
|
+
*/
|
|
188
|
+
readonly acquire: Task<BorrowedResource<T>, never, D>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Releases one previously acquired shared reference.
|
|
192
|
+
*
|
|
193
|
+
* When the last acquired reference is released, the current resource is
|
|
194
|
+
* disposed immediately by default. If
|
|
195
|
+
* {@link SharedResourceOptions.idleDisposeAfter | idleDisposeAfter} is set,
|
|
196
|
+
* disposal is scheduled instead and a new acquire during that delay reuses
|
|
197
|
+
* the current resource.
|
|
198
|
+
*
|
|
199
|
+
* Once started, release runs to completion even if the caller aborts its
|
|
200
|
+
* Fiber. Always await the result instead of assuming no cleanup happened.
|
|
201
|
+
*/
|
|
202
|
+
readonly release: Task<void, never, D>;
|
|
203
|
+
|
|
204
|
+
/** Returns the current acquire count. */
|
|
205
|
+
readonly getCount: Task<NonNegativeInt, never, D>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Snapshot returned by {@link SharedResource.snapshot}. */
|
|
209
|
+
export interface SharedResourceSnapshot {
|
|
210
|
+
/**
|
|
211
|
+
* Whether the resource has no current value, no borrowers, and no pending
|
|
212
|
+
* idle disposal.
|
|
213
|
+
*/
|
|
214
|
+
readonly isIdle: boolean;
|
|
215
|
+
|
|
216
|
+
/** Current mutex state for monitoring/debugging. */
|
|
217
|
+
readonly mutex: SemaphoreSnapshot;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Options for {@link createSharedResource}. */
|
|
221
|
+
export interface SharedResourceOptions {
|
|
222
|
+
/**
|
|
223
|
+
* Keeps the resource alive briefly after the last release.
|
|
224
|
+
*
|
|
225
|
+
* This avoids immediate disposal when the resource is expensive to create and
|
|
226
|
+
* likely to be acquired again soon. A new acquire during this delay cancels
|
|
227
|
+
* the pending disposal and reuses the current resource.
|
|
228
|
+
*/
|
|
229
|
+
readonly idleDisposeAfter?: Duration | undefined;
|
|
230
|
+
|
|
231
|
+
/** Called after the current resource is disposed and cleared. */
|
|
232
|
+
readonly onDisposed?: () => void;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Creates {@link SharedResource}. */
|
|
236
|
+
export const createSharedResource = <T extends Resource, D>(
|
|
237
|
+
create: Task<T, never, D>,
|
|
238
|
+
{ idleDisposeAfter, onDisposed }: SharedResourceOptions = {},
|
|
239
|
+
): Task<SharedResource<T, D>, never, D> =>
|
|
240
|
+
unabortable<SharedResource<T, D>, never, D>((run) => {
|
|
241
|
+
const sharedResourceRun = run.create();
|
|
242
|
+
let current: OwnedResource<T> | undefined;
|
|
243
|
+
let idleDisposeFiber: Fiber<void, AbortError, D> | undefined;
|
|
244
|
+
|
|
245
|
+
const stack = new AsyncDisposableStack();
|
|
246
|
+
const refCount = stack.use(createRefCount());
|
|
247
|
+
|
|
248
|
+
const mutex = stack.use(createMutex());
|
|
249
|
+
|
|
250
|
+
const disposeCurrent = async () => {
|
|
251
|
+
if (!current) return;
|
|
252
|
+
await using stack = new AsyncDisposableStack();
|
|
253
|
+
if (onDisposed) stack.defer(onDisposed);
|
|
254
|
+
stack.use(current.stack);
|
|
255
|
+
current = undefined;
|
|
256
|
+
};
|
|
257
|
+
stack.defer(disposeCurrent);
|
|
258
|
+
|
|
259
|
+
// Register as the last so disposal aborts further calls first.
|
|
260
|
+
stack.use(sharedResourceRun);
|
|
261
|
+
|
|
262
|
+
const moved = stack.move();
|
|
263
|
+
|
|
264
|
+
return ok({
|
|
265
|
+
snapshot: () => ({
|
|
266
|
+
isIdle: refCount.getCount() === 0 && !current && !idleDisposeFiber,
|
|
267
|
+
mutex: mutex.snapshot(),
|
|
268
|
+
}),
|
|
269
|
+
|
|
270
|
+
get: () => current?.resource,
|
|
271
|
+
|
|
272
|
+
acquire: unabortable<BorrowedResource<T>, never, D>(() =>
|
|
273
|
+
sharedResourceRun(
|
|
274
|
+
mutex.withLock(async (run) => {
|
|
275
|
+
if (idleDisposeFiber) {
|
|
276
|
+
idleDisposeFiber.abort();
|
|
277
|
+
idleDisposeFiber = undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!current) {
|
|
281
|
+
const resource = await run(create);
|
|
282
|
+
if (!resource.ok) return resource;
|
|
283
|
+
current = createOwnedResource(resource.value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
refCount.increment();
|
|
287
|
+
return ok(current.resource);
|
|
288
|
+
}),
|
|
289
|
+
),
|
|
290
|
+
),
|
|
291
|
+
|
|
292
|
+
release: unabortable<void, never, D>(() =>
|
|
293
|
+
sharedResourceRun(
|
|
294
|
+
mutex.withLock(async () => {
|
|
295
|
+
if (refCount.decrement() > 0) return ok();
|
|
296
|
+
|
|
297
|
+
if (!idleDisposeAfter) {
|
|
298
|
+
await disposeCurrent();
|
|
299
|
+
return ok();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
idleDisposeFiber = sharedResourceRun<void, never>(async (run) => {
|
|
303
|
+
const slept = await run(sleep(idleDisposeAfter));
|
|
304
|
+
if (!slept.ok) return slept;
|
|
305
|
+
|
|
306
|
+
return run(
|
|
307
|
+
mutex.withLock(async () => {
|
|
308
|
+
idleDisposeFiber = undefined;
|
|
309
|
+
await disposeCurrent();
|
|
310
|
+
return ok();
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return ok();
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
|
|
320
|
+
getCount: () =>
|
|
321
|
+
sharedResourceRun(mutex.withLock(() => ok(refCount.getCount()))),
|
|
322
|
+
|
|
323
|
+
[Symbol.asyncDispose]: () => moved.disposeAsync(),
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Shared {@link Resource}s keyed by logical identity.
|
|
329
|
+
*
|
|
330
|
+
* A map-like registry of {@link SharedResource}s. Each key owns at most one
|
|
331
|
+
* current resource instance.
|
|
332
|
+
*
|
|
333
|
+
* The first {@link SharedResourceByKey.acquire | acquire} for a key lazily
|
|
334
|
+
* creates that key's resource. Later acquires for the same key reuse the same
|
|
335
|
+
* resource until the final {@link SharedResourceByKey.release | release} starts
|
|
336
|
+
* the final disposal path for that key. Disposal and registry removal happen
|
|
337
|
+
* immediately by default, or after
|
|
338
|
+
* {@link SharedResourceByKeyOptions.idleDisposeAfter | idleDisposeAfter} elapses
|
|
339
|
+
* when configured.
|
|
340
|
+
*
|
|
341
|
+
* Different keys are independent and may progress concurrently. Operations for
|
|
342
|
+
* the same key are serialized. Calls to
|
|
343
|
+
* {@link SharedResourceByKey.release | release} must be balanced with successful
|
|
344
|
+
* calls to {@link SharedResourceByKey.acquire | acquire}. Acquire and release
|
|
345
|
+
* may still be aborted before they start on an already-stopped Run, but once
|
|
346
|
+
* started they run to completion. Releasing more times than acquired is a
|
|
347
|
+
* programmer error checked with {@link assert}.
|
|
348
|
+
*
|
|
349
|
+
* By default, {@link createSharedResourceByKey} uses reference identity for
|
|
350
|
+
* keys, matching native `Map`. Callers may instead provide a
|
|
351
|
+
* {@link Lookup
|
|
352
|
+
* lookup} so logical equality is based on a derived stable key.
|
|
353
|
+
*/
|
|
354
|
+
export interface SharedResourceByKey<
|
|
355
|
+
K,
|
|
356
|
+
T extends Resource,
|
|
357
|
+
D = unknown,
|
|
358
|
+
> extends AsyncDisposable {
|
|
359
|
+
/** Returns the current resource for `key`, or `undefined` if absent. */
|
|
360
|
+
readonly get: (key: K) => BorrowedResource<T> | undefined;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Acquires the shared resource for `key`, creating it on first use.
|
|
364
|
+
*
|
|
365
|
+
* Once started, acquire runs to completion even if the caller aborts its
|
|
366
|
+
* Fiber. Always await the result. A successful result still counts as an
|
|
367
|
+
* acquired lease for `key` and must later be balanced with
|
|
368
|
+
* {@link SharedResourceByKey.release | release}.
|
|
369
|
+
*/
|
|
370
|
+
readonly acquire: (key: K) => Task<BorrowedResource<T>, never, D>;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Releases one previously acquired shared reference for `key`.
|
|
374
|
+
*
|
|
375
|
+
* When the last acquired reference for `key` is released, that key's current
|
|
376
|
+
* resource is disposed and removed from the registry immediately by default.
|
|
377
|
+
* If {@link SharedResourceByKeyOptions.idleDisposeAfter | idleDisposeAfter} is
|
|
378
|
+
* set, disposal and registry removal are scheduled instead and a new acquire
|
|
379
|
+
* for the same key during that delay reuses the current resource.
|
|
380
|
+
*
|
|
381
|
+
* Once started, release runs to completion even if the caller aborts its
|
|
382
|
+
* Fiber. Always await the result instead of assuming no cleanup happened.
|
|
383
|
+
*/
|
|
384
|
+
readonly release: (key: K) => Task<void, never, D>;
|
|
385
|
+
|
|
386
|
+
/** Returns the current acquire count for `key`. Missing keys return `0`. */
|
|
387
|
+
readonly getCount: (key: K) => Task<NonNegativeInt, never, D>;
|
|
388
|
+
|
|
389
|
+
/** Returns current keyed resources and their per-key mutex state. */
|
|
390
|
+
readonly snapshot: () => SharedResourceByKeySnapshot<K, T>;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Snapshot returned by {@link SharedResourceByKey.snapshot}. */
|
|
394
|
+
export interface SharedResourceByKeySnapshot<K, T extends Resource> {
|
|
395
|
+
/** Current borrowed resources by key. */
|
|
396
|
+
readonly resourcesByKey: ReadonlyMap<K, BorrowedResource<T>>;
|
|
397
|
+
|
|
398
|
+
/** Current mutex state for each key in the resource snapshot. */
|
|
399
|
+
readonly mutexByKey: ReadonlyMap<K, SemaphoreSnapshot | null>;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Options for {@link createSharedResourceByKey}. */
|
|
403
|
+
export interface SharedResourceByKeyOptions<K, L = K>
|
|
404
|
+
extends Pick<SharedResourceOptions, "idleDisposeAfter">, LookupOption<K, L> {
|
|
405
|
+
/** Called after `key`'s current resource is disposed and cleared. */
|
|
406
|
+
readonly onDisposed?: (key: K) => void;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Creates {@link SharedResourceByKey}.
|
|
411
|
+
*
|
|
412
|
+
* The `create` Task is scoped to one key. It must not fail, matching
|
|
413
|
+
* {@link createSharedResource}.
|
|
414
|
+
*/
|
|
415
|
+
export function createSharedResourceByKey<
|
|
416
|
+
K = unknown,
|
|
417
|
+
T extends Resource = Resource,
|
|
418
|
+
D = unknown,
|
|
419
|
+
>(
|
|
420
|
+
create: (key: K) => Task<T, never, D>,
|
|
421
|
+
): Task<SharedResourceByKey<K, T, D>, never, D>;
|
|
422
|
+
export function createSharedResourceByKey<K, T extends Resource, D, L = K>(
|
|
423
|
+
create: (key: K) => Task<T, never, D>,
|
|
424
|
+
options: SharedResourceByKeyOptions<K, L>,
|
|
425
|
+
): Task<SharedResourceByKey<K, T, D>, never, D>;
|
|
426
|
+
export function createSharedResourceByKey<
|
|
427
|
+
K = unknown,
|
|
428
|
+
T extends Resource = Resource,
|
|
429
|
+
D = unknown,
|
|
430
|
+
L = K,
|
|
431
|
+
>(
|
|
432
|
+
create: (key: K) => Task<T, never, D>,
|
|
433
|
+
{
|
|
434
|
+
idleDisposeAfter,
|
|
435
|
+
lookup = identity as Lookup<K, L>,
|
|
436
|
+
onDisposed,
|
|
437
|
+
}: SharedResourceByKeyOptions<K, L> = {},
|
|
438
|
+
): Task<SharedResourceByKey<K, T, D>, never, D> {
|
|
439
|
+
return unabortable<SharedResourceByKey<K, T, D>, never, D>((run) => {
|
|
440
|
+
const sharedResourceByKeyRun = run.create();
|
|
441
|
+
const sharedResourcesByKey = createLookupMap<K, SharedResource<T, D>, L>({
|
|
442
|
+
lookup,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const stack = new AsyncDisposableStack();
|
|
446
|
+
|
|
447
|
+
const mutexByKey = stack.use(createMutexByKey<K, L>({ lookup }));
|
|
448
|
+
stack.defer(async () => {
|
|
449
|
+
const stack = new AsyncDisposableStack();
|
|
450
|
+
for (const resource of sharedResourcesByKey.values()) stack.use(resource);
|
|
451
|
+
await stack.disposeAsync();
|
|
452
|
+
sharedResourcesByKey.clear();
|
|
453
|
+
});
|
|
454
|
+
// Register as the last so disposal aborts further calls first.
|
|
455
|
+
stack.use(sharedResourceByKeyRun);
|
|
456
|
+
|
|
457
|
+
const moved = stack.move();
|
|
458
|
+
|
|
459
|
+
return ok({
|
|
460
|
+
get: (key) => sharedResourcesByKey.get(key)?.get(),
|
|
461
|
+
|
|
462
|
+
acquire: (key) =>
|
|
463
|
+
unabortable<BorrowedResource<T>, never, D>(() =>
|
|
464
|
+
sharedResourceByKeyRun(
|
|
465
|
+
mutexByKey.withLock(key, async (run) => {
|
|
466
|
+
let sharedResource = sharedResourcesByKey.get(key);
|
|
467
|
+
|
|
468
|
+
if (!sharedResource) {
|
|
469
|
+
const sharedResourceResult = await run(
|
|
470
|
+
createSharedResource(create(key), {
|
|
471
|
+
idleDisposeAfter,
|
|
472
|
+
onDisposed: () => {
|
|
473
|
+
onDisposed?.(key);
|
|
474
|
+
|
|
475
|
+
void sharedResourceByKeyRun(
|
|
476
|
+
mutexByKey.withLock(key, async () => {
|
|
477
|
+
if (
|
|
478
|
+
sharedResource &&
|
|
479
|
+
sharedResourcesByKey.get(key) === sharedResource &&
|
|
480
|
+
sharedResource.snapshot().isIdle
|
|
481
|
+
) {
|
|
482
|
+
sharedResourcesByKey.delete(key);
|
|
483
|
+
await sharedResource[Symbol.asyncDispose]();
|
|
484
|
+
}
|
|
485
|
+
return ok();
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
assertNotAborted(sharedResourceResult);
|
|
492
|
+
sharedResource = sharedResourceResult.value;
|
|
493
|
+
sharedResourcesByKey.set(key, sharedResource);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return run(sharedResource.acquire);
|
|
497
|
+
}),
|
|
498
|
+
),
|
|
499
|
+
),
|
|
500
|
+
|
|
501
|
+
release: (key) =>
|
|
502
|
+
unabortable<void, never, D>(() =>
|
|
503
|
+
sharedResourceByKeyRun(
|
|
504
|
+
mutexByKey.withLock(key, async () => {
|
|
505
|
+
const sharedResource = sharedResourcesByKey.get(key);
|
|
506
|
+
assert(
|
|
507
|
+
sharedResource,
|
|
508
|
+
"Release must not be called more times than acquire.",
|
|
509
|
+
);
|
|
510
|
+
return sharedResourceByKeyRun(sharedResource.release);
|
|
511
|
+
}),
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
|
|
515
|
+
getCount: (key) => () =>
|
|
516
|
+
sharedResourceByKeyRun(
|
|
517
|
+
mutexByKey.withLock(key, async (run) => {
|
|
518
|
+
const sharedResource = sharedResourcesByKey.get(key);
|
|
519
|
+
if (!sharedResource) return ok(zeroNonNegativeInt);
|
|
520
|
+
return run(sharedResource.getCount);
|
|
521
|
+
}),
|
|
522
|
+
),
|
|
523
|
+
|
|
524
|
+
snapshot: () => {
|
|
525
|
+
const resourcesByKey = new Map<K, BorrowedResource<T>>();
|
|
526
|
+
const mutexSnapshotsByKey = new Map<K, SemaphoreSnapshot | null>();
|
|
527
|
+
|
|
528
|
+
for (const [key, sharedResource] of sharedResourcesByKey.entries()) {
|
|
529
|
+
const resource = sharedResource.get();
|
|
530
|
+
if (!resource) continue;
|
|
531
|
+
|
|
532
|
+
resourcesByKey.set(key, resource);
|
|
533
|
+
mutexSnapshotsByKey.set(key, mutexByKey.snapshot(key));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
resourcesByKey,
|
|
538
|
+
mutexByKey: mutexSnapshotsByKey,
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
[Symbol.asyncDispose]: () => moved.disposeAsync(),
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Shared {@link Resource}s keyed by logical identity and retained by claims.
|
|
549
|
+
*
|
|
550
|
+
* This combines {@link SharedResourceByKey} with claim tracking. Resources are
|
|
551
|
+
* kept alive while at least one claim retains their key.
|
|
552
|
+
*
|
|
553
|
+
* By default, {@link createSharedResourceByKeyWithClaims} uses reference
|
|
554
|
+
* identity for both resource keys and claims, matching native `Map` and `Set`.
|
|
555
|
+
* Callers may instead provide {@link Lookup lookup} functions so logical
|
|
556
|
+
* equality is based on derived stable keys.
|
|
557
|
+
*/
|
|
558
|
+
export interface SharedResourceByKeyWithClaims<
|
|
559
|
+
K,
|
|
560
|
+
C,
|
|
561
|
+
T extends Resource,
|
|
562
|
+
D = unknown,
|
|
563
|
+
> extends AsyncDisposable {
|
|
564
|
+
/** Retains each resource key for `claim`. */
|
|
565
|
+
readonly addClaim: (
|
|
566
|
+
claim: C,
|
|
567
|
+
resourceKeys: ReadonlyArray<K>,
|
|
568
|
+
) => Task<void, never, D>;
|
|
569
|
+
|
|
570
|
+
/** Releases each previously retained resource key for `claim`. */
|
|
571
|
+
readonly removeClaim: (
|
|
572
|
+
claim: C,
|
|
573
|
+
resourceKeys: ReadonlyArray<K>,
|
|
574
|
+
) => Task<void, never, D>;
|
|
575
|
+
|
|
576
|
+
/** Returns the current resource for `key`, or `undefined` if absent. */
|
|
577
|
+
readonly getResource: (key: K) => BorrowedResource<T> | undefined;
|
|
578
|
+
|
|
579
|
+
/** Returns the current unique claims retaining `key`. */
|
|
580
|
+
readonly getClaimsForResource: (key: K) => ReadonlySet<C>;
|
|
581
|
+
|
|
582
|
+
/** Returns the current unique resource keys retained by `claim`. */
|
|
583
|
+
readonly getResourceKeysForClaim: (claim: C) => ReadonlySet<K>;
|
|
584
|
+
|
|
585
|
+
/** Returns the current unique resources retained by `claim`. */
|
|
586
|
+
readonly getResourcesForClaim: (claim: C) => ReadonlySet<BorrowedResource<T>>;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Options for {@link createSharedResourceByKeyWithClaims}. */
|
|
590
|
+
export interface SharedResourceByKeyWithClaimsOptions<
|
|
591
|
+
K,
|
|
592
|
+
C,
|
|
593
|
+
T extends Resource,
|
|
594
|
+
LK = K,
|
|
595
|
+
LC = C,
|
|
596
|
+
> extends Pick<SharedResourceOptions, "idleDisposeAfter"> {
|
|
597
|
+
/** Derives logical identity for resource keys. Defaults to {@link identity}. */
|
|
598
|
+
readonly resourceLookup?: Lookup<K, LK>;
|
|
599
|
+
|
|
600
|
+
/** Derives logical identity for claims. Defaults to {@link identity}. */
|
|
601
|
+
readonly claimLookup?: Lookup<C, LC>;
|
|
602
|
+
|
|
603
|
+
/** Called when a key transitions from zero claims to one claim. */
|
|
604
|
+
readonly onFirstClaimAdded?: (
|
|
605
|
+
claim: C,
|
|
606
|
+
resource: BorrowedResource<T>,
|
|
607
|
+
resourceKey: K,
|
|
608
|
+
) => void;
|
|
609
|
+
|
|
610
|
+
/** Called when a key transitions from one claim to zero claims. */
|
|
611
|
+
readonly onLastClaimRemoved?: (
|
|
612
|
+
claim: C,
|
|
613
|
+
resource: BorrowedResource<T>,
|
|
614
|
+
resourceKey: K,
|
|
615
|
+
) => void;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Creates {@link SharedResourceByKeyWithClaims}.
|
|
620
|
+
*
|
|
621
|
+
* Claim-resource pairs are reference-counted by logical identity. The
|
|
622
|
+
* underlying resource for a key is acquired on the first active claim and
|
|
623
|
+
* released when the last active claim for that key is removed.
|
|
624
|
+
*/
|
|
625
|
+
export function createSharedResourceByKeyWithClaims<
|
|
626
|
+
T extends Resource,
|
|
627
|
+
K = unknown,
|
|
628
|
+
C = unknown,
|
|
629
|
+
D = unknown,
|
|
630
|
+
>(
|
|
631
|
+
create: (key: K) => Task<T, never, D>,
|
|
632
|
+
): Task<SharedResourceByKeyWithClaims<K, C, T, D>, never, D>;
|
|
633
|
+
export function createSharedResourceByKeyWithClaims<
|
|
634
|
+
T extends Resource,
|
|
635
|
+
K,
|
|
636
|
+
C,
|
|
637
|
+
D,
|
|
638
|
+
LK = K,
|
|
639
|
+
LC = C,
|
|
640
|
+
>(
|
|
641
|
+
create: (key: K) => Task<T, never, D>,
|
|
642
|
+
options: SharedResourceByKeyWithClaimsOptions<K, C, T, LK, LC>,
|
|
643
|
+
): Task<SharedResourceByKeyWithClaims<K, C, T, D>, never, D>;
|
|
644
|
+
export function createSharedResourceByKeyWithClaims<
|
|
645
|
+
T extends Resource,
|
|
646
|
+
K = unknown,
|
|
647
|
+
C = unknown,
|
|
648
|
+
D = unknown,
|
|
649
|
+
LK = K,
|
|
650
|
+
LC = C,
|
|
651
|
+
>(
|
|
652
|
+
create: (key: K) => Task<T, never, D>,
|
|
653
|
+
{
|
|
654
|
+
idleDisposeAfter,
|
|
655
|
+
resourceLookup = identity as Lookup<K, LK>,
|
|
656
|
+
claimLookup = identity as Lookup<C, LC>,
|
|
657
|
+
onFirstClaimAdded,
|
|
658
|
+
onLastClaimRemoved,
|
|
659
|
+
}: SharedResourceByKeyWithClaimsOptions<K, C, T, LK, LC> = {},
|
|
660
|
+
): Task<SharedResourceByKeyWithClaims<K, C, T, D>, never, D> {
|
|
661
|
+
return unabortable<SharedResourceByKeyWithClaims<K, C, T, D>, never, D>(
|
|
662
|
+
async (run) => {
|
|
663
|
+
const sharedResourceClaimsRun = run.create();
|
|
664
|
+
await using stack = new AsyncDisposableStack();
|
|
665
|
+
|
|
666
|
+
const keyByClaim = createRelation<C, K, LC, LK>({
|
|
667
|
+
lookupA: claimLookup,
|
|
668
|
+
lookupB: resourceLookup,
|
|
669
|
+
});
|
|
670
|
+
const pairRefCountsByClaim = stack.adopt(
|
|
671
|
+
createLookupMap<C, RefCountByKey<K>, LC>({ lookup: claimLookup }),
|
|
672
|
+
(pairRefCountsByClaim) => {
|
|
673
|
+
for (const pairRefCountByKey of pairRefCountsByClaim.values()) {
|
|
674
|
+
pairRefCountByKey[Symbol.dispose]();
|
|
675
|
+
}
|
|
676
|
+
pairRefCountsByClaim.clear();
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const mutexByKey = stack.use(
|
|
681
|
+
createMutexByKey<K, LK>({ lookup: resourceLookup }),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
stack.defer(() => {
|
|
685
|
+
keyByClaim.clear();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const sharedResourcesByKeyResult = await sharedResourceClaimsRun(
|
|
689
|
+
createSharedResourceByKey(create, {
|
|
690
|
+
idleDisposeAfter,
|
|
691
|
+
lookup: resourceLookup,
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
assertNotAborted(sharedResourcesByKeyResult);
|
|
695
|
+
const sharedResourcesByKey = stack.use(sharedResourcesByKeyResult.value);
|
|
696
|
+
|
|
697
|
+
// Register as the last so disposal aborts further calls first.
|
|
698
|
+
stack.use(sharedResourceClaimsRun);
|
|
699
|
+
|
|
700
|
+
/** Asserts that one call does not contain the same logical key twice. */
|
|
701
|
+
const assertNoDuplicateResourceKeys = (
|
|
702
|
+
resourceKeys: ReadonlyArray<K>,
|
|
703
|
+
) => {
|
|
704
|
+
assert(
|
|
705
|
+
createLookupSet<K, LK>({
|
|
706
|
+
lookup: resourceLookup,
|
|
707
|
+
values: resourceKeys,
|
|
708
|
+
}).size === resourceKeys.length,
|
|
709
|
+
"resourceKeys must not contain lookup duplicates.",
|
|
710
|
+
);
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const moved = stack.move();
|
|
714
|
+
|
|
715
|
+
return ok({
|
|
716
|
+
addClaim: (claim, resourceKeys) =>
|
|
717
|
+
unabortable<void, never, D>(() =>
|
|
718
|
+
sharedResourceClaimsRun(async (run) => {
|
|
719
|
+
assertNoDuplicateResourceKeys(resourceKeys);
|
|
720
|
+
|
|
721
|
+
for (const resourceKey of resourceKeys) {
|
|
722
|
+
const added = await run(
|
|
723
|
+
mutexByKey.withLock(resourceKey, async (run) => {
|
|
724
|
+
let pairRefCountByKey = pairRefCountsByClaim.get(claim);
|
|
725
|
+
if (!pairRefCountByKey) {
|
|
726
|
+
pairRefCountByKey = createRefCountByKey<K, LK>({
|
|
727
|
+
lookup: resourceLookup,
|
|
728
|
+
});
|
|
729
|
+
pairRefCountsByClaim.set(claim, pairRefCountByKey);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (pairRefCountByKey.has(resourceKey)) {
|
|
733
|
+
pairRefCountByKey.increment(resourceKey);
|
|
734
|
+
return ok();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const hasClaimsForKey = keyByClaim.hasB(resourceKey);
|
|
738
|
+
let firstResource: BorrowedResource<T> | undefined;
|
|
739
|
+
if (!hasClaimsForKey) {
|
|
740
|
+
const resourceResult = await run(
|
|
741
|
+
sharedResourcesByKey.acquire(resourceKey),
|
|
742
|
+
);
|
|
743
|
+
assertNotAborted(resourceResult);
|
|
744
|
+
firstResource = resourceResult.value;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
assert(
|
|
748
|
+
keyByClaim.add(claim, resourceKey),
|
|
749
|
+
"Claim-resource relation must be absent before first retain.",
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
pairRefCountByKey.increment(resourceKey);
|
|
753
|
+
|
|
754
|
+
if (firstResource) {
|
|
755
|
+
onFirstClaimAdded?.(claim, firstResource, resourceKey);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return ok();
|
|
759
|
+
}),
|
|
760
|
+
);
|
|
761
|
+
assertNotAborted(added);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return ok();
|
|
765
|
+
}),
|
|
766
|
+
),
|
|
767
|
+
|
|
768
|
+
removeClaim: (claim, resourceKeys) =>
|
|
769
|
+
unabortable<void, never, D>(() =>
|
|
770
|
+
sharedResourceClaimsRun(async (run) => {
|
|
771
|
+
assertNoDuplicateResourceKeys(resourceKeys);
|
|
772
|
+
|
|
773
|
+
for (const resourceKey of resourceKeys) {
|
|
774
|
+
const removed = await run(
|
|
775
|
+
mutexByKey.withLock(resourceKey, async (run) => {
|
|
776
|
+
const pairRefCountByKey = pairRefCountsByClaim.get(claim);
|
|
777
|
+
|
|
778
|
+
assert(
|
|
779
|
+
pairRefCountByKey,
|
|
780
|
+
"Claim-resource pair must not be removed more times than added.",
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
if (pairRefCountByKey.decrement(resourceKey) > 0) {
|
|
784
|
+
return ok();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (pairRefCountByKey.keys().size === 0) {
|
|
788
|
+
pairRefCountsByClaim.delete(claim);
|
|
789
|
+
pairRefCountByKey[Symbol.dispose]();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
assert(
|
|
793
|
+
keyByClaim.remove(claim, resourceKey),
|
|
794
|
+
"Claim-resource relation must exist while its ref count is positive.",
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
if (keyByClaim.hasB(resourceKey)) {
|
|
798
|
+
return ok();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const resource = sharedResourcesByKey.get(resourceKey);
|
|
802
|
+
assert(
|
|
803
|
+
resource,
|
|
804
|
+
"Resource must exist when the last claim is removed.",
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
onLastClaimRemoved?.(claim, resource, resourceKey);
|
|
808
|
+
|
|
809
|
+
const releaseResult = await run(
|
|
810
|
+
sharedResourcesByKey.release(resourceKey),
|
|
811
|
+
);
|
|
812
|
+
assertNotAborted(releaseResult);
|
|
813
|
+
|
|
814
|
+
return ok();
|
|
815
|
+
}),
|
|
816
|
+
);
|
|
817
|
+
assertNotAborted(removed);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return ok();
|
|
821
|
+
}),
|
|
822
|
+
),
|
|
823
|
+
|
|
824
|
+
getResource: (key) => sharedResourcesByKey.get(key),
|
|
825
|
+
|
|
826
|
+
getClaimsForResource: (key) => new Set(keyByClaim.iterateA(key)),
|
|
827
|
+
|
|
828
|
+
getResourceKeysForClaim: (claim) => new Set(keyByClaim.iterateB(claim)),
|
|
829
|
+
|
|
830
|
+
getResourcesForClaim: (claim) => {
|
|
831
|
+
const resources = new Set<BorrowedResource<T>>();
|
|
832
|
+
for (const key of keyByClaim.iterateB(claim)) {
|
|
833
|
+
const resource = sharedResourcesByKey.get(key);
|
|
834
|
+
assert(
|
|
835
|
+
resource,
|
|
836
|
+
"Resource must exist for every retained claim-resource relation.",
|
|
837
|
+
);
|
|
838
|
+
resources.add(resource);
|
|
839
|
+
}
|
|
840
|
+
return resources;
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
[Symbol.asyncDispose]: () => moved.disposeAsync(),
|
|
844
|
+
});
|
|
845
|
+
},
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
interface OwnedResource<T extends Resource> {
|
|
850
|
+
readonly resource: BorrowedResource<T>;
|
|
851
|
+
readonly stack: AsyncDisposableStack;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const createOwnedResource = <T extends Resource>(
|
|
855
|
+
resource: T,
|
|
856
|
+
): OwnedResource<T> => {
|
|
857
|
+
const stack = new AsyncDisposableStack();
|
|
858
|
+
stack.use(resource);
|
|
859
|
+
return { resource, stack };
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// TODO: Make lifecycle callbacks exception-safe. `onDisposed`,
|
|
863
|
+
// `onFirstClaimAdded`, and `onLastClaimRemoved` can currently leave resource
|
|
864
|
+
// bookkeeping in a partially updated state if they throw.
|