@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.
Files changed (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
@@ -55,384 +55,633 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
55
55
  var e = new Error(message);
56
56
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
57
57
  });
58
- import { assert, assertNotAborted } from "./Assert.js";
58
+ import { assert, assertNonNullable } from "./Assert.js";
59
59
  import { identity } from "./Function.js";
60
60
  import { createLookupMap, createLookupSet, } from "./Lookup.js";
61
- import { createRefCount, createRefCountByKey, } from "./RefCount.js";
62
- import { createRelation } from "./Relation.js";
61
+ import { createRefCountedRelation } from "./Relation.js";
63
62
  import { ok } from "./Result.js";
64
- import { createMutex, createMutexByKey, sleep, unabortable, } from "./Task.js";
63
+ import { createMutex, createMutexByKey, sleep, } from "./Task.js";
65
64
  import {} from "./Time.js";
66
- import { NonNegativeInt, zeroNonNegativeInt } from "./Type.js";
67
- export { createRefCount, createRefCountByKey, } from "./RefCount.js";
68
- /** Creates {@link ResourceRef}. */
69
- export const createResourceRef = (create) => unabortable(async (run) => {
70
- const env_1 = { stack: [], error: void 0, hasError: false };
71
- try {
72
- const resourceRefRun = run.create();
73
- const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
74
- disposer.use(resourceRefRun);
75
- const initial = await resourceRefRun(create);
76
- if (!initial.ok)
77
- return initial;
78
- let current = createOwnedResource(initial.value);
79
- const mutex = disposer.use(createMutex());
80
- disposer.defer(() => current.stack.disposeAsync());
81
- // Register as the last so disposal aborts further calls first.
82
- // Repeated registration is safe because disposal is idempotent.
83
- disposer.use(resourceRefRun);
84
- const disposables = disposer.move();
85
- return ok({
86
- get: () => resourceRefRun(mutex.withLock(() => ok(current.resource))),
87
- set: (create) => unabortable(() => resourceRefRun(mutex.withLock(async (run) => {
88
- await current.stack.disposeAsync();
89
- const next = await run(create);
90
- if (!next.ok)
91
- return next;
92
- current = createOwnedResource(next.value);
93
- return ok();
94
- }))),
95
- [Symbol.asyncDispose]: () => disposables.disposeAsync(),
65
+ import { NonNegativeInt } from "./Type.js";
66
+ import {} from "./Types.js";
67
+ /**
68
+ * Creates {@link SharedResource}.
69
+ *
70
+ * The `create` Task must not fail. Creation establishes shared resource state
71
+ * and may serve multiple concurrent acquirers, so a recoverable failure does
72
+ * not belong to one lease. Handle recoverable failures inside `create`: retry
73
+ * until creation succeeds (for example with a Schedule), or return a resource
74
+ * whose state models connection and reconnection failures.
75
+ *
76
+ * A successfully returned resource must be live and independently owned. The
77
+ * `create` Task must not bind its disposal to that Task's {@link DisposableRun}
78
+ * with `defer`.
79
+ *
80
+ * Create resources with {@link disposable} when their shape supports it. Its
81
+ * disposal guard makes calls through a lease fail eagerly after resource
82
+ * disposal instead of operating on disposed state.
83
+ *
84
+ * The `create` Task runs while acquisition is locked and must not directly or
85
+ * transitively acquire from the same SharedResource. The lock is non-reentrant,
86
+ * so doing so deadlocks.
87
+ *
88
+ * Internal work runs on a Run created from the Run that executes this Task, so
89
+ * dependencies are captured at creation time. Deps provided to later acquire
90
+ * calls do not change what `create` observes.
91
+ *
92
+ * Lifecycle callbacks must not throw. A throwing
93
+ * {@link SharedResourceOptions.onDisposed | onDisposed} is a defect that panics
94
+ * the Run tree.
95
+ */
96
+ export const createSharedResource = (create, { idleDisposeAfter, onDisposed } = {}) => (run) => {
97
+ const sharedResourceRun = run.create();
98
+ const { leakDetector } = run.deps;
99
+ let current;
100
+ let idleDisposeFiber;
101
+ const heldLeases = new Set();
102
+ const mutex = createMutex();
103
+ const sharedResourceHandle = {};
104
+ // Idle disposal calls this under the mutex. Owner finalization calls it only
105
+ // after child Tasks, including idle disposal, settle, so calls cannot
106
+ // overlap.
107
+ const disposeCurrent = async () => {
108
+ const env_1 = { stack: [], error: void 0, hasError: false };
109
+ try {
110
+ if (!current)
111
+ return;
112
+ const resourceDisposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
113
+ if (onDisposed)
114
+ resourceDisposer.defer(onDisposed);
115
+ resourceDisposer.use(current);
116
+ current = undefined;
117
+ }
118
+ catch (e_1) {
119
+ env_1.error = e_1;
120
+ env_1.hasError = true;
121
+ }
122
+ finally {
123
+ const result_1 = __disposeResources(env_1);
124
+ if (result_1)
125
+ await result_1;
126
+ }
127
+ };
128
+ /** Disposes the current resource unless a new lease arrived meanwhile. */
129
+ const disposeCurrentWhenUnused = mutex.withLock(async () => {
130
+ // For delayed disposal, this is safe to clear unconditionally: a newer
131
+ // idle-dispose fiber would require a release after a completed acquire,
132
+ // but FIFO orders that acquire behind this task. Therefore this is the
133
+ // current fiber or already undefined, never newer. An acquire that got
134
+ // the mutex before this task queued aborted this fiber while it was
135
+ // still sleeping. If this fiber was already queued on the mutex, abort
136
+ // removes its semaphore waiter before grant, so this body cannot run
137
+ // stale.
138
+ idleDisposeFiber = undefined;
139
+ if (heldLeases.size === 0)
140
+ await disposeCurrent();
141
+ return ok();
142
+ });
143
+ // Execution order is LIFO: clear idle state, drain leases, dispose current,
144
+ // then untrack the owner. Separate entries preserve later cleanup when an
145
+ // earlier finalizer defects.
146
+ sharedResourceRun.defer(() => {
147
+ leakDetector.untrack(sharedResourceHandle);
148
+ });
149
+ sharedResourceRun.defer(disposeCurrent);
150
+ sharedResourceRun.defer(() => {
151
+ for (const handle of heldLeases)
152
+ leakDetector.untrack(handle);
153
+ heldLeases.clear();
154
+ });
155
+ sharedResourceRun.defer(() => {
156
+ idleDisposeFiber = undefined;
157
+ });
158
+ const createLease = (created) => {
159
+ const handle = {};
160
+ heldLeases.add(handle);
161
+ const release = () => {
162
+ if (!heldLeases.delete(handle))
163
+ return false;
164
+ leakDetector.untrack(handle);
165
+ if (heldLeases.size > 0)
166
+ return true;
167
+ // During shutdown, disposal owns the resource; release is accounting
168
+ // only.
169
+ if (sharedResourceRun.getState().type !== "Running")
170
+ return true;
171
+ if (idleDisposeAfter) {
172
+ idleDisposeFiber = sharedResourceRun.abortable(async (run) => {
173
+ await run.ok(sleep(idleDisposeAfter));
174
+ return run(disposeCurrentWhenUnused);
175
+ });
176
+ }
177
+ else {
178
+ // Immediate disposal is not retained for acquire to cancel. Starting
179
+ // it synchronously queues or acquires the mutex before release
180
+ // returns. The mutex orders it against acquires, and disposal
181
+ // rechecks the lease count under the lock.
182
+ void sharedResourceRun.abortable(disposeCurrentWhenUnused);
183
+ }
184
+ return true;
185
+ };
186
+ const lease = {
187
+ resource: current,
188
+ created,
189
+ release,
190
+ [Symbol.dispose]: release,
191
+ };
192
+ leakDetector.track(lease, { name: "Lease", isLeaked: () => heldLeases.has(handle) }, handle);
193
+ return lease;
194
+ };
195
+ const sharedResource = {
196
+ // Run on the resource-owned tree so caller abort cannot interrupt acquire
197
+ // and later acquire deps cannot replace the owner's captured deps.
198
+ acquire: () => sharedResourceRun(mutex.withLock(async (run) => {
199
+ if (idleDisposeFiber) {
200
+ idleDisposeFiber.abort();
201
+ idleDisposeFiber = undefined;
202
+ }
203
+ const created = current === undefined;
204
+ current ??= await run.ok(create);
205
+ // Publish ownership before the checkpoint so owner finalization
206
+ // disposes a resource returned after shutdown started.
207
+ run.signal.throwIfAborted();
208
+ return ok(createLease(created));
209
+ })),
210
+ acquireCurrent: () => sharedResourceRun(
211
+ // This body must stay synchronous so owner disposal cannot start
212
+ // between checking current and transferring its lease.
213
+ mutex.withLock(() => {
214
+ if (!current)
215
+ return ok(undefined);
216
+ if (idleDisposeFiber) {
217
+ idleDisposeFiber.abort();
218
+ idleDisposeFiber = undefined;
219
+ }
220
+ return ok(createLease(false));
221
+ })),
222
+ use: (callback) => async (run) => {
223
+ const env_2 = { stack: [], error: void 0, hasError: false };
224
+ try {
225
+ const lease = __addDisposableResource(env_2, await run.ok(sharedResource.acquire), false);
226
+ return await run(callback(lease.resource, lease.created));
227
+ }
228
+ catch (e_2) {
229
+ env_2.error = e_2;
230
+ env_2.hasError = true;
231
+ }
232
+ finally {
233
+ __disposeResources(env_2);
234
+ }
235
+ },
236
+ snapshot: () => {
237
+ const mutexSnapshot = mutex.snapshot();
238
+ return {
239
+ isIdle: heldLeases.size === 0 &&
240
+ !current &&
241
+ !idleDisposeFiber &&
242
+ mutexSnapshot.isIdle,
243
+ leaseCount: NonNegativeInt.orThrow(heldLeases.size),
244
+ hasResource: current !== undefined,
245
+ idleDisposePending: idleDisposeFiber !== undefined,
246
+ mutex: mutexSnapshot,
247
+ };
248
+ },
249
+ [Symbol.asyncDispose]: () => sharedResourceRun[Symbol.asyncDispose](),
250
+ };
251
+ leakDetector.track(sharedResource, {
252
+ name: "SharedResource",
253
+ isLeaked: () => sharedResourceRun.getState().type === "Running",
254
+ }, sharedResourceHandle);
255
+ return ok(sharedResource);
256
+ };
257
+ export function createSharedResourceByKey(create, { idleDisposeAfter, lookup = identity, onDisposed, } = {}) {
258
+ return (run) => {
259
+ const sharedResourceByKeyRun = run.create();
260
+ const { leakDetector } = run.deps;
261
+ const sharedResourcesByKey = createLookupMap({
262
+ lookup,
96
263
  });
97
- }
98
- catch (e_1) {
99
- env_1.error = e_1;
100
- env_1.hasError = true;
101
- }
102
- finally {
103
- const result_1 = __disposeResources(env_1);
104
- if (result_1)
105
- await result_1;
106
- }
107
- });
108
- /** Creates {@link SharedResource}. */
109
- export const createSharedResource = (create, { idleDisposeAfter, onDisposed } = {}) => unabortable(async (run) => {
110
- const env_2 = { stack: [], error: void 0, hasError: false };
111
- try {
112
- const sharedResourceRun = run.create();
113
- let current;
114
- let idleDisposeFiber;
115
- const disposer = __addDisposableResource(env_2, new AsyncDisposableStack(), true);
116
- const refCount = disposer.use(createRefCount());
117
- const mutex = disposer.use(createMutex());
118
- const disposeCurrent = async () => {
264
+ const mutexByKey = createMutexByKey({ lookup });
265
+ const disposer = new AsyncDisposableStack();
266
+ const sharedResourceByKeyHandle = {};
267
+ const removeKey = (key, sharedResource) => {
268
+ // Callers must ensure no same-key operation can still use sharedResource.
269
+ // onDisposed checks that the key mutex is idle; acquireCurrent holds the
270
+ // key mutex after observing that the inner resource is empty.
271
+ // Explicit registry disposal owns every registered SharedResource. Keep
272
+ // the key visible until keyDisposer captures it.
273
+ if (disposer.disposed)
274
+ return;
275
+ sharedResourcesByKey.delete(key);
276
+ // During root shutdown, the inner Run is already being disposed.
277
+ if (sharedResourceByKeyRun.getState().type !== "Running")
278
+ return;
279
+ void sharedResourceByKeyRun.abortable(async () => {
280
+ await sharedResource[Symbol.asyncDispose]();
281
+ return ok();
282
+ });
283
+ };
284
+ // Disposed in reverse order: the Run first, so pending acquires and removed
285
+ // SharedResource disposals are aborted and awaited before the registered
286
+ // SharedResources are disposed.
287
+ disposer.defer(async () => {
119
288
  const env_3 = { stack: [], error: void 0, hasError: false };
120
289
  try {
121
- if (!current)
122
- return;
123
- const disposer = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
124
- if (onDisposed)
125
- disposer.defer(onDisposed);
126
- disposer.use(current.stack);
127
- current = undefined;
290
+ const keyDisposer = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
291
+ for (const sharedResource of sharedResourcesByKey.values())
292
+ keyDisposer.use(sharedResource);
293
+ sharedResourcesByKey.clear();
128
294
  }
129
295
  catch (e_3) {
130
296
  env_3.error = e_3;
131
297
  env_3.hasError = true;
132
298
  }
133
299
  finally {
134
- const result_3 = __disposeResources(env_3);
135
- if (result_3)
136
- await result_3;
300
+ const result_2 = __disposeResources(env_3);
301
+ if (result_2)
302
+ await result_2;
137
303
  }
304
+ });
305
+ disposer.use(sharedResourceByKeyRun);
306
+ sharedResourceByKeyRun.defer(() => {
307
+ leakDetector.untrack(sharedResourceByKeyHandle);
308
+ });
309
+ const releaseLeaseIfAborted = (signal, lease) => {
310
+ if (!signal.aborted)
311
+ return;
312
+ lease?.release();
313
+ signal.throwIfAborted();
138
314
  };
139
- disposer.defer(disposeCurrent);
140
- // Register as the last so disposal aborts further calls first.
141
- disposer.use(sharedResourceRun);
142
- const disposables = disposer.move();
143
- return ok({
144
- acquire: unabortable(() => sharedResourceRun(mutex.withLock(async (run) => {
145
- if (idleDisposeFiber) {
146
- idleDisposeFiber.abort();
147
- idleDisposeFiber = undefined;
315
+ const sharedResourceByKey = {
316
+ acquire: (key) => () => sharedResourceByKeyRun(mutexByKey.withLock(key, async (run) => {
317
+ // The outer key mutex spans registration and the inner acquire so
318
+ // key removal cannot race resource creation or lease transfer.
319
+ let sharedResource = sharedResourcesByKey.get(key);
320
+ if (!sharedResource) {
321
+ sharedResource = await run.ok(createSharedResource(create(key), {
322
+ idleDisposeAfter,
323
+ onDisposed: () => {
324
+ assertNonNullable(sharedResource, "SharedResource must be assigned before its resource is disposed.");
325
+ if (mutexByKey.isIdle(key))
326
+ removeKey(key, sharedResource);
327
+ onDisposed?.(key);
328
+ },
329
+ }));
330
+ sharedResourcesByKey.set(key, sharedResource);
148
331
  }
149
- if (!current) {
150
- const resource = await run(create);
151
- if (!resource.ok)
152
- return resource;
153
- current = createOwnedResource(resource.value);
332
+ const lease = await run.ok(sharedResource.acquire);
333
+ releaseLeaseIfAborted(run.signal, lease);
334
+ return ok(lease);
335
+ })),
336
+ acquireCurrent: (key) => () => sharedResourceByKeyRun(mutexByKey.withLock(key, async (run) => {
337
+ const sharedResource = sharedResourcesByKey.get(key);
338
+ if (!sharedResource)
339
+ return ok(undefined);
340
+ const lease = await run.ok(sharedResource.acquireCurrent);
341
+ releaseLeaseIfAborted(run.signal, lease);
342
+ if (!lease)
343
+ removeKey(key, sharedResource);
344
+ return ok(lease);
345
+ })),
346
+ use: (key, callback) => async (run) => {
347
+ const env_4 = { stack: [], error: void 0, hasError: false };
348
+ try {
349
+ const lease = __addDisposableResource(env_4, await run.ok(sharedResourceByKey.acquire(key)), false);
350
+ return await run(callback(lease.resource, lease.created));
154
351
  }
155
- refCount.increment();
156
- return ok(current.resource);
157
- }))),
158
- release: unabortable(() => sharedResourceRun(mutex.withLock(async () => {
159
- if (refCount.decrement() > 0)
160
- return ok();
161
- if (!idleDisposeAfter) {
162
- await disposeCurrent();
163
- return ok();
352
+ catch (e_4) {
353
+ env_4.error = e_4;
354
+ env_4.hasError = true;
164
355
  }
165
- idleDisposeFiber = sharedResourceRun(async (run) => {
166
- const slept = await run(sleep(idleDisposeAfter));
167
- if (!slept.ok)
168
- return slept;
169
- return run(mutex.withLock(async () => {
170
- idleDisposeFiber = undefined;
171
- await disposeCurrent();
172
- return ok();
173
- }));
174
- });
175
- return ok();
176
- }))),
177
- getCount: () => sharedResourceRun(mutex.withLock(() => ok(refCount.getCount()))),
178
- get: () => current?.resource,
179
- snapshot: () => ({
180
- isIdle: refCount.getCount() === 0 && !current && !idleDisposeFiber,
181
- mutex: mutex.snapshot(),
182
- }),
183
- [Symbol.asyncDispose]: () => disposables.disposeAsync(),
184
- });
185
- }
186
- catch (e_2) {
187
- env_2.error = e_2;
188
- env_2.hasError = true;
189
- }
190
- finally {
191
- const result_2 = __disposeResources(env_2);
192
- if (result_2)
193
- await result_2;
194
- }
195
- });
196
- export function createSharedResourceByKey(create, { idleDisposeAfter, lookup = identity, onDisposed, } = {}) {
197
- return unabortable(async (run) => {
198
- const env_4 = { stack: [], error: void 0, hasError: false };
199
- try {
200
- const sharedResourceByKeyRun = run.create();
201
- const sharedResourcesByKey = createLookupMap({
202
- lookup,
203
- });
204
- const disposer = __addDisposableResource(env_4, new AsyncDisposableStack(), true);
205
- const mutexByKey = disposer.use(createMutexByKey({ lookup }));
206
- disposer.defer(async () => {
356
+ finally {
357
+ __disposeResources(env_4);
358
+ }
359
+ },
360
+ forEachCurrent: (callback) => async (run) => {
207
361
  const env_5 = { stack: [], error: void 0, hasError: false };
208
362
  try {
209
- const disposer = __addDisposableResource(env_5, new AsyncDisposableStack(), true);
210
- for (const resource of sharedResourcesByKey.values())
211
- disposer.use(resource);
212
- await disposer.disposeAsync();
213
- sharedResourcesByKey.clear();
363
+ const leaseDisposer = __addDisposableResource(env_5, new DisposableStack(), false);
364
+ const leasesByKey = [];
365
+ for (const [key] of [...sharedResourcesByKey]) {
366
+ const lease = await run.ok(sharedResourceByKey.acquireCurrent(key));
367
+ if (lease)
368
+ leasesByKey.push([leaseDisposer.use(lease), key]);
369
+ run.signal.throwIfAborted();
370
+ }
371
+ for (const [lease, key] of leasesByKey)
372
+ callback(lease.resource, key);
373
+ return ok();
214
374
  }
215
375
  catch (e_5) {
216
376
  env_5.error = e_5;
217
377
  env_5.hasError = true;
218
378
  }
219
379
  finally {
220
- const result_5 = __disposeResources(env_5);
221
- if (result_5)
222
- await result_5;
380
+ __disposeResources(env_5);
223
381
  }
224
- });
225
- // Register as the last so disposal aborts further calls first.
226
- disposer.use(sharedResourceByKeyRun);
227
- const disposables = disposer.move();
228
- return ok({
229
- acquire: (key) => unabortable(() => sharedResourceByKeyRun(mutexByKey.withLock(key, async (run) => {
230
- let sharedResource = sharedResourcesByKey.get(key);
231
- if (!sharedResource) {
232
- const sharedResourceResult = await run(createSharedResource(create(key), {
233
- idleDisposeAfter,
234
- onDisposed: () => {
235
- onDisposed?.(key);
236
- void sharedResourceByKeyRun(mutexByKey.withLock(key, async () => {
237
- if (sharedResource &&
238
- sharedResourcesByKey.get(key) === sharedResource &&
239
- sharedResource.snapshot().isIdle) {
240
- sharedResourcesByKey.delete(key);
241
- await sharedResource[Symbol.asyncDispose]();
242
- }
243
- return ok();
244
- }));
245
- },
246
- }));
247
- assertNotAborted(sharedResourceResult);
248
- sharedResource = sharedResourceResult.value;
249
- sharedResourcesByKey.set(key, sharedResource);
382
+ },
383
+ snapshot: () => {
384
+ const resourcesByKey = new Map();
385
+ for (const [key, sharedResource] of sharedResourcesByKey)
386
+ resourcesByKey.set(key, sharedResource.snapshot());
387
+ return { resourcesByKey };
388
+ },
389
+ [Symbol.asyncDispose]: () => disposer.disposeAsync(),
390
+ };
391
+ leakDetector.track(sharedResourceByKey, {
392
+ name: "SharedResourceByKey",
393
+ isLeaked: () => sharedResourceByKeyRun.getState().type === "Running",
394
+ }, sharedResourceByKeyHandle);
395
+ return ok(sharedResourceByKey);
396
+ };
397
+ }
398
+ /**
399
+ * Creates {@link SharedResourceByKeyWithClaims}.
400
+ *
401
+ * The `create` callback is called with a resource key when that resource must
402
+ * be created. The returned Task must not fail. Its dependencies are captured
403
+ * when the registry is created; later claim calls do not require or replace
404
+ * them.
405
+ *
406
+ * Transition callbacks represent semantic claim-resource pair changes and are
407
+ * not called when this registry or its Run tree drains claims during disposal.
408
+ * They must not throw. If a first-claim callback defects after earlier
409
+ * callbacks in the same claim completed, matching last-claim callbacks
410
+ * compensate those completed transitions in reverse order before cleanup.
411
+ * Relation reads during compensation still include the failed claim. Callback
412
+ * defects panic the owner Run after claim metadata and resource ownership are
413
+ * cleaned up.
414
+ *
415
+ * Transition callbacks may synchronously read relation state. They must not
416
+ * call `claim`, release a {@link ClaimLease} owned by this registry, or dispose
417
+ * this registry.
418
+ *
419
+ * Transition callbacks do not report actual resource disposal. Disposal can
420
+ * happen later when `idleDisposeAfter` is configured.
421
+ */
422
+ export const createSharedResourceByKeyWithClaims = (create, { idleDisposeAfter, resourceLookup = identity, claimLookup = identity, onFirstClaimAdded, onLastClaimRemoved, } = {}) => async (run) => {
423
+ const env_6 = { stack: [], error: void 0, hasError: false };
424
+ try {
425
+ const sharedResourceByKeyWithClaimsRun = run.create();
426
+ const { leakDetector } = run.deps;
427
+ const claimResourceRelation = createRefCountedRelation({
428
+ lookupA: claimLookup,
429
+ lookupB: resourceLookup,
430
+ });
431
+ // This map and the relation's resource-key index use the same lookup and
432
+ // are inserted and removed together, so they keep the same canonical keys.
433
+ const resourcesByKey = createLookupMap({
434
+ lookup: resourceLookup,
435
+ });
436
+ const heldClaimLeases = new Set();
437
+ const sharedResourceByKeyWithClaimsHandle = {};
438
+ const notifyLastClaimRemoved = (claim, resource, resourceKey) => {
439
+ try {
440
+ onLastClaimRemoved?.(claim, resource, resourceKey);
441
+ }
442
+ catch (defect) {
443
+ sharedResourceByKeyWithClaimsRun.panic(defect);
444
+ }
445
+ };
446
+ const disposer = __addDisposableResource(env_6, new AsyncDisposableStack(), true);
447
+ // Explicit disposal stops claim operations and runs their finalizers before
448
+ // disposing the keyed registry. The nested stack is empty if keyed-registry
449
+ // creation fails. Both internal Runs attach to the root, whose disposal
450
+ // awaits them independently of this explicit-disposal stack.
451
+ const sharedResourceByKeyDisposer = disposer.use(new AsyncDisposableStack());
452
+ disposer.use(sharedResourceByKeyWithClaimsRun);
453
+ const releaseClaimLease = (heldClaimLease, notifyTransition = true) => {
454
+ const env_7 = { stack: [], error: void 0, hasError: false };
455
+ try {
456
+ if (!heldClaimLeases.delete(heldClaimLease))
457
+ return false;
458
+ leakDetector.untrack(heldClaimLease);
459
+ const _resourceLeases = __addDisposableResource(env_7, heldClaimLease.resourceLeases, false);
460
+ for (const resourceKey of heldClaimLease.resourceKeys) {
461
+ const decremented = claimResourceRelation.decrement(heldClaimLease.claim, resourceKey);
462
+ if (decremented.count > 0)
463
+ continue;
464
+ const resource = resourcesByKey.get(decremented.b);
465
+ assertNonNullable(resource, "Resource must exist while its claim-resource pair is retained.");
466
+ if (notifyTransition) {
467
+ notifyLastClaimRemoved(decremented.a, resource, decremented.b);
250
468
  }
251
- return run(sharedResource.acquire);
252
- }))),
253
- release: (key) => unabortable(() => sharedResourceByKeyRun(mutexByKey.withLock(key, async () => {
254
- const sharedResource = sharedResourcesByKey.get(key);
255
- assert(sharedResource, "Release must not be called more times than acquire.");
256
- return sharedResourceByKeyRun(sharedResource.release);
257
- }))),
258
- getCount: (key) => () => sharedResourceByKeyRun(mutexByKey.withLock(key, async (run) => {
259
- const sharedResource = sharedResourcesByKey.get(key);
260
- if (!sharedResource)
261
- return ok(zeroNonNegativeInt);
262
- return run(sharedResource.getCount);
263
- })),
264
- get: (key) => sharedResourcesByKey.get(key)?.get(),
265
- snapshot: () => {
266
- const resourcesByKey = new Map();
267
- const mutexSnapshotsByKey = new Map();
268
- for (const [key, sharedResource] of sharedResourcesByKey.entries()) {
269
- const resource = sharedResource.get();
270
- if (!resource)
271
- continue;
272
- resourcesByKey.set(key, resource);
273
- mutexSnapshotsByKey.set(key, mutexByKey.snapshot(key));
469
+ if (!claimResourceRelation.hasB(decremented.b)) {
470
+ // Delete before _resourceLeases disposes at function exit. While this
471
+ // entry exists, its leases pin one resource generation.
472
+ resourcesByKey.delete(decremented.b);
274
473
  }
275
- return {
276
- resourcesByKey,
277
- mutexByKey: mutexSnapshotsByKey,
278
- };
279
- },
280
- [Symbol.asyncDispose]: () => disposables.disposeAsync(),
281
- });
282
- }
283
- catch (e_4) {
284
- env_4.error = e_4;
285
- env_4.hasError = true;
286
- }
287
- finally {
288
- const result_4 = __disposeResources(env_4);
289
- if (result_4)
290
- await result_4;
291
- }
292
- });
293
- }
294
- export function createSharedResourceByKeyWithClaims(create, { idleDisposeAfter, resourceLookup = identity, claimLookup = identity, onFirstClaimAdded, onLastClaimRemoved, } = {}) {
295
- return unabortable(async (run) => {
296
- const env_6 = { stack: [], error: void 0, hasError: false };
297
- try {
298
- const sharedResourceClaimsRun = run.create();
299
- const disposer = __addDisposableResource(env_6, new AsyncDisposableStack(), true);
300
- const keyByClaim = createRelation({
301
- lookupA: claimLookup,
302
- lookupB: resourceLookup,
303
- });
304
- const pairRefCountsByClaim = disposer.adopt(createLookupMap({ lookup: claimLookup }), (pairRefCountsByClaim) => {
305
- const env_7 = { stack: [], error: void 0, hasError: false };
306
- try {
307
- const disposer = __addDisposableResource(env_7, new DisposableStack(), false);
474
+ }
475
+ return true;
476
+ }
477
+ catch (e_7) {
478
+ env_7.error = e_7;
479
+ env_7.hasError = true;
480
+ }
481
+ finally {
482
+ __disposeResources(env_7);
483
+ }
484
+ };
485
+ const drainClaimLeases = () => {
486
+ const env_8 = { stack: [], error: void 0, hasError: false };
487
+ try {
488
+ const disposer = __addDisposableResource(env_8, new DisposableStack(), false);
489
+ disposer.defer(() => {
490
+ claimResourceRelation.clear();
491
+ resourcesByKey.clear();
492
+ });
493
+ for (const heldClaimLease of heldClaimLeases) {
308
494
  disposer.defer(() => {
309
- pairRefCountsByClaim.clear();
495
+ releaseClaimLease(heldClaimLease, false);
310
496
  });
311
- for (const pairRefCountByKey of pairRefCountsByClaim.values()) {
312
- disposer.use(pairRefCountByKey);
313
- }
314
- }
315
- catch (e_7) {
316
- env_7.error = e_7;
317
- env_7.hasError = true;
318
- }
319
- finally {
320
- __disposeResources(env_7);
321
497
  }
322
- });
323
- const mutexByKey = disposer.use(createMutexByKey({ lookup: resourceLookup }));
324
- disposer.defer(() => {
325
- keyByClaim.clear();
326
- });
327
- const sharedResourcesByKeyResult = await sharedResourceClaimsRun(createSharedResourceByKey(create, {
328
- idleDisposeAfter,
329
- lookup: resourceLookup,
330
- }));
331
- assertNotAborted(sharedResourcesByKeyResult);
332
- const sharedResourcesByKey = disposer.use(sharedResourcesByKeyResult.value);
333
- // Register as the last so disposal aborts further calls first.
334
- disposer.use(sharedResourceClaimsRun);
335
- /** Asserts that one call does not contain the same logical key twice. */
336
- const assertNoDuplicateResourceKeys = (resourceKeys) => {
337
- assert(createLookupSet({
338
- lookup: resourceLookup,
339
- values: resourceKeys,
340
- }).size === resourceKeys.length, "resourceKeys must not contain lookup duplicates.");
341
- };
342
- const disposables = disposer.move();
343
- return ok({
344
- addClaim: (claim, resourceKeys) => unabortable(() => sharedResourceClaimsRun(async (run) => {
345
- assertNoDuplicateResourceKeys(resourceKeys);
346
- for (const resourceKey of resourceKeys) {
347
- const added = await run(mutexByKey.withLock(resourceKey, async (run) => {
348
- let pairRefCountByKey = pairRefCountsByClaim.get(claim);
349
- if (!pairRefCountByKey) {
350
- pairRefCountByKey = createRefCountByKey({
351
- lookup: resourceLookup,
352
- });
353
- pairRefCountsByClaim.set(claim, pairRefCountByKey);
354
- }
355
- if (pairRefCountByKey.has(resourceKey)) {
356
- pairRefCountByKey.increment(resourceKey);
357
- return ok();
358
- }
359
- const hasClaimsForKey = keyByClaim.hasB(resourceKey);
360
- let firstResource;
361
- if (!hasClaimsForKey) {
362
- const resourceResult = await run(sharedResourcesByKey.acquire(resourceKey));
363
- assertNotAborted(resourceResult);
364
- firstResource = resourceResult.value;
365
- }
366
- assert(keyByClaim.add(claim, resourceKey), "Claim-resource relation must be absent before first retain.");
367
- pairRefCountByKey.increment(resourceKey);
368
- if (firstResource) {
369
- onFirstClaimAdded?.(claim, firstResource, resourceKey);
370
- }
371
- return ok();
372
- }));
373
- assertNotAborted(added);
498
+ }
499
+ catch (e_8) {
500
+ env_8.error = e_8;
501
+ env_8.hasError = true;
502
+ }
503
+ finally {
504
+ __disposeResources(env_8);
505
+ }
506
+ };
507
+ sharedResourceByKeyWithClaimsRun.defer(() => {
508
+ leakDetector.untrack(sharedResourceByKeyWithClaimsHandle);
509
+ });
510
+ sharedResourceByKeyWithClaimsRun.defer(drainClaimLeases);
511
+ const sharedResourceByKey = sharedResourceByKeyDisposer.use(await sharedResourceByKeyWithClaimsRun.ok(createSharedResourceByKey(create, {
512
+ idleDisposeAfter,
513
+ lookup: resourceLookup,
514
+ })));
515
+ run.signal.throwIfAborted();
516
+ const disposables = disposer.move();
517
+ const acquireClaim = (claim, resourceKeys, project) => () => {
518
+ const resourceKeysSnapshot = [
519
+ resourceKeys[0],
520
+ ...resourceKeys.slice(1),
521
+ ];
522
+ return sharedResourceByKeyWithClaimsRun(async (run) => {
523
+ const env_9 = { stack: [], error: void 0, hasError: false };
524
+ try {
525
+ assert(createLookupSet({
526
+ lookup: resourceLookup,
527
+ values: resourceKeysSnapshot,
528
+ }).size === resourceKeysSnapshot.length, "resourceKeys must not contain lookup duplicates.");
529
+ const resourceLeases = __addDisposableResource(env_9, new DisposableStack(), false);
530
+ const [firstResourceKey, ...remainingResourceKeys] = resourceKeysSnapshot;
531
+ // acquire releases a transferred lease before surfacing owner abort,
532
+ // so a throwing await cannot leave ownership uncollected.
533
+ const firstResourceLease = resourceLeases.use(await run.ok(sharedResourceByKey.acquire(firstResourceKey)));
534
+ const resources = [
535
+ [firstResourceLease.resource, firstResourceKey],
536
+ ];
537
+ const firstClaimTransitions = [];
538
+ for (const resourceKey of remainingResourceKeys) {
539
+ const resourceLease = resourceLeases.use(await run.ok(sharedResourceByKey.acquire(resourceKey)));
540
+ resources.push([resourceLease.resource, resourceKey]);
374
541
  }
375
- return ok();
376
- })),
377
- removeClaim: (claim, resourceKeys) => unabortable(() => sharedResourceClaimsRun(async (run) => {
378
- assertNoDuplicateResourceKeys(resourceKeys);
379
- for (const resourceKey of resourceKeys) {
380
- const removed = await run(mutexByKey.withLock(resourceKey, async (run) => {
381
- const pairRefCountByKey = pairRefCountsByClaim.get(claim);
382
- assert(pairRefCountByKey, "Claim-resource pair must not be removed more times than added.");
383
- if (pairRefCountByKey.decrement(resourceKey) > 0) {
384
- return ok();
385
- }
386
- if (pairRefCountByKey.keys().size === 0) {
387
- pairRefCountsByClaim.delete(claim);
388
- pairRefCountByKey[Symbol.dispose]();
389
- }
390
- assert(keyByClaim.remove(claim, resourceKey), "Claim-resource relation must exist while its ref count is positive.");
391
- if (keyByClaim.hasB(resourceKey)) {
392
- return ok();
542
+ run.signal.throwIfAborted();
543
+ for (const [resource, resourceKey] of resources) {
544
+ const incremented = claimResourceRelation.increment(claim, resourceKey);
545
+ resourcesByKey.getOrInsert(incremented.b, resource);
546
+ if (incremented.count === 1) {
547
+ firstClaimTransitions.push([
548
+ incremented.a,
549
+ resource,
550
+ incremented.b,
551
+ ]);
552
+ }
553
+ }
554
+ const heldClaimLease = {
555
+ claim,
556
+ resourceKeys: resourceKeysSnapshot,
557
+ resourceLeases: resourceLeases.move(),
558
+ };
559
+ heldClaimLeases.add(heldClaimLease);
560
+ const release = () => releaseClaimLease(heldClaimLease);
561
+ const claimLease = {
562
+ release,
563
+ [Symbol.dispose]: release,
564
+ };
565
+ leakDetector.track(claimLease, {
566
+ name: "ClaimLease",
567
+ isLeaked: () => heldClaimLeases.has(heldClaimLease),
568
+ }, heldClaimLease);
569
+ try {
570
+ const env_10 = { stack: [], error: void 0, hasError: false };
571
+ try {
572
+ let succeeded = false;
573
+ const compensations = __addDisposableResource(env_10, new DisposableStack(), false);
574
+ for (const [claim, resource, resourceKey,] of firstClaimTransitions) {
575
+ onFirstClaimAdded?.(claim, resource, resourceKey);
576
+ compensations.defer(() => {
577
+ if (!succeeded) {
578
+ notifyLastClaimRemoved(claim, resource, resourceKey);
579
+ }
580
+ });
393
581
  }
394
- const resource = sharedResourcesByKey.get(resourceKey);
395
- assert(resource, "Resource must exist when the last claim is removed.");
396
- onLastClaimRemoved?.(claim, resource, resourceKey);
397
- const releaseResult = await run(sharedResourcesByKey.release(resourceKey));
398
- assertNotAborted(releaseResult);
399
- return ok();
400
- }));
401
- assertNotAborted(removed);
582
+ succeeded = true;
583
+ }
584
+ catch (e_9) {
585
+ env_10.error = e_9;
586
+ env_10.hasError = true;
587
+ }
588
+ finally {
589
+ __disposeResources(env_10);
590
+ }
402
591
  }
403
- return ok();
404
- })),
405
- getResource: (key) => sharedResourcesByKey.get(key),
406
- getClaimsForResource: (key) => new Set(keyByClaim.iterateA(key)),
407
- getResourceKeysForClaim: (claim) => new Set(keyByClaim.iterateB(claim)),
408
- getResourcesForClaim: (claim) => {
409
- const resources = new Set();
410
- for (const key of keyByClaim.iterateB(claim)) {
411
- const resource = sharedResourcesByKey.get(key);
412
- assert(resource, "Resource must exist for every retained claim-resource relation.");
413
- resources.add(resource);
592
+ catch (defect) {
593
+ releaseClaimLease(heldClaimLease, false);
594
+ throw defect;
414
595
  }
415
- return resources;
416
- },
417
- [Symbol.asyncDispose]: () => disposables.disposeAsync(),
596
+ return ok(project(claimLease, resources));
597
+ }
598
+ catch (e_10) {
599
+ env_9.error = e_10;
600
+ env_9.hasError = true;
601
+ }
602
+ finally {
603
+ __disposeResources(env_9);
604
+ }
418
605
  });
419
- }
420
- catch (e_6) {
421
- env_6.error = e_6;
422
- env_6.hasError = true;
423
- }
424
- finally {
425
- const result_6 = __disposeResources(env_6);
426
- if (result_6)
427
- await result_6;
428
- }
429
- });
430
- }
431
- const createOwnedResource = (resource) => {
432
- const stack = new AsyncDisposableStack();
433
- stack.use(resource);
434
- return { resource, stack };
606
+ };
607
+ const sharedResourceByKeyWithClaims = {
608
+ claim: (claim, resourceKeys) => acquireClaim(claim, resourceKeys, (claimLease) => claimLease),
609
+ use: (claim, resourceKeys, callback) => async (run) => {
610
+ const env_11 = { stack: [], error: void 0, hasError: false };
611
+ try {
612
+ const acquired = await run.ok(acquireClaim(claim, resourceKeys, (claimLease, resources) => ({
613
+ claimLease,
614
+ resources,
615
+ })));
616
+ const _claimLease = __addDisposableResource(env_11, acquired.claimLease, false);
617
+ return await run(callback(acquired.resources));
618
+ }
619
+ catch (e_11) {
620
+ env_11.error = e_11;
621
+ env_11.hasError = true;
622
+ }
623
+ finally {
624
+ __disposeResources(env_11);
625
+ }
626
+ },
627
+ getClaimsForResource: (resourceKey) => new Set(claimResourceRelation.getAs(resourceKey)),
628
+ getResourceKeysForClaim: (claim) => new Set(claimResourceRelation.getBs(claim)),
629
+ forEachResourceForClaim: (claim, callback) => {
630
+ const resources = claimResourceRelation
631
+ .getBs(claim)
632
+ .map((resourceKey) => {
633
+ const resource = resourcesByKey.get(resourceKey);
634
+ assertNonNullable(resource, "Resource must exist for every active claim-resource relation.");
635
+ return [resource, resourceKey];
636
+ });
637
+ // Releasing the final ClaimLease from a callback only schedules inner
638
+ // disposal: the mutex Task resumes after this synchronous frame, so all
639
+ // snapshotted resources remain live through the loop.
640
+ for (const [resource, resourceKey] of resources) {
641
+ callback(resource, resourceKey);
642
+ }
643
+ },
644
+ snapshot: () => {
645
+ const retainCountsByResourceKeyByClaimSnapshot = new Map();
646
+ for (const [claim, resourceKey, count,] of claimResourceRelation.getEntries()) {
647
+ retainCountsByResourceKeyByClaimSnapshot
648
+ .getOrInsertComputed(claim, () => new Map())
649
+ .set(resourceKey, count);
650
+ }
651
+ return {
652
+ claimLeaseCount: NonNegativeInt.orThrow(heldClaimLeases.size),
653
+ retainCountsByResourceKeyByClaim: retainCountsByResourceKeyByClaimSnapshot,
654
+ resourcesByKey: sharedResourceByKey.snapshot().resourcesByKey,
655
+ };
656
+ },
657
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
658
+ };
659
+ leakDetector.track(sharedResourceByKeyWithClaims, {
660
+ name: "SharedResourceByKeyWithClaims",
661
+ isLeaked: () => sharedResourceByKeyWithClaimsRun.getState().type === "Running",
662
+ }, sharedResourceByKeyWithClaimsHandle);
663
+ return ok(sharedResourceByKeyWithClaims);
664
+ }
665
+ catch (e_6) {
666
+ env_6.error = e_6;
667
+ env_6.hasError = true;
668
+ }
669
+ finally {
670
+ const result_3 = __disposeResources(env_6);
671
+ if (result_3)
672
+ await result_3;
673
+ }
435
674
  };
436
- // TODO: Make lifecycle callbacks exception-safe. `onDisposed`,
437
- // `onFirstClaimAdded`, and `onLastClaimRemoved` can currently leave resource
438
- // bookkeeping in a partially updated state if they throw.
675
+ // TODO: Add a lease-based reloadable resource when a concrete use case needs
676
+ // resource swapping. A ResourceRef-style get/set API is unsafe because set can
677
+ // dispose a resource still held by a caller of get. Model resource generations
678
+ // explicitly and distinguish two replacement policies, probably as separate
679
+ // APIs rather than a boolean option:
680
+ // - Overlapping: create and validate the replacement, publish it to new leases,
681
+ // then dispose the old generation after its existing leases drain. This
682
+ // supports zero-downtime reload and keeps the old generation when creation
683
+ // fails, but both generations temporarily exist.
684
+ // - Exclusive: stop or queue new leases, drain and dispose the old generation,
685
+ // then create and publish the replacement. This guarantees at most one live
686
+ // resource, but introduces downtime and leaves no valid resource when creation
687
+ // fails unless the owner retries or recreates the old configuration.