better-effect 0.10.0 → 0.11.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 (48) hide show
  1. package/README.md +244 -5
  2. package/dist/adapters/iti.d.mts +3 -4
  3. package/dist/adapters/iti.d.mts.map +1 -1
  4. package/dist/adapters/iti.mjs +9 -4
  5. package/dist/adapters/iti.mjs.map +1 -1
  6. package/dist/effect-2ZcGZI8A.mjs +513 -0
  7. package/dist/effect-2ZcGZI8A.mjs.map +1 -0
  8. package/dist/hono.d.mts +18 -13
  9. package/dist/hono.d.mts.map +1 -1
  10. package/dist/hono.mjs +9 -5
  11. package/dist/hono.mjs.map +1 -1
  12. package/dist/{index-BafDna0B.d.mts → index-C7Qild0_.d.mts} +158 -28
  13. package/dist/index-C7Qild0_.d.mts.map +1 -0
  14. package/dist/{index-DklNCz7w.d.mts → index-DStz0JMN.d.mts} +4 -4
  15. package/dist/{index-DklNCz7w.d.mts.map → index-DStz0JMN.d.mts.map} +1 -1
  16. package/dist/{index-CULgSzUw.d.mts → index-heuaRmXR.d.mts} +5 -5
  17. package/dist/{index-CULgSzUw.d.mts.map → index-heuaRmXR.d.mts.map} +1 -1
  18. package/dist/{index-C7KX5rAP.d.mts → index-rQhZk3Nt.d.mts} +127 -24
  19. package/dist/index-rQhZk3Nt.d.mts.map +1 -0
  20. package/dist/index.d.mts +4 -5
  21. package/dist/index.mjs +7 -538
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/runtime/explicit.d.mts +1 -1
  24. package/dist/runtime/node.d.mts +1 -1
  25. package/dist/runtime-DnMn0X0X.mjs +613 -0
  26. package/dist/runtime-DnMn0X0X.mjs.map +1 -0
  27. package/dist/scope-GGnmTQck.mjs +245 -0
  28. package/dist/scope-GGnmTQck.mjs.map +1 -0
  29. package/dist/{signal-C1bagvrO.mjs → signal-B97cs85Z.mjs} +81 -2
  30. package/dist/signal-B97cs85Z.mjs.map +1 -0
  31. package/dist/{standard-services-DW-i4UuA.mjs → standard-services-BFBq-4lo.mjs} +2 -2
  32. package/dist/{standard-services-DW-i4UuA.mjs.map → standard-services-BFBq-4lo.mjs.map} +1 -1
  33. package/dist/standard-services.d.mts +2 -2
  34. package/dist/standard-services.mjs +2 -2
  35. package/dist/testing.d.mts +198 -2
  36. package/dist/testing.d.mts.map +1 -0
  37. package/dist/testing.mjs +976 -2
  38. package/dist/testing.mjs.map +1 -0
  39. package/package.json +1 -1
  40. package/dist/effect-DAMqvegy.mjs +0 -544
  41. package/dist/effect-DAMqvegy.mjs.map +0 -1
  42. package/dist/index-BafDna0B.d.mts.map +0 -1
  43. package/dist/index-C7KX5rAP.d.mts.map +0 -1
  44. package/dist/map-layer-backend-gal-mcRv.mjs +0 -53
  45. package/dist/map-layer-backend-gal-mcRv.mjs.map +0 -1
  46. package/dist/map-layer-backend-rNwfH0Bz.d.mts +0 -42
  47. package/dist/map-layer-backend-rNwfH0Bz.d.mts.map +0 -1
  48. package/dist/signal-C1bagvrO.mjs.map +0 -1
package/README.md CHANGED
@@ -99,6 +99,40 @@ const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
99
99
  const runtime = await Runtime.make(AppLive)
100
100
  ```
101
101
 
102
+ Use `Layer.empty` when a composition root intentionally has no providers. It is
103
+ stable and has the exact `Layer<never, never>` type:
104
+
105
+ ```ts
106
+ const EmptyLive = Layer.empty
107
+ const runtime = await Runtime.make(EmptyLive)
108
+ await runtime.run(() => 'no Services required')
109
+ ```
110
+
111
+ For a port/adapter boundary, `Layer.alias` exposes one compatible implementation
112
+ under another Service token without constructing, cloning, or proxying it:
113
+
114
+ ```ts
115
+ class SqlUserRepository extends Service<SqlUserRepository>()('SqlUserRepository') {
116
+ findById(id: string): string {
117
+ return `sql:${id}`
118
+ }
119
+ }
120
+
121
+ class UserRepository extends Service<UserRepository>()('UserRepository') {
122
+ declare findById: SqlUserRepository['findById']
123
+ }
124
+
125
+ const UserRepositoryPort = Layer.alias({
126
+ from: SqlUserRepository,
127
+ to: UserRepository
128
+ })
129
+ const ApplicationLive = Layer.merge(Layer.empty, Layer.make(SqlUserRepository), UserRepositoryPort)
130
+ ```
131
+
132
+ The alias lazily resolves `from`, returns the same object under `to`, and checks
133
+ that the source satisfies the target's `Service.Contract`. Its source remains an
134
+ external Layer requirement until the source provider is composed.
135
+
102
136
  And the contract does not disappear after startup.
103
137
 
104
138
  A Runtime also knows which Services exist in its environment:
@@ -128,11 +162,102 @@ input order. If a Program returns an error or throws, scheduling stops, already-
128
162
  started Programs are allowed to settle, and the deterministic primary failure remains selected;
129
163
  there is no cancellation or Fiber scheduler.
130
164
 
165
+ Use `Program.forEach` when each item needs a lazy Program factory. The callback
166
+ receives the item and its input index, and the returned Program produces a
167
+ readonly collection in input order:
168
+
169
+ ```ts
170
+ const synchronized = Program.forEach(userIds, (userId, index) => synchronizeUser(userId, index), {
171
+ concurrency: 8
172
+ })
173
+
174
+ const result = await runtime.run(synchronized)
175
+ ```
176
+
177
+ Use `Program.allResults` when typed validation errors should be retained rather
178
+ than short-circuiting. It returns every exact child `Result` as a successful
179
+ collection element; defects still stop new work and reject the outer Program:
180
+
181
+ ```ts
182
+ const validations = Program.allResults(
183
+ [validateIdentity, validateAddress, validateDocuments] as const,
184
+ { concurrency: 3 }
185
+ )
186
+
187
+ const results = await runtime.run(validations)
188
+ ```
189
+
190
+ Both helpers use the same lazy bounded scheduler as `Program.all`: indexes are
191
+ claimed in order, output order is stable, and already-started work is always
192
+ allowed to settle.
193
+
194
+ Use `Effect.*` to transform an already-created Result, and `Program.*` to compose
195
+ an `Effect.fn` Program without starting it. `Program.map`, `mapError`, `tap`, and
196
+ `tapError` preserve that laziness; `andThen` and `recover` accept an Effect, a
197
+ Promise of an Effect, or another Program only after their matching Result branch
198
+ is selected. `Program.andThen` unions its source and continuation error and
199
+ Service requirement channels. `Program.recover` handles and removes the source
200
+ `Err` channel, exposes the recovery error channel, and unions the source and
201
+ recovery Service requirements. Taps preserve the original Result object on
202
+ success.
203
+
131
204
  `Runtime.make(AppLive)` and `Runtime.run(AppLive, program)` use the built-in
132
205
  `MapLayerBackend`. Pass `{ backend: new ItiLayerBackend() }` when an external
133
206
  container is needed; `MemoryLayerBackend` remains its compatibility alias from
134
207
  `better-effect/testing`.
135
208
 
209
+ ### Verify custom adapters
210
+
211
+ `better-effect/testing` provides runner-neutral conformance scenarios for
212
+ third-party `LayerBackend` and `RuntimeContextStorage` implementations. Each
213
+ scenario has a stable `name` and `run` callback, creates a fresh adapter, and
214
+ runs the optional adapter cleanup after every assertion outcome. Backends must
215
+ synchronously pass the actual readonly pending acquisition Promise collection to
216
+ `disposeAll`'s `onPendingAcquisitions` hook, await the callback, then await the
217
+ acquisitions before clearing state. Declare the backend's acquisition-failure
218
+ policy explicitly: `MapLayerBackend` retries,
219
+ while ITI keeps an asynchronous failure cached until disposal.
220
+
221
+ Register the scenarios with Bun:
222
+
223
+ ```ts
224
+ import { describe, test } from 'bun:test'
225
+ import { MapLayerBackend } from 'better-effect'
226
+ import { layerBackendContract } from 'better-effect/testing'
227
+
228
+ describe('My backend', () => {
229
+ for (const scenario of layerBackendContract({
230
+ makeBackend: () => new MapLayerBackend(),
231
+ acquisitionFailure: 'retry'
232
+ })) {
233
+ test(scenario.name, scenario.run)
234
+ }
235
+ })
236
+ ```
237
+
238
+ The same scenarios work with Vitest without adding a runner dependency to the
239
+ published entrypoint:
240
+
241
+ ```ts
242
+ import { describe, it } from 'vitest'
243
+ import { NodeRuntimeContextStorage } from 'better-effect/runtime/node'
244
+ import { runtimeContextStorageContract } from 'better-effect/testing'
245
+
246
+ describe('My context storage', () => {
247
+ for (const scenario of runtimeContextStorageContract({
248
+ makeStorage: () => new NodeRuntimeContextStorage(),
249
+ concurrency: 'concurrent'
250
+ })) {
251
+ it(scenario.name, scenario.run)
252
+ }
253
+ })
254
+ ```
255
+
256
+ Use `concurrency: 'sequential'` for a storage that rejects overlapping roots
257
+ with `RuntimeContextOverlapError`; pass `makeCompanionStorage` to also verify
258
+ that it does not leak frames into a Node or explicit storage. See the adapter
259
+ guide for the complete `LayerBackend` contract.
260
+
136
261
  Runtimes are async disposables, so request-scoped code can use:
137
262
 
138
263
  ```ts
@@ -146,6 +271,42 @@ Or let `Runtime.use` own the lifetime:
146
271
  const result = await Runtime.use(AppLive, (runtime) => runtime.run(program))
147
272
  ```
148
273
 
274
+ For isolated application tests, use the testing facade over that same Layer and
275
+ Runtime. It installs only the controlled Services you pass, records lifecycle
276
+ events, and disposes automatically:
277
+
278
+ ```ts
279
+ import { ClockTest, LoggerTest, TestRuntime } from 'better-effect/testing'
280
+
281
+ const logger = new LoggerTest()
282
+ const result = await TestRuntime.use(
283
+ AppLive,
284
+ {
285
+ overrides: [DatabaseTest],
286
+ clock: new ClockTest(Date.UTC(2026, 0, 1)),
287
+ logger
288
+ },
289
+ async (test) => {
290
+ const value = await test.run(loadDashboard)
291
+ expect(test.observer.executionEnds).toHaveLength(1)
292
+ return value
293
+ }
294
+ )
295
+
296
+ expect(logger.events).toHaveLength(1)
297
+ ```
298
+
299
+ A long-lived test boundary supports integration-style request Layers as well:
300
+
301
+ ```ts
302
+ await using test = await TestRuntime.make(AppLive, { overrides: [DatabaseTest] })
303
+ const result = await test.runWith(RequestLive, handleRequest)
304
+ ```
305
+
306
+ `TestRuntime.use` preserves program-vs-cleanup failure precedence. The default
307
+ recorder is available as `test.observer`; use `test.runtime` only when an
308
+ advanced test explicitly needs the underlying Runtime.
309
+
149
310
  Layer providers remain lazy unless startup validation is requested. Warm them
150
311
  all before accepting work with either form:
151
312
 
@@ -176,6 +337,34 @@ const runtime = await Runtime.make(AppLive, {
176
337
  })
177
338
  ```
178
339
 
340
+ For lifecycle assertions, `better-effect/testing` provides a recorder and a
341
+ best-effort composition utility:
342
+
343
+ ```ts
344
+ import { RecordedRuntimeObserver, RuntimeObserver } from 'better-effect/testing'
345
+
346
+ const recorded = RecordedRuntimeObserver.make()
347
+ const runtime = await Runtime.make(AppTest, {
348
+ observers: [
349
+ RuntimeObserver.compose(recorded, {
350
+ onExecutionEnd: ({ outcome }) => console.debug(outcome.status)
351
+ })
352
+ ]
353
+ })
354
+
355
+ await runtime.run(program)
356
+ const snapshot = recorded.snapshot()
357
+ expect(snapshot.executionEnds).toHaveLength(1)
358
+ expect(snapshot.timeline).toContain(snapshot.executionEnds[0])
359
+
360
+ await runtime.dispose()
361
+ ```
362
+
363
+ `RecordedRuntimeObserver` preserves event identity in immutable category views
364
+ and its ordered `timeline`; call `clear()` to reuse it. Composition invokes
365
+ observers in declaration order and isolates thrown or rejected observer
366
+ failures from the Runtime result.
367
+
179
368
  Cancellation is cooperative and uses `AbortSignal`; no scheduler or fibers are
180
369
  created. Pass a signal to one execution and read it from the program when an
181
370
  I/O operation supports cancellation. Runtime disposal waits for active work;
@@ -229,9 +418,9 @@ app.get(
229
418
  )
230
419
  ```
231
420
 
232
- Hono validators can precede the generator or handler callback. Their validated
233
- `c.req.valid(...)` inputs are combined and inferred without a manual `Input`
234
- helper:
421
+ One or more Hono validators can precede the generator or handler callback, in
422
+ the order they should run. Their validated `c.req.valid(...)` inputs are
423
+ combined and inferred without a manual `Input` helper:
235
424
 
236
425
  ```ts
237
426
  import { sValidator } from '@hono/standard-validator'
@@ -408,7 +597,9 @@ Some dependencies are values.
408
597
 
409
598
  Others own connections, sessions, files or other resources.
410
599
 
411
- `Layer.scoped`, `Layer.scopedGen`, `Effect.acquireRelease`, `Effect.add` and `Scope` make their lifetime explicit.
600
+ `Layer.scoped`, `Layer.scopedGen`, `Layer.scopedDisposable`, `Effect.acquireRelease`,
601
+ `Effect.acquireReleaseResult`, `Effect.acquireDisposable`, `Effect.add` and `Scope` make their
602
+ lifetime explicit.
412
603
 
413
604
  ```ts
414
605
  const DatabaseLive = Layer.scoped(
@@ -422,6 +613,35 @@ Runtime owns the application lifetime and safely releases scoped resources when
422
613
 
423
614
  Resources acquired during an individual execution belong to that execution instead.
424
615
 
616
+ When an existing API already returns a `Result`, keep its typed failure channel while
617
+ registering only successful acquisitions:
618
+
619
+ ```ts
620
+ const connection =
621
+ yield *
622
+ Effect.acquireReleaseResult(
623
+ () => pool.connect(),
624
+ (connection, outcome) => connection.close(outcome)
625
+ )
626
+ ```
627
+
628
+ An `Err` is returned unchanged and is never released. Thrown or rejected acquisition
629
+ defects use the normal `UnhandledException` channel; release failures remain Scope
630
+ cleanup failures rather than widening the acquisition error type.
631
+
632
+ For values that implement JavaScript disposal, use the disposable helpers instead of
633
+ repeating a release callback. Async disposal is preferred when both protocols exist:
634
+
635
+ ```ts
636
+ const file = yield * Effect.acquireDisposable(() => openFile(path))
637
+
638
+ const DatabaseLive = Layer.scopedDisposable(Database, () => Database.connect())
639
+ ```
640
+
641
+ `Effect.acquireDisposable` belongs to the current execution Scope. `Layer.scopedDisposable`
642
+ keeps the client alive across executions and disposes it with the Runtime root; the DI
643
+ backend never owns that release.
644
+
425
645
  ### Keep your runtime choices
426
646
 
427
647
  `better-effect` is not a replacement implementation of Effect.
@@ -572,7 +792,26 @@ still rejects it when its Layer does not provide every required Service.
572
792
 
573
793
  Observation helpers such as `Effect.tap`, `Effect.tapError`, and `Effect.tapBoth`
574
794
  run only the active branch and return the original Result, so logging or metrics
575
- do not change the pipeline's value or requirement channel.
795
+ do not change the pipeline's value or requirement channel. The async variants
796
+ `Effect.tapAsync`, `Effect.tapErrorAsync`, and `Effect.tapBothAsync` accept
797
+ `PromiseLike<void>` observers, always return a Promise, and preserve the source
798
+ requirements. They delegate branch selection and defect handling to
799
+ `better-result`; only the active observer runs, and a successful observation
800
+ returns the exact original Result. They do not create a Scope or resolve
801
+ Services inside the callback.
802
+
803
+ ```ts
804
+ const audited = pipe(
805
+ loadUser(userId),
806
+ Effect.tapAsync((user) => metrics.recordUserLoaded(user.id)),
807
+ Effect.tapErrorAsync((error) => metrics.recordUserFailure(error))
808
+ )
809
+ ```
810
+
811
+ `Effect.matchError` exhaustively maps a tagged `Err` union, while
812
+ `Effect.matchErrorPartial` maps selected tags and retains unhandled variants in
813
+ the resulting error union. Both delegate to `better-result`'s tagged-error
814
+ matchers and preserve the source success and requirement channels.
576
815
 
577
816
  Use `Effect.recover` or `Effect.recoverAsync` for an explicit fallback Result;
578
817
  the fallback is evaluated only when the input is an `Err`, and its Service
@@ -1,6 +1,5 @@
1
- import { N as AnyServiceToken } from "../index-CULgSzUw.mjs";
2
- import "../index-C7KX5rAP.mjs";
3
- import { a as LayerRegistration, n as LayerBackend } from "../map-layer-backend-rNwfH0Bz.mjs";
1
+ import { N as AnyServiceToken } from "../index-heuaRmXR.mjs";
2
+ import { v as LayerBackend, x as LayerRegistration, y as LayerBackendDisposeOptions } from "../index-rQhZk3Nt.mjs";
4
3
  //#region src/adapters/iti.d.ts
5
4
  /**
6
5
  * ITI-backed Layer backend.
@@ -24,7 +23,7 @@ declare class ItiLayerBackend implements LayerBackend {
24
23
  /** Resolve a registered Service through the ITI container. */
25
24
  resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>;
26
25
  /** Reset container-owned ITI state; Scope owns Layer provider releases. */
27
- disposeAll(): Promise<void>;
26
+ disposeAll(options?: LayerBackendDisposeOptions): Promise<void>;
28
27
  }
29
28
  //#endregion
30
29
  export { ItiLayerBackend };
@@ -1 +1 @@
1
- {"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":";;;;;;;;;;cAsBa,2BAA2B;UAC9B;mBAES;mBAEA;;;;;;mBAOA;UAET;;EAgBR,SAAS,cAAc;;EAuBvB,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;;EAiCnF,cAAc"}
1
+ {"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":";;;;;;;;;cAuBa,2BAA2B;UAC9B;mBAES;mBAEA;;;;;;mBAOA;UAET;;EAgBR,SAAS,cAAc;;EAuBvB,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;;EAiCnF,WAAW,UAAU,6BAA6B"}
@@ -18,7 +18,7 @@ var ItiLayerBackend = class {
18
18
  * ITI caches rejected acquisitions; replacing the container is the explicit
19
19
  * retry boundary for that sticky failure behavior.
20
20
  */
21
- pending = /* @__PURE__ */ new Set();
21
+ pending = /* @__PURE__ */ new Map();
22
22
  keyFor(token) {
23
23
  const tag = token.serviceTag;
24
24
  const existing = this.keys.get(tag);
@@ -50,17 +50,22 @@ var ItiLayerBackend = class {
50
50
  };
51
51
  if (isPromiseLike(resolved)) {
52
52
  const pending = Promise.resolve(resolved).then(validate);
53
- this.pending.add(pending);
53
+ this.pending.set(pending, registered);
54
54
  pending.then(() => this.pending.delete(pending), () => this.pending.delete(pending));
55
55
  return pending;
56
56
  }
57
57
  return validate(resolved);
58
58
  }
59
59
  /** Reset container-owned ITI state; Scope owns Layer provider releases. */
60
- async disposeAll() {
60
+ async disposeAll(options) {
61
61
  const container = this.container;
62
+ const acquisitions = [...this.pending.keys()];
62
63
  try {
63
- await Promise.allSettled(this.pending);
64
+ if (acquisitions.length > 0) {
65
+ const observePending = options?.onPendingAcquisitions;
66
+ if (observePending) await observePending(acquisitions);
67
+ await Promise.allSettled(acquisitions);
68
+ }
64
69
  await container.disposeAll();
65
70
  } finally {
66
71
  this.container = createContainer();
@@ -1 +1 @@
1
- {"version":3,"file":"iti.mjs","names":[],"sources":["../../src/adapters/iti.ts"],"sourcesContent":["import { createContainer } from 'iti'\n\nimport {\n DuplicateServiceError,\n ServiceTagCollisionError,\n type LayerBackend,\n type LayerRegistration\n} from '../layer'\n\nimport { ServiceNotFoundError, type AnyServiceToken } from '../service'\n\nimport { assertServiceCompatibility } from '../layer/internal-identity'\nimport { isPromiseLike } from '../utils/runtime'\n\ntype LayerAcquiredValue = Awaited<ReturnType<LayerRegistration['acquire']>>\n\n/**\n * ITI-backed Layer backend.\n *\n * Install `iti` as the optional peer dependency and pass an instance to\n * `Runtime.make` when using ITI's container implementation.\n */\nexport class ItiLayerBackend implements LayerBackend {\n private container: any = createContainer()\n\n private readonly keys = new Map<string, string>()\n\n private readonly registered = new Map<string, AnyServiceToken>()\n\n /**\n * Track async gets so disposal cannot reset ITI while a provider is acquiring.\n * ITI caches rejected acquisitions; replacing the container is the explicit\n * retry boundary for that sticky failure behavior.\n */\n private readonly pending = new Set<Promise<unknown>>()\n\n private keyFor(token: AnyServiceToken): string {\n const tag = token.serviceTag\n const existing = this.keys.get(tag)\n\n if (existing) {\n return existing\n }\n\n const key = `better-effect:${tag}`\n\n this.keys.set(tag, key)\n\n return key\n }\n\n /** Register a Layer provider under its deterministic Service-tag key. */\n register(registration: LayerRegistration): void {\n const token = registration.service\n const tag = token.serviceTag\n const existing = this.registered.get(tag)\n\n if (existing === token) {\n throw new DuplicateServiceError(token)\n }\n\n if (existing) {\n throw new ServiceTagCollisionError(existing, token)\n }\n\n const key = this.keyFor(token)\n\n this.container = this.container.add({\n [key]: registration.acquire\n })\n\n this.registered.set(tag, token)\n }\n\n /** Resolve a registered Service through the ITI container. */\n resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>> {\n const registered = this.registered.get(token.serviceTag)\n\n if (registered === undefined) {\n throw new ServiceNotFoundError(token)\n }\n\n const key = this.keyFor(token)\n const resolved = this.container.get(key)\n\n const validate = (instance: LayerAcquiredValue): InstanceType<T> => {\n assertServiceCompatibility(token, registered, instance)\n\n // SAFETY: The registered tag and compatibility check establish the constructor-to-instance relationship after ITI erases it.\n return instance as InstanceType<T>\n }\n\n if (isPromiseLike(resolved)) {\n const pending = Promise.resolve(resolved).then(validate)\n\n this.pending.add(pending)\n void pending.then(\n () => this.pending.delete(pending),\n () => this.pending.delete(pending)\n )\n\n return pending\n }\n\n return validate(resolved)\n }\n\n /** Reset container-owned ITI state; Scope owns Layer provider releases. */\n async disposeAll(): Promise<void> {\n const container = this.container\n\n try {\n await Promise.allSettled(this.pending)\n await container.disposeAll()\n } finally {\n this.container = createContainer()\n this.registered.clear()\n this.keys.clear()\n this.pending.clear()\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAsBA,IAAa,kBAAb,MAAqD;CACnD,YAAyB,gBAAgB;CAEzC,uBAAwB,IAAI,IAAoB;CAEhD,6BAA8B,IAAI,IAA6B;;;;;;CAO/D,0BAA2B,IAAI,IAAsB;CAErD,OAAe,OAAgC;EAC7C,MAAM,MAAM,MAAM;EAClB,MAAM,WAAW,KAAK,KAAK,IAAI,GAAG;EAElC,IAAI,UACF,OAAO;EAGT,MAAM,MAAM,iBAAiB;EAE7B,KAAK,KAAK,IAAI,KAAK,GAAG;EAEtB,OAAO;CACT;;CAGA,SAAS,cAAuC;EAC9C,MAAM,QAAQ,aAAa;EAC3B,MAAM,MAAM,MAAM;EAClB,MAAM,WAAW,KAAK,WAAW,IAAI,GAAG;EAExC,IAAI,aAAa,OACf,MAAM,IAAI,sBAAsB,KAAK;EAGvC,IAAI,UACF,MAAM,IAAI,yBAAyB,UAAU,KAAK;EAGpD,MAAM,MAAM,KAAK,OAAO,KAAK;EAE7B,KAAK,YAAY,KAAK,UAAU,IAAI,GACjC,MAAM,aAAa,QACtB,CAAC;EAED,KAAK,WAAW,IAAI,KAAK,KAAK;CAChC;;CAGA,QAAmC,OAA0D;EAC3F,MAAM,aAAa,KAAK,WAAW,IAAI,MAAM,UAAU;EAEvD,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,qBAAqB,KAAK;EAGtC,MAAM,MAAM,KAAK,OAAO,KAAK;EAC7B,MAAM,WAAW,KAAK,UAAU,IAAI,GAAG;EAEvC,MAAM,YAAY,aAAkD;GAClE,2BAA2B,OAAO,YAAY,QAAQ;GAGtD,OAAO;EACT;EAEA,IAAI,cAAc,QAAQ,GAAG;GAC3B,MAAM,UAAU,QAAQ,QAAQ,QAAQ,CAAC,CAAC,KAAK,QAAQ;GAEvD,KAAK,QAAQ,IAAI,OAAO;GACxB,QAAa,WACL,KAAK,QAAQ,OAAO,OAAO,SAC3B,KAAK,QAAQ,OAAO,OAAO,CACnC;GAEA,OAAO;EACT;EAEA,OAAO,SAAS,QAAQ;CAC1B;;CAGA,MAAM,aAA4B;EAChC,MAAM,YAAY,KAAK;EAEvB,IAAI;GACF,MAAM,QAAQ,WAAW,KAAK,OAAO;GACrC,MAAM,UAAU,WAAW;EAC7B,UAAU;GACR,KAAK,YAAY,gBAAgB;GACjC,KAAK,WAAW,MAAM;GACtB,KAAK,KAAK,MAAM;GAChB,KAAK,QAAQ,MAAM;EACrB;CACF;AACF"}
1
+ {"version":3,"file":"iti.mjs","names":[],"sources":["../../src/adapters/iti.ts"],"sourcesContent":["import { createContainer } from 'iti'\n\nimport {\n DuplicateServiceError,\n ServiceTagCollisionError,\n type LayerBackend,\n type LayerBackendDisposeOptions,\n type LayerRegistration\n} from '../layer'\n\nimport { ServiceNotFoundError, type AnyServiceToken } from '../service'\n\nimport { assertServiceCompatibility } from '../layer/internal-identity'\nimport { isPromiseLike } from '../utils/runtime'\n\ntype LayerAcquiredValue = Awaited<ReturnType<LayerRegistration['acquire']>>\n\n/**\n * ITI-backed Layer backend.\n *\n * Install `iti` as the optional peer dependency and pass an instance to\n * `Runtime.make` when using ITI's container implementation.\n */\nexport class ItiLayerBackend implements LayerBackend {\n private container: any = createContainer()\n\n private readonly keys = new Map<string, string>()\n\n private readonly registered = new Map<string, AnyServiceToken>()\n\n /**\n * Track async gets so disposal cannot reset ITI while a provider is acquiring.\n * ITI caches rejected acquisitions; replacing the container is the explicit\n * retry boundary for that sticky failure behavior.\n */\n private readonly pending = new Map<Promise<unknown>, AnyServiceToken>()\n\n private keyFor(token: AnyServiceToken): string {\n const tag = token.serviceTag\n const existing = this.keys.get(tag)\n\n if (existing) {\n return existing\n }\n\n const key = `better-effect:${tag}`\n\n this.keys.set(tag, key)\n\n return key\n }\n\n /** Register a Layer provider under its deterministic Service-tag key. */\n register(registration: LayerRegistration): void {\n const token = registration.service\n const tag = token.serviceTag\n const existing = this.registered.get(tag)\n\n if (existing === token) {\n throw new DuplicateServiceError(token)\n }\n\n if (existing) {\n throw new ServiceTagCollisionError(existing, token)\n }\n\n const key = this.keyFor(token)\n\n this.container = this.container.add({\n [key]: registration.acquire\n })\n\n this.registered.set(tag, token)\n }\n\n /** Resolve a registered Service through the ITI container. */\n resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>> {\n const registered = this.registered.get(token.serviceTag)\n\n if (registered === undefined) {\n throw new ServiceNotFoundError(token)\n }\n\n const key = this.keyFor(token)\n const resolved = this.container.get(key)\n\n const validate = (instance: LayerAcquiredValue): InstanceType<T> => {\n assertServiceCompatibility(token, registered, instance)\n\n // SAFETY: The registered tag and compatibility check establish the constructor-to-instance relationship after ITI erases it.\n return instance as InstanceType<T>\n }\n\n if (isPromiseLike(resolved)) {\n const pending = Promise.resolve(resolved).then(validate)\n\n this.pending.set(pending, registered)\n void pending.then(\n () => this.pending.delete(pending),\n () => this.pending.delete(pending)\n )\n\n return pending\n }\n\n return validate(resolved)\n }\n\n /** Reset container-owned ITI state; Scope owns Layer provider releases. */\n async disposeAll(options?: LayerBackendDisposeOptions): Promise<void> {\n const container = this.container\n const acquisitions = [...this.pending.keys()]\n\n try {\n if (acquisitions.length > 0) {\n const observePending = options?.onPendingAcquisitions\n\n if (observePending) {\n await observePending(acquisitions)\n }\n\n await Promise.allSettled(acquisitions)\n }\n await container.disposeAll()\n } finally {\n this.container = createContainer()\n this.registered.clear()\n this.keys.clear()\n this.pending.clear()\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAuBA,IAAa,kBAAb,MAAqD;CACnD,YAAyB,gBAAgB;CAEzC,uBAAwB,IAAI,IAAoB;CAEhD,6BAA8B,IAAI,IAA6B;;;;;;CAO/D,0BAA2B,IAAI,IAAuC;CAEtE,OAAe,OAAgC;EAC7C,MAAM,MAAM,MAAM;EAClB,MAAM,WAAW,KAAK,KAAK,IAAI,GAAG;EAElC,IAAI,UACF,OAAO;EAGT,MAAM,MAAM,iBAAiB;EAE7B,KAAK,KAAK,IAAI,KAAK,GAAG;EAEtB,OAAO;CACT;;CAGA,SAAS,cAAuC;EAC9C,MAAM,QAAQ,aAAa;EAC3B,MAAM,MAAM,MAAM;EAClB,MAAM,WAAW,KAAK,WAAW,IAAI,GAAG;EAExC,IAAI,aAAa,OACf,MAAM,IAAI,sBAAsB,KAAK;EAGvC,IAAI,UACF,MAAM,IAAI,yBAAyB,UAAU,KAAK;EAGpD,MAAM,MAAM,KAAK,OAAO,KAAK;EAE7B,KAAK,YAAY,KAAK,UAAU,IAAI,GACjC,MAAM,aAAa,QACtB,CAAC;EAED,KAAK,WAAW,IAAI,KAAK,KAAK;CAChC;;CAGA,QAAmC,OAA0D;EAC3F,MAAM,aAAa,KAAK,WAAW,IAAI,MAAM,UAAU;EAEvD,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,qBAAqB,KAAK;EAGtC,MAAM,MAAM,KAAK,OAAO,KAAK;EAC7B,MAAM,WAAW,KAAK,UAAU,IAAI,GAAG;EAEvC,MAAM,YAAY,aAAkD;GAClE,2BAA2B,OAAO,YAAY,QAAQ;GAGtD,OAAO;EACT;EAEA,IAAI,cAAc,QAAQ,GAAG;GAC3B,MAAM,UAAU,QAAQ,QAAQ,QAAQ,CAAC,CAAC,KAAK,QAAQ;GAEvD,KAAK,QAAQ,IAAI,SAAS,UAAU;GACpC,QAAa,WACL,KAAK,QAAQ,OAAO,OAAO,SAC3B,KAAK,QAAQ,OAAO,OAAO,CACnC;GAEA,OAAO;EACT;EAEA,OAAO,SAAS,QAAQ;CAC1B;;CAGA,MAAM,WAAW,SAAqD;EACpE,MAAM,YAAY,KAAK;EACvB,MAAM,eAAe,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;EAE5C,IAAI;GACF,IAAI,aAAa,SAAS,GAAG;IAC3B,MAAM,iBAAiB,SAAS;IAEhC,IAAI,gBACF,MAAM,eAAe,YAAY;IAGnC,MAAM,QAAQ,WAAW,YAAY;GACvC;GACA,MAAM,UAAU,WAAW;EAC7B,UAAU;GACR,KAAK,YAAY,gBAAgB;GACjC,KAAK,WAAW,MAAM;GACtB,KAAK,KAAK,MAAM;GAChB,KAAK,QAAQ,MAAM;EACrB;CACF;AACF"}