better-effect 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,28 +1,99 @@
1
- import { a as LayerRegistrationError, i as LayerGeneratorYieldError, n as DuplicateServiceError, o as ServiceNotFoundError, r as LayerDisposeError, s as ServiceRuntimeNotConfiguredError, t as BuiltLayerDisposedError } from "./errors-DlHCwICc.mjs";
1
+ import { a as ServiceTagCollisionError, i as LayerRegistrationError, n as LayerDisposeError, o as ServiceNotFoundError, r as LayerGeneratorYieldError, s as ServiceRuntimeNotConfiguredError, t as DuplicateServiceError } from "./errors-GR3K_nRu.mjs";
2
+ import { t as isPromiseLike } from "./runtime-CDcCF5cb.mjs";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
3
4
  import { Result, TaggedError } from "better-result";
4
5
  //#region src/service/runtime.ts
5
6
  const storage$1 = new AsyncLocalStorage();
7
+ /** Provides the resolver context used by Service tokens during execution. */
6
8
  var ServiceRuntime = class ServiceRuntime {
9
+ /**
10
+ * Run a callback with a resolver available to `yield* Service` expressions.
11
+ *
12
+ * The context is scoped to the callback and is restored afterward.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const value = ServiceRuntime.run(resolver, () => {
17
+ * return ServiceRuntime.resolve(Database)
18
+ * })
19
+ * ```
20
+ */
7
21
  static run(resolver, program) {
8
22
  return storage$1.run(resolver, program);
9
23
  }
24
+ /** Return the resolver active in the current execution context. */
10
25
  static current() {
11
26
  const resolver = storage$1.getStore();
12
27
  if (!resolver) throw new ServiceRuntimeNotConfiguredError();
13
28
  return resolver;
14
29
  }
30
+ /** Resolve a Service token using the active resolver. */
15
31
  static async resolve(token) {
16
32
  return await ServiceRuntime.current().resolve(token);
17
33
  }
18
34
  };
19
35
  //#endregion
20
36
  //#region src/service/service.ts
37
+ /**
38
+ * Declare a class-backed Service with a stable string-literal identity.
39
+ *
40
+ * The returned class is simultaneously the implementation type, the runtime
41
+ * dependency token, and the value yielded by `yield*` in an Effect generator.
42
+ * The explicit self type preserves exact instance inference, while the second
43
+ * call captures the tag as a literal for Layer composition and diagnostics.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * class Database extends Service<Database>()('Database') {
48
+ * query(): string {
49
+ * return 'ok'
50
+ * }
51
+ * }
52
+ *
53
+ * const database = yield* Database
54
+ * database.query()
55
+ * ```
56
+ *
57
+ * @typeParam Self The instance type implemented by the declared Service.
58
+ */
21
59
  function Service() {
22
- return class {
23
- static async *[Symbol.asyncIterator]() {
24
- return await ServiceRuntime.resolve(this);
60
+ return function(tag) {
61
+ if (tag.length === 0) throw new TypeError("Service tags must not be empty");
62
+ class BaseService {
63
+ /** The stable logical identity used by Layers and resolver backends. */
64
+ static serviceTag = tag;
65
+ /**
66
+ * Type-check a structural implementation of this Service.
67
+ *
68
+ * This is an identity helper. It returns the supplied value unchanged
69
+ * and does not invoke a constructor or modify its prototype.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * class Database extends Service<Database>()('Database') {
74
+ * query(sql: string): string {
75
+ * return sql
76
+ * }
77
+ * }
78
+ *
79
+ * const database = Database.of({
80
+ * query: (sql) => `Result: ${sql}`
81
+ * })
82
+ *
83
+ * database.query('SELECT 1')
84
+ * // 'Result: SELECT 1'
85
+ * // database is the original object, not an instance of Database
86
+ * ```
87
+ */
88
+ static of(implementation) {
89
+ return implementation;
90
+ }
91
+ /** Resolve this Service from the resolver active in the current runtime. */
92
+ static async *[Symbol.asyncIterator]() {
93
+ return await ServiceRuntime.resolve(this);
94
+ }
25
95
  }
96
+ return BaseService;
26
97
  };
27
98
  }
28
99
  //#endregion
@@ -39,74 +110,116 @@ const runLayerGenerator = async (service, factory) => {
39
110
  };
40
111
  //#endregion
41
112
  //#region src/layer/layer.ts
113
+ /**
114
+ * Declarative collection of Service providers.
115
+ *
116
+ * A Layer describes how to acquire implementations; it does not execute
117
+ * providers until a `Runtime` is created. Use `merge` to compose distinct
118
+ * providers and `override` when replacing an existing provider intentionally.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const AppLive = Layer.merge(
123
+ * Layer.succeed(Database, database),
124
+ * Layer.make(UserRepository)
125
+ * )
126
+ *
127
+ * const runtime = await Runtime.make(AppLive, backend)
128
+ * ```
129
+ */
42
130
  var Layer = class Layer {
131
+ /** The provider registrations retained by this Layer. */
43
132
  providers;
44
133
  constructor(providers) {
45
134
  this.providers = Object.freeze([...providers]);
46
135
  }
47
136
  static make(service, acquire) {
137
+ const defaultAcquire = () => {
138
+ return new service();
139
+ };
140
+ const normalizedAcquire = normalizeAcquire(acquire ?? defaultAcquire);
48
141
  return new Layer([{
49
142
  service,
50
- acquire
143
+ acquire: normalizedAcquire
51
144
  }]);
52
145
  }
146
+ /** Create a Layer from an already-constructed Service instance. */
53
147
  static succeed(service, instance) {
54
- return Layer.make(service, () => instance);
148
+ const normalizedAcquire = normalizeAcquire(() => instance);
149
+ return new Layer([{
150
+ service,
151
+ acquire: normalizedAcquire
152
+ }]);
55
153
  }
56
- /**
57
- * Define a dependency-free provider with Runtime-root cleanup.
58
- *
59
- * The release callback intentionally keeps its compatibility-friendly
60
- * one-argument shape. Use `scopedGen` when cleanup needs `ScopeOutcome`.
61
- */
154
+ /** Define a provider with Runtime-root cleanup. */
62
155
  static scoped(service, acquire, release) {
63
156
  return new Layer([{
64
157
  service,
65
- acquire,
66
- release: (instance) => release(instance)
158
+ acquire: normalizeAcquire(acquire),
159
+ release: (instance) => {
160
+ return release(instance);
161
+ }
67
162
  }]);
68
163
  }
69
- /** Define a contextual provider with Runtime-root, outcome-aware cleanup. */
164
+ /** Define a provider whose acquisition can yield contextual Services. */
70
165
  static scopedGen(service, factory, release) {
71
166
  return new Layer([{
72
167
  service,
73
168
  acquire: () => runLayerGenerator(service, factory),
74
- release: (instance, outcome) => release(instance, outcome)
169
+ release: (instance, outcome) => {
170
+ return release(instance, outcome);
171
+ }
75
172
  }]);
76
173
  }
174
+ /** Define a provider whose acquisition can yield contextual Services. */
77
175
  static gen(service, factory) {
78
- return Layer.make(service, () => runLayerGenerator(service, factory));
176
+ return new Layer([{
177
+ service,
178
+ acquire: () => runLayerGenerator(service, factory)
179
+ }]);
79
180
  }
181
+ /** Compose Layers without replacing providers. */
80
182
  static merge(...layers) {
81
183
  const providers = /* @__PURE__ */ new Map();
82
184
  for (const layer of layers) for (const provider of layer.providers) {
83
185
  const service = provider.service;
84
- if (providers.has(service)) throw new DuplicateServiceError(service);
85
- providers.set(service, provider);
186
+ const existing = providers.get(service.serviceTag);
187
+ if (existing) {
188
+ if (existing.service !== service) throw new ServiceTagCollisionError(existing.service, service);
189
+ throw new DuplicateServiceError(service);
190
+ }
191
+ providers.set(service.serviceTag, provider);
86
192
  }
87
193
  return new Layer([...providers.values()]);
88
194
  }
195
+ /** Replace providers in a base Layer, using tag identity and compatible contracts. */
89
196
  static override(base, ...overrides) {
90
197
  const providers = /* @__PURE__ */ new Map();
91
- for (const provider of base.providers) providers.set(provider.service, provider);
92
- for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service, provider);
198
+ for (const provider of base.providers) providers.set(provider.service.serviceTag, provider);
199
+ for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service.serviceTag, provider);
93
200
  return new Layer([...providers.values()]);
94
201
  }
95
202
  };
203
+ const normalizeAcquire = (acquire) => () => {
204
+ return acquire();
205
+ };
96
206
  //#endregion
97
207
  //#region src/scope/errors.ts
208
+ /** Thrown when Scope context is accessed outside an active Scope execution. */
98
209
  var ScopeRuntimeNotConfiguredError = class extends Error {
99
210
  constructor() {
100
211
  super("No Scope is available in the current execution context");
101
212
  this.name = "ScopeRuntimeNotConfiguredError";
102
213
  }
103
214
  };
215
+ /** Thrown when a resource or finalizer is added after Scope closure begins. */
104
216
  var ScopeClosedError = class extends Error {
105
217
  constructor() {
106
218
  super("Cannot add resources or finalizers to a closed Scope");
107
219
  this.name = "ScopeClosedError";
108
220
  }
109
221
  };
222
+ /** Aggregates finalizer failures encountered while closing a Scope. */
110
223
  var ScopeCloseError = class extends Error {
111
224
  causes;
112
225
  constructor(causes) {
@@ -115,6 +228,7 @@ var ScopeCloseError = class extends Error {
115
228
  this.name = "ScopeCloseError";
116
229
  }
117
230
  };
231
+ /** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
118
232
  var ResourceNotDisposableError = class extends Error {
119
233
  constructor() {
120
234
  super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
@@ -124,23 +238,28 @@ var ResourceNotDisposableError = class extends Error {
124
238
  //#endregion
125
239
  //#region src/scope/disposable.ts
126
240
  const SCOPE_SUCCESS$2 = { status: "success" };
241
+ /** Return a Scope finalizer for a value's async or sync disposal protocol. */
127
242
  const getDisposeFinalizer = (resource) => {
128
243
  const candidate = Object(resource);
129
244
  const asyncDispose = candidate[Symbol.asyncDispose];
130
- if (typeof asyncDispose === "function") return () => asyncDispose.call(resource);
245
+ if (asyncDispose instanceof Function) return () => asyncDispose.call(resource);
131
246
  const dispose = candidate[Symbol.dispose];
132
- if (typeof dispose === "function") return () => dispose.call(resource);
247
+ if (dispose instanceof Function) return () => dispose.call(resource);
133
248
  };
249
+ /** Dispose a value immediately when it implements a disposal protocol. */
134
250
  const disposeResource = (resource) => {
135
251
  return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
136
252
  };
137
253
  //#endregion
138
254
  //#region src/scope/runtime.ts
139
255
  const storage = new AsyncLocalStorage();
256
+ /** Bridges the current Scope through async execution context. */
140
257
  var ScopeRuntime = class {
258
+ /** Supply a Scope while invoking a callback. */
141
259
  static run(scope, program) {
142
260
  return storage.run(scope, program);
143
261
  }
262
+ /** Return the Scope active in the current execution context. */
144
263
  static current() {
145
264
  const scope = storage.getStore();
146
265
  if (!scope) throw new ScopeRuntimeNotConfiguredError();
@@ -286,15 +405,19 @@ var ScopeImpl = class ScopeImpl {
286
405
  }
287
406
  };
288
407
  const Scope = {
408
+ /** Create an owned, initially open Scope. */
289
409
  make() {
290
410
  return new ScopeImpl();
291
411
  },
412
+ /** Return the non-owning Scope available in the current execution context. */
292
413
  current() {
293
414
  return ScopeRuntime.current();
294
415
  },
416
+ /** Run a callback with an existing Scope supplied as the current context. */
295
417
  provide(scope, program) {
296
418
  return ScopeRuntime.run(scope, program);
297
419
  },
420
+ /** Resolve the current Scope through `yield* Scope` inside an Effect. */
298
421
  *[Symbol.iterator]() {
299
422
  return ScopeRuntime.current();
300
423
  },
@@ -304,6 +427,14 @@ const Scope = {
304
427
  * Scope is independent from `better-result`, so returned values—including
305
428
  * `Result.err`—close this Scope with a successful outcome. Result-aware
306
429
  * outcome classification belongs to `Runtime.run`.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * await Scope.run(async (scope) => {
434
+ * const connection = await scope.acquire(connect, (connection) => connection.close())
435
+ * return connection.query()
436
+ * })
437
+ * ```
307
438
  */
308
439
  run(program) {
309
440
  const scope = new ScopeImpl();
@@ -311,8 +442,222 @@ const Scope = {
311
442
  }
312
443
  };
313
444
  //#endregion
445
+ //#region src/effect/combinators.ts
446
+ const mapResult = (result, fn) => {
447
+ return Result.map(result, fn);
448
+ };
449
+ const mapErrorResult = (result, fn) => {
450
+ return Result.mapError(result, fn);
451
+ };
452
+ const andThenResult = (result, next) => {
453
+ const resultNext = next;
454
+ return Result.andThen(result, resultNext);
455
+ };
456
+ const andThenAsyncResult = (result, next) => {
457
+ const resultNext = (value) => {
458
+ return Promise.resolve(next(value));
459
+ };
460
+ return Result.andThenAsync(result, resultNext);
461
+ };
462
+ function map(first, second) {
463
+ if (first instanceof Function && second === void 0) {
464
+ const callback = first;
465
+ return (effect) => {
466
+ return map(effect, callback);
467
+ };
468
+ }
469
+ const fn = second;
470
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
471
+ return mapResult(result, fn);
472
+ });
473
+ return mapResult(first, fn);
474
+ }
475
+ function mapError(first, second) {
476
+ if (first instanceof Function && second === void 0) {
477
+ const callback = first;
478
+ return (effect) => {
479
+ return mapError(effect, callback);
480
+ };
481
+ }
482
+ const fn = second;
483
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
484
+ return mapErrorResult(result, fn);
485
+ });
486
+ return mapErrorResult(first, fn);
487
+ }
488
+ function andThen(first, second) {
489
+ if (first instanceof Function && second === void 0) {
490
+ const callback = first;
491
+ return (effect) => {
492
+ return andThen(effect, callback);
493
+ };
494
+ }
495
+ return andThenResult(first, second);
496
+ }
497
+ function andThenAsync(first, second) {
498
+ if (first instanceof Function && second === void 0) {
499
+ const callback = first;
500
+ return (effect) => {
501
+ return andThenAsync(effect, callback);
502
+ };
503
+ }
504
+ const next = second;
505
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
506
+ return andThenAsyncResult(result, next);
507
+ });
508
+ return andThenAsyncResult(first, next);
509
+ }
510
+ //#endregion
511
+ //#region src/effect/effect.ts
512
+ function gen(body) {
513
+ const runResultGenerator = Result.gen;
514
+ return runResultGenerator(body);
515
+ }
516
+ /**
517
+ * Acquire a resource in the current Scope and register its release callback.
518
+ *
519
+ * Acquisition failures are represented in the Effect Result error channel;
520
+ * release failures remain owned by Scope cleanup. The release callback
521
+ * receives the final outcome chosen by the enclosing execution boundary.
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * const connection = yield* Effect.acquireRelease(
526
+ * () => pool.connect(),
527
+ * (connection, outcome) => connection.close(outcome)
528
+ * )
529
+ * ```
530
+ */
531
+ function acquireRelease(acquire, release) {
532
+ const scope = Scope.current();
533
+ return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
534
+ }
535
+ /**
536
+ * Register an already-acquired disposable resource in the current Scope.
537
+ *
538
+ * The resource is not acquired by this helper. Registration failures are
539
+ * represented in the Effect Result error channel; disposal failures remain
540
+ * owned by Scope cleanup.
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * const file = yield* Effect.add(await openFile('notes.txt'))
545
+ * ```
546
+ */
547
+ function add(resource) {
548
+ const scope = Scope.current();
549
+ return Result.await(Result.tryPromise(() => scope.add(resource)));
550
+ }
551
+ /**
552
+ * Effect namespace containing generator, resource, and Result combinators.
553
+ *
554
+ * Prefer these helpers when a program needs typed Service requirements or
555
+ * Scope-aware acquisition and cleanup.
556
+ */
557
+ const Effect = {
558
+ /** Compose a generator-based Effect program. */
559
+ gen,
560
+ /** Acquire and register a resource in the current Scope. */
561
+ acquireRelease,
562
+ /** Register an already-acquired disposable in the current Scope. */
563
+ add,
564
+ /** Map a successful Effect result. */
565
+ map,
566
+ /** Map an Effect error. */
567
+ mapError,
568
+ /** Chain a synchronous Effect result. */
569
+ andThen,
570
+ /** Chain an asynchronous Effect result. */
571
+ andThenAsync
572
+ };
573
+ //#endregion
574
+ //#region src/function/pipe.ts
575
+ function pipe(value, ...operations) {
576
+ return operations.reduce((current, operation) => operation(current), value);
577
+ }
578
+ //#endregion
579
+ //#region src/resource/errors.ts
580
+ /** Describes a failure encountered while releasing a Resource. */
581
+ var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
582
+ //#endregion
583
+ //#region src/resource/internal.ts
584
+ const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
585
+ resource,
586
+ cause,
587
+ message: `Failed to release resource: ${resource}`
588
+ });
589
+ const runResult = async (operation) => {
590
+ return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
591
+ };
592
+ const normalizeReleaseOutcome = (name, outcome) => {
593
+ if (outcome === void 0) return Result.ok();
594
+ return outcome.mapError((cause) => toReleaseFailure(name, cause));
595
+ };
596
+ const runRelease = async (name, resource, release) => {
597
+ return (await Result.tryPromise({
598
+ try: () => Promise.resolve(release(resource)),
599
+ catch: (cause) => toReleaseFailure(name, cause)
600
+ })).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
601
+ };
602
+ const notifyReleaseFailure = async (observer, failure) => {
603
+ if (!observer) return;
604
+ try {
605
+ await observer(failure);
606
+ } catch {}
607
+ };
608
+ const combineUseAndRelease = async (used, released, onReleaseFailure) => {
609
+ if (Result.isError(used)) {
610
+ if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
611
+ return Result.err(used.error);
612
+ }
613
+ if (Result.isError(released)) {
614
+ await notifyReleaseFailure(onReleaseFailure, released.error);
615
+ return Result.err(released.error);
616
+ }
617
+ return Result.ok(used.value);
618
+ };
619
+ //#endregion
620
+ //#region src/resource/resource.ts
621
+ /**
622
+ * Acquire a resource, use it, and always attempt release afterward.
623
+ *
624
+ * Acquisition, use, and release may be synchronous or asynchronous Result
625
+ * operations. If both use and release fail, the use error remains primary and
626
+ * `onReleaseFailure` receives the cleanup failure as a diagnostic.
627
+ *
628
+ * When `release` is omitted, `Symbol.asyncDispose` is preferred over
629
+ * `Symbol.dispose`.
630
+ *
631
+ * @example
632
+ * ```ts
633
+ * const result = await Resource.acquireUseRelease({
634
+ * name: 'database connection',
635
+ * acquire: () => connect(),
636
+ * use: (connection) => query(connection),
637
+ * release: (connection) => connection.close()
638
+ * })
639
+ * ```
640
+ */
641
+ const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
642
+ const resource = yield* Result.await(runResult(acquire));
643
+ const scope = Scope.make();
644
+ let released = Result.ok();
645
+ scope.addFinalizer(async () => {
646
+ released = await runRelease(name, resource, release);
647
+ });
648
+ const used = await runResult(() => use(resource));
649
+ await scope.close();
650
+ return await combineUseAndRelease(used, released, onReleaseFailure);
651
+ });
652
+ const Resource = {
653
+ /** Acquire, use, and release a resource with deterministic error precedence. */
654
+ acquireUseRelease };
655
+ //#endregion
314
656
  //#region src/runtime/outcome.ts
315
- const isResultLike = (value) => typeof value === "object" && value !== null && "status" in value && (value.status === "ok" || value.status === "error");
657
+ const isResultLike = (value) => {
658
+ const candidate = Object(value);
659
+ return Object.prototype.toString.call(value) !== "[object Function]" && "status" in candidate && (candidate.status === "ok" || candidate.status === "error");
660
+ };
316
661
  const classifyRuntimeOutcome = (value) => {
317
662
  if (isResultLike(value) && Result.isError(value)) return {
318
663
  status: "failure",
@@ -323,6 +668,12 @@ const classifyRuntimeOutcome = (value) => {
323
668
  //#endregion
324
669
  //#region src/layer/runtime.ts
325
670
  const SCOPE_SUCCESS = Object.freeze({ status: "success" });
671
+ var RuntimeHandleDisposedError = class extends Error {
672
+ constructor() {
673
+ super("Cannot run a program using a disposed Layer");
674
+ this.name = "RuntimeHandleDisposedError";
675
+ }
676
+ };
326
677
  const normalizeDisposeCauses = (cause) => {
327
678
  if (cause instanceof AggregateError) return [...cause.errors];
328
679
  return [cause];
@@ -340,7 +691,7 @@ const bindProviderToScope = (provider, rootScope) => ({
340
691
  return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
341
692
  })
342
693
  });
343
- var BuiltLayerImpl = class {
694
+ var RuntimeHandleImpl = class {
344
695
  backend;
345
696
  rootScope;
346
697
  onCleanupFailure;
@@ -416,15 +767,11 @@ var BuiltLayerImpl = class {
416
767
  }
417
768
  }
418
769
  assertActive() {
419
- if (this.state !== "active") throw new BuiltLayerDisposedError();
770
+ if (this.state !== "active") throw new RuntimeHandleDisposedError();
420
771
  }
421
772
  };
422
- /**
423
- * Build a low-level Layer handle.
424
- *
425
- * @deprecated Prefer `Runtime.make()` for application code.
426
- */
427
- const buildLayer = async (layer, backend, options = {}) => {
773
+ /** Build a Runtime handle for a complete Layer and register its providers. */
774
+ const createRuntimeHandle = async (layer, backend, options = {}) => {
428
775
  const rootScope = Scope.make();
429
776
  let current;
430
777
  try {
@@ -455,149 +802,53 @@ const buildLayer = async (layer, backend, options = {}) => {
455
802
  error: shutdownError
456
803
  });
457
804
  }
458
- let cleanupCause;
459
- if (cleanupCauses.length === 1) cleanupCause = cleanupCauses[0];
460
- else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
805
+ const cleanupCause = cleanupCauses.length === 1 ? cleanupCauses[0] : cleanupCauses.length > 1 ? new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses)) : void 0;
461
806
  throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
462
807
  }
463
- return new BuiltLayerImpl(backend, rootScope, options.onCleanupFailure);
464
- };
465
- //#endregion
466
- //#region src/effect/combinators.ts
467
- const isPromiseLike = (value) => {
468
- if (typeof value !== "object" && typeof value !== "function" || value === null) return false;
469
- return "then" in value && typeof value.then === "function";
470
- };
471
- const mapResult = (result, fn) => Result.map(result, fn);
472
- const mapErrorResult = (result, fn) => Result.mapError(result, fn);
473
- const andThenResult = (result, next) => {
474
- const chained = Result.andThen(result, next);
475
- if (!isPromiseLike(chained)) return chained;
476
- return Result.andThenAsync(result, () => Promise.resolve(chained));
808
+ return new RuntimeHandleImpl(backend, rootScope, options.onCleanupFailure);
477
809
  };
478
- function map(first, second) {
479
- if (typeof first === "function" && second === void 0) return (effect) => map(effect, first);
480
- const fn = second;
481
- if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapResult(result, fn));
482
- return mapResult(first, fn);
483
- }
484
- function mapError(first, second) {
485
- if (typeof first === "function" && second === void 0) return (effect) => mapError(effect, first);
486
- const fn = second;
487
- if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapErrorResult(result, fn));
488
- return mapErrorResult(first, fn);
489
- }
490
- function andThen(first, second) {
491
- if (typeof first === "function" && second === void 0) return (effect) => andThen(effect, first);
492
- const next = second;
493
- if (isPromiseLike(first)) return Promise.resolve(first).then((result) => andThenResult(result, next));
494
- return andThenResult(first, next);
495
- }
496
810
  //#endregion
497
- //#region src/effect/effect.ts
498
- function gen(body) {
499
- return Result.gen(body);
500
- }
811
+ //#region src/runtime/runtime.ts
501
812
  /**
502
- * Acquire a resource in the current Scope and register its release callback.
813
+ * Long-lived execution environment backed by a complete Layer.
503
814
  *
504
- * Acquisition failures are represented in the Effect Result error channel;
505
- * release failures remain owned by Scope cleanup.
506
- */
507
- function acquireRelease(acquire, release) {
508
- const scope = Scope.current();
509
- return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
510
- }
511
- /**
512
- * Register an already-acquired disposable resource in the current Scope.
815
+ * A Runtime owns Layer resources until `dispose()` is called. Each `run()` is
816
+ * isolated in a child Scope, while Layer-scoped resources remain shared.
513
817
  *
514
- * Registration failures are represented in the Effect Result error channel;
515
- * disposal failures remain owned by Scope cleanup.
818
+ * @example
819
+ * ```ts
820
+ * const runtime = await Runtime.make(AppLive, new MemoryLayerBackend())
821
+ * const result = await runtime.run(loadUser('u1'))
822
+ * await runtime.dispose()
823
+ * ```
824
+ *
825
+ * @typeParam Provided The branded Service instances supplied by the Layer.
516
826
  */
517
- function add(resource) {
518
- const scope = Scope.current();
519
- return Result.await(Result.tryPromise(() => scope.add(resource)));
520
- }
521
- const Effect = {
522
- gen,
523
- acquireRelease,
524
- add,
525
- map,
526
- mapError,
527
- andThen
528
- };
529
- //#endregion
530
- //#region src/function/pipe.ts
531
- function pipe(value, ...operations) {
532
- return operations.reduce((current, operation) => operation(current), value);
533
- }
534
- //#endregion
535
- //#region src/resource/errors.ts
536
- var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
537
- //#endregion
538
- //#region src/resource/internal.ts
539
- const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
540
- resource,
541
- cause,
542
- message: `Failed to release resource: ${resource}`
543
- });
544
- const runResult = async (operation) => {
545
- return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
546
- };
547
- const normalizeReleaseOutcome = (name, outcome) => {
548
- if (outcome === void 0) return Result.ok();
549
- return outcome.mapError((cause) => toReleaseFailure(name, cause));
550
- };
551
- const runRelease = async (name, resource, release) => {
552
- return (await Result.tryPromise({
553
- try: () => Promise.resolve(release(resource)),
554
- catch: (cause) => toReleaseFailure(name, cause)
555
- })).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
556
- };
557
- const notifyReleaseFailure = async (observer, failure) => {
558
- if (!observer) return;
559
- try {
560
- await observer(failure);
561
- } catch {}
562
- };
563
- const combineUseAndRelease = async (used, released, onReleaseFailure) => {
564
- if (Result.isError(used)) {
565
- if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
566
- return Result.err(used.error);
567
- }
568
- if (Result.isError(released)) {
569
- await notifyReleaseFailure(onReleaseFailure, released.error);
570
- return Result.err(released.error);
571
- }
572
- return Result.ok(used.value);
573
- };
574
- //#endregion
575
- //#region src/resource/resource.ts
576
- const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
577
- const resource = yield* Result.await(runResult(acquire));
578
- const scope = Scope.make();
579
- let released = Result.ok();
580
- scope.addFinalizer(async () => {
581
- released = await runRelease(name, resource, release);
582
- });
583
- const used = await runResult(() => use(resource));
584
- await scope.close();
585
- return await combineUseAndRelease(used, released, onReleaseFailure);
586
- });
587
- const Resource = { acquireUseRelease };
588
- //#endregion
589
- //#region src/runtime/runtime.ts
590
827
  var Runtime = class Runtime {
591
- built;
592
- constructor(built) {
593
- this.built = built;
828
+ handle;
829
+ constructor(handle) {
830
+ this.handle = handle;
594
831
  }
595
- /** Create a long-lived Runtime that owns its Layer resources. */
832
+ /**
833
+ * Create a long-lived Runtime that owns its Layer resources.
834
+ *
835
+ * @example
836
+ * ```ts
837
+ * const runtime = await Runtime.make(AppLive, backend)
838
+ * const result = await runtime.run(program)
839
+ * await runtime.dispose()
840
+ * ```
841
+ */
596
842
  static async make(layer, backend, options = {}) {
597
- const built = await buildLayer(layer, backend, options);
598
- return new Runtime(built);
843
+ const handle = await createRuntimeHandle(layer, backend, options);
844
+ return new Runtime(handle);
599
845
  }
600
- /** Run one program and dispose its Layer resources before resolving. */
846
+ /**
847
+ * Run one program and dispose its Layer resources before resolving.
848
+ *
849
+ * This is convenient for request-style or command-style execution where a
850
+ * Runtime should not outlive the operation.
851
+ */
601
852
  static async run(layer, backend, program, options = {}) {
602
853
  const runtime = await Runtime.make(layer, backend, options);
603
854
  let value;
@@ -636,20 +887,20 @@ var Runtime = class Runtime {
636
887
  }
637
888
  /** Run one execution in this Runtime's child Scope. */
638
889
  run(program) {
639
- return this.built.run(program);
890
+ return this.handle.run(program);
640
891
  }
641
892
  runUnchecked(program) {
642
- return this.built.run(program);
893
+ return this.handle.run(program);
643
894
  }
644
895
  /** Stop new executions and release the Runtime's Layer resources. */
645
896
  dispose() {
646
- return this.built.dispose();
897
+ return this.handle.dispose();
647
898
  }
648
899
  disposeWithOutcome(outcome) {
649
- return this.built.dispose(outcome);
900
+ return this.handle.dispose(outcome);
650
901
  }
651
902
  };
652
903
  //#endregion
653
- export { BuiltLayerDisposedError, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, pipe };
904
+ export { DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
654
905
 
655
906
  //# sourceMappingURL=index.mjs.map