@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.
Files changed (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -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.