better-effect 0.11.0 → 0.12.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 (67) hide show
  1. package/README.md +220 -6
  2. package/dist/adapters/iti.d.mts +2 -1
  3. package/dist/adapters/iti.d.mts.map +1 -1
  4. package/dist/adapters/iti.mjs +11 -10
  5. package/dist/adapters/iti.mjs.map +1 -1
  6. package/dist/{effect-2ZcGZI8A.mjs → effect-CK4YZaX0.mjs} +56 -10
  7. package/dist/effect-CK4YZaX0.mjs.map +1 -0
  8. package/dist/hono.d.mts +3 -3
  9. package/dist/hono.mjs +3 -3
  10. package/dist/{index-DStz0JMN.d.mts → index-B1nH4luq.d.mts} +73 -6
  11. package/dist/index-B1nH4luq.d.mts.map +1 -0
  12. package/dist/index-BMdU_Z72.d.mts +18 -0
  13. package/dist/index-BMdU_Z72.d.mts.map +1 -0
  14. package/dist/{index-C7Qild0_.d.mts → index-CX6iAoCB.d.mts} +15 -151
  15. package/dist/index-CX6iAoCB.d.mts.map +1 -0
  16. package/dist/index-GYJ4qm9K.d.mts +179 -0
  17. package/dist/index-GYJ4qm9K.d.mts.map +1 -0
  18. package/dist/index-heuaRmXR.d.mts.map +1 -1
  19. package/dist/index.d.mts +5 -3
  20. package/dist/index.mjs +6 -5
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{internal-identity-DmUpBeeL.mjs → internal-identity-CbXoZIfw.mjs} +3 -3
  23. package/dist/internal-identity-CbXoZIfw.mjs.map +1 -0
  24. package/dist/{signal-B97cs85Z.mjs → layer-D56fRdLt.mjs} +30 -111
  25. package/dist/layer-D56fRdLt.mjs.map +1 -0
  26. package/dist/node-context-DWsCUBRa.d.mts +12 -0
  27. package/dist/node-context-DWsCUBRa.d.mts.map +1 -0
  28. package/dist/node-context-lggRF4bR.mjs +25 -0
  29. package/dist/node-context-lggRF4bR.mjs.map +1 -0
  30. package/dist/node.d.mts +51 -0
  31. package/dist/node.d.mts.map +1 -0
  32. package/dist/node.mjs +233 -0
  33. package/dist/node.mjs.map +1 -0
  34. package/dist/{index-rQhZk3Nt.d.mts → outcome-CB43bnFW.d.mts} +29 -19
  35. package/dist/outcome-CB43bnFW.d.mts.map +1 -0
  36. package/dist/{scope-GGnmTQck.mjs → program-metadata-DhQCCirO.mjs} +18 -3
  37. package/dist/program-metadata-DhQCCirO.mjs.map +1 -0
  38. package/dist/registration-drnZk0SY.mjs +182 -0
  39. package/dist/registration-drnZk0SY.mjs.map +1 -0
  40. package/dist/runtime/node.d.mts +2 -11
  41. package/dist/runtime/node.mjs +2 -24
  42. package/dist/{runtime-DnMn0X0X.mjs → runtime-CBMrX8Wl.mjs} +210 -47
  43. package/dist/runtime-CBMrX8Wl.mjs.map +1 -0
  44. package/dist/signal-D2UTcaaG.mjs +105 -0
  45. package/dist/signal-D2UTcaaG.mjs.map +1 -0
  46. package/dist/{standard-services-BFBq-4lo.mjs → standard-services-GBZ32tll.mjs} +200 -16
  47. package/dist/standard-services-GBZ32tll.mjs.map +1 -0
  48. package/dist/standard-services.d.mts +3 -3
  49. package/dist/standard-services.mjs +3 -3
  50. package/dist/testing.d.mts +65 -6
  51. package/dist/testing.d.mts.map +1 -1
  52. package/dist/testing.mjs +191 -8
  53. package/dist/testing.mjs.map +1 -1
  54. package/package.json +8 -3
  55. package/dist/effect-2ZcGZI8A.mjs.map +0 -1
  56. package/dist/errors-Dnjhzbt0.mjs +0 -99
  57. package/dist/errors-Dnjhzbt0.mjs.map +0 -1
  58. package/dist/index-C7Qild0_.d.mts.map +0 -1
  59. package/dist/index-DStz0JMN.d.mts.map +0 -1
  60. package/dist/index-rQhZk3Nt.d.mts.map +0 -1
  61. package/dist/internal-identity-DmUpBeeL.mjs.map +0 -1
  62. package/dist/runtime/node.d.mts.map +0 -1
  63. package/dist/runtime/node.mjs.map +0 -1
  64. package/dist/runtime-DnMn0X0X.mjs.map +0 -1
  65. package/dist/scope-GGnmTQck.mjs.map +0 -1
  66. package/dist/signal-B97cs85Z.mjs.map +0 -1
  67. package/dist/standard-services-BFBq-4lo.mjs.map +0 -1
package/README.md CHANGED
@@ -12,7 +12,9 @@ bun add better-effect better-result
12
12
 
13
13
  The published Runtime entrypoint is officially supported on Node.js and Bun.
14
14
  The tested runtime matrix is Node.js 24 and Bun 1.3.14, and the default runtime
15
- context uses Node/Bun async context propagation.
15
+ context uses Node/Bun async context propagation. `bun run check` also deletes
16
+ and rebuilds `dist`, packs the result into a temporary consumer, and runs the
17
+ full Node/Bun `NodeRuntime` child-process suite.
16
18
 
17
19
  ## TypeScript knows what your application needs
18
20
 
@@ -156,6 +158,28 @@ Scope.
156
158
  Scope. `Effect.fn` captures the generator as a lazy `Program` for Runtime
157
159
  boundaries; the callback form remains supported for compatibility.
158
160
 
161
+ Name a Program without executing it. The name is private diagnostic metadata,
162
+ so the value remains an ordinary callable function and can be used with `pipe`:
163
+
164
+ ```ts
165
+ const loadUser = pipe(
166
+ Effect.fn(async function* () {
167
+ return Result.ok(await userRepository.load(userId))
168
+ }),
169
+ Program.named('user.load')
170
+ )
171
+
172
+ const result = await runtime.run(loadUser, {
173
+ attributes: { userId, requestId }
174
+ })
175
+ ```
176
+
177
+ `Program.map`, `mapError`, `andThen`, `tap`, `tapError`, and `recover` preserve
178
+ their source name. A later `Program.named` call overrides it. Collection
179
+ Programs do not concatenate child names; give `Program.all`, `Program.forEach`,
180
+ or `Program.allResults` an optional `{ name }` when the collection itself needs
181
+ a diagnostic name.
182
+
159
183
  `Program.all` keeps a collection lazy until the returned Program is run. Pass
160
184
  `{ concurrency: n }` for a positive bounded FIFO worker pool; values retain
161
185
  input order. If a Program returns an error or throws, scheduling stops, already-
@@ -320,7 +344,56 @@ await runtime.warmup()
320
344
  Warmup failures include the Service and resolution path, release resources
321
345
  already acquired, dispose the backend and reject the Runtime. Optional
322
346
  observers expose Service resolution/acquisition, execution and Layer release
323
- events without coupling the core to an observability SDK:
347
+ events without coupling the core to an observability SDK. Execution events
348
+ carry one matching `executionId`, the optional Program name, copied readonly
349
+ attributes, and a monotonic `durationMs` measured through execution cleanup:
350
+
351
+ ```ts
352
+ const runtime = await Runtime.make(AppLive, {
353
+ observers: [
354
+ {
355
+ onExecutionStart: ({ executionId, name, attributes }) =>
356
+ console.debug('program.start', { executionId, name, attributes }),
357
+ onExecutionEnd: ({ executionId, outcome, durationMs }) =>
358
+ metrics.observe('program.duration_ms', durationMs, {
359
+ executionId,
360
+ outcome: outcome.status
361
+ })
362
+ }
363
+ ]
364
+ })
365
+ ```
366
+
367
+ The Runtime does not serialize or inspect attribute values. Attributes are
368
+ shallow-copied and exposed as a readonly event view; do not attach secrets,
369
+ large objects, or mutable application state. Keep sensitive values out of
370
+ observer logs and metrics labels.
371
+
372
+ For coarse, synchronous diagnostics, use `runtime.inspect()`:
373
+
374
+ ```ts
375
+ const inspection = runtime.inspect()
376
+ // {
377
+ // state: 'active',
378
+ // warmup: 'idle',
379
+ // activeExecutions: 0,
380
+ // executions: [],
381
+ // services: ['Database', 'UserRepository'],
382
+ // shutdownSignalAborted: false
383
+ // }
384
+ ```
385
+
386
+ The returned snapshot and its arrays are detached and immutable. It contains
387
+ only public Service tags and execution IDs, names and start timestamps; it never
388
+ resolves Services, warms the Runtime, creates Scopes, invokes observers or
389
+ exposes providers, instances, signals, attributes or backend state. Execution
390
+ entries remain present until their execution Scope cleanup settles. Warmup is
391
+ reported as `idle`, `running`, `completed` or `failed`, and `state` reports
392
+ `active`, `disposing` or `disposed`.
393
+
394
+ `inspect()` is diagnostic information, not a lock, synchronization primitive or
395
+ readiness guarantee. It cannot cancel or force shutdown of any execution; use
396
+ `dispose()` and cooperative `AbortSignal` handling for lifecycle coordination.
324
397
 
325
398
  Missing, circular, and provider-construction failures use the logical Service
326
399
  tags in `ServiceNotFoundError`, `CircularDependencyError`, and
@@ -365,6 +438,29 @@ and its ordered `timeline`; call `clear()` to reuse it. Composition invokes
365
438
  observers in declaration order and isolates thrown or rejected observer
366
439
  failures from the Runtime result.
367
440
 
441
+ For a startup view of the graph actually observed by a Runtime, compose the
442
+ small graph observer and warm the Layer before accepting work:
443
+
444
+ ```ts
445
+ import { Runtime } from 'better-effect'
446
+ import { RuntimeGraphObserver } from 'better-effect/testing'
447
+
448
+ const graph = RuntimeGraphObserver.make({ rootLabel: 'Runtime' })
449
+ const runtime = await Runtime.make(AppLive, {
450
+ warmup: true,
451
+ observers: [graph]
452
+ })
453
+
454
+ console.log(graph.toJSON())
455
+ console.log(graph.toMermaid())
456
+ await runtime.dispose()
457
+ ```
458
+
459
+ `RuntimeGraphObserver` uses only public resolution and acquisition events. Its
460
+ snapshot is sorted, detached and immutable; it records Service tags and counts,
461
+ not instances, scopes, causes or execution attributes. Providers that are never
462
+ resolved remain absent, and `clear()` starts a new diagnostic session.
463
+
368
464
  Cancellation is cooperative and uses `AbortSignal`; no scheduler or fibers are
369
465
  created. Pass a signal to one execution and read it from the program when an
370
466
  I/O operation supports cancellation. Runtime disposal waits for active work;
@@ -379,6 +475,38 @@ const cancellableProgram = Effect.fn(async function* () {
379
475
  })
380
476
  ```
381
477
 
478
+ For a Node.js or Bun CLI, use the host-specific `better-effect/node` entrypoint.
479
+ `NodeRuntime.runMain` validates its signal and callback options before installing
480
+ `SIGINT`/`SIGTERM` listeners, links the first signal to `CurrentAbortSignal`,
481
+ and disposes the Runtime exactly once:
482
+
483
+ ```ts
484
+ import { NodeRuntime } from 'better-effect/node'
485
+
486
+ const main = Effect.fn(async function* () {
487
+ const signal = yield* CurrentAbortSignal
488
+ return Result.ok(await runCommand({ signal }))
489
+ })
490
+
491
+ await NodeRuntime.runMain(AppLive, main, {
492
+ onFailure: (error) => {
493
+ console.error(error)
494
+ return 1
495
+ },
496
+ onSuccess: () => 0
497
+ })
498
+ ```
499
+
500
+ `Result.err` uses `onFailure` (or exit code `1` by default), while thrown defects
501
+ remain rejected and may be reported with `onDefect`. Cleanup-only failures use
502
+ `onCleanupFailure`, remain observable, and still set a non-zero
503
+ `process.exitCode` after successful work. The first `SIGINT` or `SIGTERM`
504
+ immediately aborts `CurrentAbortSignal`; Runtime disposal then waits
505
+ cooperatively for the main execution. Listeners are removed in `finally`,
506
+ repeated signals are ignored, and the helper never calls `process.exit()`.
507
+ The Node boundary intentionally does not expose a second grace-period policy;
508
+ use `Runtime.dispose` directly when a managed Runtime needs one.
509
+
382
510
  For request-local context or overrides, add a Layer only to that execution:
383
511
 
384
512
  ```ts
@@ -671,10 +799,96 @@ const now = await runtime.run(async () => ServiceRuntime.resolve(Clock))
671
799
  await runtime.dispose()
672
800
  ```
673
801
 
674
- The entrypoint also provides `Random`/`RandomSeeded`, `Logger`/`LoggerTest`,
675
- `Config`, `CurrentRequest`, and the compatible `CurrentAbortSignal` bridge. None
676
- is installed implicitly; compose a normal Layer or use the provided test
677
- helpers.
802
+ `IdGenerator` uses the host's cryptographic `crypto.randomUUID()` and is also
803
+ opt-in. Use `IdGeneratorTest` to make entity creation deterministic without
804
+ introducing a domain-specific ID type:
805
+
806
+ ```ts
807
+ import { Result } from 'better-result'
808
+ import { Effect, Layer, Service } from 'better-effect'
809
+ import { IdGenerator } from 'better-effect/standard-services'
810
+ import { IdGeneratorTest, TestRuntime } from 'better-effect/testing'
811
+
812
+ class User extends Service<User>()('User') {
813
+ constructor(readonly id: string) {
814
+ super()
815
+ }
816
+ }
817
+
818
+ const createUser = Effect.fn(async function* () {
819
+ const ids = yield* IdGenerator
820
+ return Result.ok(new User(ids.next()))
821
+ })
822
+
823
+ const result = await TestRuntime.use(
824
+ Layer.merge(),
825
+ { idGenerator: new IdGeneratorTest(['user-1']) },
826
+ (test) => test.run(createUser)
827
+ )
828
+ // Result.ok(User { id: 'user-1' })
829
+ ```
830
+
831
+ `IdGeneratorTest.from((index) => ...)` provides an unbounded deterministic
832
+ sequence; its first factory index is zero and increases monotonically. The
833
+ entrypoint also provides `Random`/`RandomSeeded`, `Logger`/`LoggerTest`, `Config`,
834
+ `CurrentRequest`, and the compatible `CurrentAbortSignal` bridge. None is
835
+ installed implicitly; compose a normal Layer or use the provided test helpers.
836
+
837
+ `Clock.sleep` keeps the original `clock.sleep(milliseconds)` form and accepts
838
+ an optional `AbortSignal`. Invalid delays still throw synchronously. Aborted
839
+ sleeps clear their timer and listener; a supplied `signal.reason` is rejected
840
+ unchanged, otherwise the rejection is an `AbortError`-named `DOMException`.
841
+
842
+ Use the same signal in polling, retry delays and expiration checks:
843
+
844
+ ```ts
845
+ const poll = async (clock: Clock, signal: AbortSignal) => {
846
+ while (true) {
847
+ const status = await readStatus()
848
+ if (status.ready) return status
849
+ await clock.sleep(1_000, { signal })
850
+ }
851
+ }
852
+
853
+ const retry = async <A>(operation: () => Promise<A>, clock: Clock, signal: AbortSignal) => {
854
+ for (let attempt = 0; ; attempt += 1) {
855
+ try {
856
+ return await operation()
857
+ } catch (error) {
858
+ if (attempt === 2) throw error
859
+ await clock.sleep(100 * 2 ** attempt, { signal })
860
+ }
861
+ }
862
+ }
863
+
864
+ const waitUntilExpired = async (clock: Clock, expiresAt: number, signal: AbortSignal) => {
865
+ while (clock.now().getTime() < expiresAt) {
866
+ await clock.sleep(Math.min(expiresAt - clock.now().getTime(), 1_000), { signal })
867
+ }
868
+ }
869
+ ```
870
+
871
+ `ClockTest` orders sleeps by absolute deadline and FIFO for equal deadlines.
872
+ `pendingSleeps` is a readonly count. `advanceToNext()` returns `false` when
873
+ there is no pending sleep; `runAll({ maxSteps })` advances repeatedly and
874
+ awaits one microtask checkpoint between deadlines so resumed code can schedule
875
+ its next wait:
876
+
877
+ ```ts
878
+ const clock = new ClockTest(0)
879
+ const task = (async () => {
880
+ await clock.sleep(100)
881
+ await clock.sleep(50)
882
+ })()
883
+
884
+ await clock.runAll({ maxSteps: 10 })
885
+ await task
886
+ // clock.now().getTime() === 150
887
+ ```
888
+
889
+ `setTime` may move backward. Pending sleeps keep their absolute deadlines and
890
+ resolve only when reached. `ClockTest` does not virtualize `Date`, global
891
+ `setTimeout` or the JavaScript microtask queue.
678
892
 
679
893
  For typed environment configuration, bind a Standard Schema directly to a
680
894
  reusable descriptor:
@@ -1,5 +1,6 @@
1
1
  import { N as AnyServiceToken } from "../index-heuaRmXR.mjs";
2
- import { v as LayerBackend, x as LayerRegistration, y as LayerBackendDisposeOptions } from "../index-rQhZk3Nt.mjs";
2
+ import { S as LayerRegistration, b as LayerBackendDisposeOptions, y as LayerBackend } from "../outcome-CB43bnFW.mjs";
3
+ import "../index-BMdU_Z72.mjs";
3
4
  //#region src/adapters/iti.d.ts
4
5
  /**
5
6
  * ITI-backed Layer backend.
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":";;;;;;;;;;cAyBa,2BAA2B;UAC9B;mBAES;mBAEA;;;;;;mBAOA;UAET;;EAeR,SAAS,cAAc;;EAwBvB,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;;EAkCnF,WAAW,UAAU,6BAA6B"}
@@ -1,6 +1,6 @@
1
- import { a as ServiceTagCollisionError, c as ServiceNotFoundError, t as DuplicateServiceError } from "../errors-Dnjhzbt0.mjs";
1
+ import { f as captureServiceTag, n as normalizeLayerRegistration, r as DuplicateServiceError, s as ServiceTagCollisionError, t as captureLayerRegistrationTag, u as ServiceNotFoundError } from "../registration-drnZk0SY.mjs";
2
2
  import { t as isPromiseLike } from "../runtime-CDcCF5cb.mjs";
3
- import { t as assertServiceCompatibility } from "../internal-identity-DmUpBeeL.mjs";
3
+ import { t as assertServiceCompatibility } from "../internal-identity-CbXoZIfw.mjs";
4
4
  import { createContainer } from "iti";
5
5
  //#region src/adapters/iti.ts
6
6
  /**
@@ -19,8 +19,7 @@ var ItiLayerBackend = class {
19
19
  * retry boundary for that sticky failure behavior.
20
20
  */
21
21
  pending = /* @__PURE__ */ new Map();
22
- keyFor(token) {
23
- const tag = token.serviceTag;
22
+ keyFor(tag) {
24
23
  const existing = this.keys.get(tag);
25
24
  if (existing) return existing;
26
25
  const key = `better-effect:${tag}`;
@@ -29,20 +28,22 @@ var ItiLayerBackend = class {
29
28
  }
30
29
  /** Register a Layer provider under its deterministic Service-tag key. */
31
30
  register(registration) {
32
- const token = registration.service;
33
- const tag = token.serviceTag;
31
+ const normalized = normalizeLayerRegistration(registration);
32
+ const token = normalized.service;
33
+ const tag = captureLayerRegistrationTag(normalized);
34
34
  const existing = this.registered.get(tag);
35
35
  if (existing === token) throw new DuplicateServiceError(token);
36
36
  if (existing) throw new ServiceTagCollisionError(existing, token);
37
- const key = this.keyFor(token);
38
- this.container = this.container.add({ [key]: registration.acquire });
37
+ const key = this.keyFor(tag);
38
+ this.container = this.container.add({ [key]: normalized.acquire });
39
39
  this.registered.set(tag, token);
40
40
  }
41
41
  /** Resolve a registered Service through the ITI container. */
42
42
  resolve(token) {
43
- const registered = this.registered.get(token.serviceTag);
43
+ const tag = captureServiceTag(token);
44
+ const registered = this.registered.get(tag);
44
45
  if (registered === void 0) throw new ServiceNotFoundError(token);
45
- const key = this.keyFor(token);
46
+ const key = this.keyFor(tag);
46
47
  const resolved = this.container.get(key);
47
48
  const validate = (instance) => {
48
49
  assertServiceCompatibility(token, registered, instance);
@@ -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 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"}
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'\nimport { captureServiceTag } from '../service/tag'\n\nimport { assertServiceCompatibility } from '../layer/internal-identity'\nimport { captureLayerRegistrationTag, normalizeLayerRegistration } from '../layer/registration'\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(tag: string): string {\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 normalized = normalizeLayerRegistration(registration)\n const token = normalized.service\n const tag = captureLayerRegistrationTag(normalized)\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(tag)\n\n this.container = this.container.add({\n [key]: normalized.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 tag = captureServiceTag(token)\n const registered = this.registered.get(tag)\n\n if (registered === undefined) {\n throw new ServiceNotFoundError(token)\n }\n\n const key = this.keyFor(tag)\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":";;;;;;;;;;;AAyBA,IAAa,kBAAb,MAAqD;CACnD,YAAyB,gBAAgB;CAEzC,uBAAwB,IAAI,IAAoB;CAEhD,6BAA8B,IAAI,IAA6B;;;;;;CAO/D,0BAA2B,IAAI,IAAuC;CAEtE,OAAe,KAAqB;EAClC,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,aAAa,2BAA2B,YAAY;EAC1D,MAAM,QAAQ,WAAW;EACzB,MAAM,MAAM,4BAA4B,UAAU;EAClD,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,GAAG;EAE3B,KAAK,YAAY,KAAK,UAAU,IAAI,GACjC,MAAM,WAAW,QACpB,CAAC;EAED,KAAK,WAAW,IAAI,KAAK,KAAK;CAChC;;CAGA,QAAmC,OAA0D;EAC3F,MAAM,MAAM,kBAAkB,KAAK;EACnC,MAAM,aAAa,KAAK,WAAW,IAAI,GAAG;EAE1C,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,qBAAqB,KAAK;EAGtC,MAAM,MAAM,KAAK,OAAO,GAAG;EAC3B,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"}
@@ -1,5 +1,5 @@
1
1
  import { t as isPromiseLike } from "./runtime-CDcCF5cb.mjs";
2
- import { t as Scope } from "./scope-GGnmTQck.mjs";
2
+ import { i as Scope, n as inheritProgramName, r as setProgramName } from "./program-metadata-DhQCCirO.mjs";
3
3
  import { Err, Result, matchError, matchErrorPartial } from "better-result";
4
4
  //#region src/effect/combinators.ts
5
5
  const asResult = (value) => {
@@ -227,10 +227,22 @@ const recoverRuntime = (effect, recover) => {
227
227
  if (isAsyncEffect(effect)) return Promise.resolve(effect).then((result) => Result.tryRecoverAsync(result, continueWith));
228
228
  return Result.tryRecoverAsync(effect, continueWith);
229
229
  };
230
- const mapProgram = (program, fn) => asProgram(() => mapRuntime(program(), fn));
231
- const mapErrorProgram = (program, fn) => asProgram(() => mapErrorRuntime(program(), fn));
232
- const tapProgram = (program, fn) => asProgram(() => tapRuntime(program(), fn));
233
- const tapErrorProgram = (program, fn) => asProgram(() => tapErrorRuntime(program(), fn));
230
+ const mapProgram = (program, fn) => {
231
+ const mapped = asProgram(() => mapRuntime(program(), fn));
232
+ return inheritProgramName(program, mapped);
233
+ };
234
+ const mapErrorProgram = (program, fn) => {
235
+ const mapped = asProgram(() => mapErrorRuntime(program(), fn));
236
+ return inheritProgramName(program, mapped);
237
+ };
238
+ const tapProgram = (program, fn) => {
239
+ const tapped = asProgram(() => tapRuntime(program(), fn));
240
+ return inheritProgramName(program, tapped);
241
+ };
242
+ const tapErrorProgram = (program, fn) => {
243
+ const tapped = asProgram(() => tapErrorRuntime(program(), fn));
244
+ return inheritProgramName(program, tapped);
245
+ };
234
246
  function map(first, second) {
235
247
  if (second === void 0) return (program) => {
236
248
  return mapProgram(program, first);
@@ -244,8 +256,13 @@ function mapError(first, second) {
244
256
  return mapErrorProgram(first, second);
245
257
  }
246
258
  function andThen(first, second) {
247
- if (second === void 0) return (program) => () => andThenRuntime(program(), first);
248
- return () => andThenRuntime(first(), second);
259
+ if (second === void 0) return (program) => {
260
+ const chained = () => andThenRuntime(program(), first);
261
+ return inheritProgramName(program, chained);
262
+ };
263
+ const source = first;
264
+ const chained = () => andThenRuntime(source(), second);
265
+ return inheritProgramName(source, chained);
249
266
  }
250
267
  function tap(first, second) {
251
268
  if (second === void 0) return (program) => {
@@ -260,8 +277,13 @@ function tapError(first, second) {
260
277
  return tapErrorProgram(first, second);
261
278
  }
262
279
  function recover(first, second) {
263
- if (second === void 0) return (program) => () => recoverRuntime(program(), first);
264
- return () => recoverRuntime(first(), second);
280
+ if (second === void 0) return (program) => {
281
+ const recovered = () => recoverRuntime(program(), first);
282
+ return inheritProgramName(program, recovered);
283
+ };
284
+ const source = first;
285
+ const recovered = () => recoverRuntime(source(), second);
286
+ return inheritProgramName(source, recovered);
265
287
  }
266
288
  //#endregion
267
289
  //#region src/effect/program-scheduler.ts
@@ -326,6 +348,14 @@ function fn(body) {
326
348
  const program = () => runResultGenerator(body);
327
349
  return program;
328
350
  }
351
+ const validateProgramName = (name) => {
352
+ if (typeof name !== "string") throw new TypeError("Program name must be a string");
353
+ };
354
+ function programNamed(name, program) {
355
+ validateProgramName(name);
356
+ if (program === void 0) return (source) => setProgramName(source, name);
357
+ return setProgramName(program, name);
358
+ }
329
359
  const runShortCircuitingCollection = async (length, task, concurrency) => {
330
360
  const outcome = await runProgramCollection(length, task, {
331
361
  concurrency,
@@ -348,26 +378,42 @@ const runAllResultsCollection = async (length, task, concurrency) => {
348
378
  /** Build a lazy Program collection with optional bounded concurrency. */
349
379
  function programAll(programs, options = {}) {
350
380
  const concurrency = options.concurrency;
381
+ const name = options?.name;
351
382
  validateProgramConcurrency(concurrency);
352
383
  const program = () => runShortCircuitingCollection(programs.length, (index) => programs[index](), concurrency);
384
+ if (name !== void 0) {
385
+ validateProgramName(name);
386
+ setProgramName(program, name);
387
+ }
353
388
  return program;
354
389
  }
355
390
  /** Build a lazy Program collection from an input array and Program factory. */
356
391
  function programForEach(items, makeProgram, options = {}) {
357
392
  const concurrency = options.concurrency;
393
+ const name = options?.name;
358
394
  validateProgramConcurrency(concurrency);
359
395
  const program = () => runShortCircuitingCollection(items.length, (index) => makeProgram(items[index], index)(), concurrency);
396
+ if (name !== void 0) {
397
+ validateProgramName(name);
398
+ setProgramName(program, name);
399
+ }
360
400
  return program;
361
401
  }
362
402
  /** Build a lazy Program collection that retains every child Result. */
363
403
  function programAllResults(programs, options = {}) {
364
404
  const concurrency = options.concurrency;
405
+ const name = options?.name;
365
406
  validateProgramConcurrency(concurrency);
366
407
  const program = () => runAllResultsCollection(programs.length, (index) => programs[index](), concurrency);
408
+ if (name !== void 0) {
409
+ validateProgramName(name);
410
+ setProgramName(program, name);
411
+ }
367
412
  return program;
368
413
  }
369
414
  /** Value-level namespace for lazy Program combinators. */
370
415
  const Program = {
416
+ named: programNamed,
371
417
  all: programAll,
372
418
  forEach: programForEach,
373
419
  allResults: programAllResults,
@@ -510,4 +556,4 @@ const Effect = {
510
556
  //#endregion
511
557
  export { Program as n, Effect as t };
512
558
 
513
- //# sourceMappingURL=effect-2ZcGZI8A.mjs.map
559
+ //# sourceMappingURL=effect-CK4YZaX0.mjs.map