better-effect 0.3.0 → 0.5.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,98 @@
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-CnvKqBpb.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
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { Result, TaggedError } from "better-result";
4
4
  //#region src/service/runtime.ts
5
5
  const storage$1 = new AsyncLocalStorage();
6
+ /** Provides the resolver context used by Service tokens during execution. */
6
7
  var ServiceRuntime = class ServiceRuntime {
8
+ /**
9
+ * Run a callback with a resolver available to `yield* Service` expressions.
10
+ *
11
+ * The context is scoped to the callback and is restored afterward.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const value = ServiceRuntime.run(resolver, () => {
16
+ * return ServiceRuntime.resolve(Database)
17
+ * })
18
+ * ```
19
+ */
7
20
  static run(resolver, program) {
8
21
  return storage$1.run(resolver, program);
9
22
  }
23
+ /** Return the resolver active in the current execution context. */
10
24
  static current() {
11
25
  const resolver = storage$1.getStore();
12
26
  if (!resolver) throw new ServiceRuntimeNotConfiguredError();
13
27
  return resolver;
14
28
  }
29
+ /** Resolve a Service token using the active resolver. */
15
30
  static async resolve(token) {
16
31
  return await ServiceRuntime.current().resolve(token);
17
32
  }
18
33
  };
19
34
  //#endregion
20
35
  //#region src/service/service.ts
36
+ /**
37
+ * Declare a class-backed Service with a stable string-literal identity.
38
+ *
39
+ * The returned class is simultaneously the implementation type, the runtime
40
+ * dependency token, and the value yielded by `yield*` in an Effect generator.
41
+ * The explicit self type preserves exact instance inference, while the second
42
+ * call captures the tag as a literal for Layer composition and diagnostics.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * class Database extends Service<Database>()('Database') {
47
+ * query(): string {
48
+ * return 'ok'
49
+ * }
50
+ * }
51
+ *
52
+ * const database = yield* Database
53
+ * database.query()
54
+ * ```
55
+ *
56
+ * @typeParam Self The instance type implemented by the declared Service.
57
+ */
21
58
  function Service() {
22
- return class {
23
- static async *[Symbol.asyncIterator]() {
24
- return await ServiceRuntime.resolve(this);
59
+ return function(tag) {
60
+ if (tag.length === 0) throw new TypeError("Service tags must not be empty");
61
+ class BaseService {
62
+ /** The stable logical identity used by Layers and resolver backends. */
63
+ static serviceTag = tag;
64
+ /**
65
+ * Type-check a structural implementation of this Service.
66
+ *
67
+ * This is an identity helper. It returns the supplied value unchanged
68
+ * and does not invoke a constructor or modify its prototype.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * class Database extends Service<Database>()('Database') {
73
+ * query(sql: string): string {
74
+ * return sql
75
+ * }
76
+ * }
77
+ *
78
+ * const database = Database.of({
79
+ * query: (sql) => `Result: ${sql}`
80
+ * })
81
+ *
82
+ * database.query('SELECT 1')
83
+ * // 'Result: SELECT 1'
84
+ * // database is the original object, not an instance of Database
85
+ * ```
86
+ */
87
+ static of(implementation) {
88
+ return implementation;
89
+ }
90
+ /** Resolve this Service from the resolver active in the current runtime. */
91
+ static async *[Symbol.asyncIterator]() {
92
+ return await ServiceRuntime.resolve(this);
93
+ }
25
94
  }
95
+ return BaseService;
26
96
  };
27
97
  }
28
98
  //#endregion
@@ -39,20 +109,68 @@ const runLayerGenerator = async (service, factory) => {
39
109
  };
40
110
  //#endregion
41
111
  //#region src/layer/layer.ts
112
+ /**
113
+ * Declarative collection of Service providers.
114
+ *
115
+ * A Layer describes how to acquire implementations; it does not execute
116
+ * providers until a `Runtime` is created. Use `merge` to compose distinct
117
+ * providers and `override` when replacing an existing provider intentionally.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const AppLive = Layer.merge(
122
+ * Layer.succeed(Database, database),
123
+ * Layer.make(UserRepository)
124
+ * )
125
+ *
126
+ * const runtime = await Runtime.make(AppLive, backend)
127
+ * ```
128
+ */
42
129
  var Layer = class Layer {
130
+ /** The provider registrations retained by this Layer. */
43
131
  providers;
44
132
  constructor(providers) {
45
133
  this.providers = Object.freeze([...providers]);
46
134
  }
47
135
  static make(service, acquire) {
136
+ const defaultAcquire = () => {
137
+ return new service();
138
+ };
48
139
  return new Layer([{
49
140
  service,
50
- acquire
141
+ acquire: acquire ?? defaultAcquire
51
142
  }]);
52
143
  }
144
+ /**
145
+ * Create a Layer from an already-constructed Service instance.
146
+ *
147
+ * The instance is returned as-is whenever the Service is resolved.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * const DatabaseLive = Layer.succeed(Database, database)
152
+ * ```
153
+ */
53
154
  static succeed(service, instance) {
54
155
  return Layer.make(service, () => instance);
55
156
  }
157
+ /**
158
+ * Define a provider with Runtime-root cleanup.
159
+ *
160
+ * The release callback intentionally keeps its compatibility-friendly
161
+ * one-argument shape and runs when the owning Runtime is disposed. Use
162
+ * `scopedGen` when acquisition needs contextual Services or cleanup needs
163
+ * `ScopeOutcome`.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * const DatabaseLive = Layer.scoped(
168
+ * Database,
169
+ * () => openDatabase(),
170
+ * (database) => database.close()
171
+ * )
172
+ * ```
173
+ */
56
174
  static scoped(service, acquire, release) {
57
175
  return new Layer([{
58
176
  service,
@@ -60,6 +178,24 @@ var Layer = class Layer {
60
178
  release: (instance) => release(instance)
61
179
  }]);
62
180
  }
181
+ /**
182
+ * Define a provider whose acquisition can yield contextual Services.
183
+ *
184
+ * The release callback receives the acquired instance and the final
185
+ * `ScopeOutcome` selected by the owning Runtime.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const RepositoryLive = Layer.scopedGen(
190
+ * UserRepository,
191
+ * async function* () {
192
+ * const database = yield* Database
193
+ * return new UserRepository(database)
194
+ * },
195
+ * (repository, outcome) => repository.close(outcome)
196
+ * )
197
+ * ```
198
+ */
63
199
  static scopedGen(service, factory, release) {
64
200
  return new Layer([{
65
201
  service,
@@ -67,39 +203,84 @@ var Layer = class Layer {
67
203
  release: (instance, outcome) => release(instance, outcome)
68
204
  }]);
69
205
  }
206
+ /**
207
+ * Define a provider whose acquisition can yield contextual Services.
208
+ *
209
+ * Unlike `scopedGen`, this variant has no release callback. Use it for
210
+ * providers whose lifetime is managed elsewhere or that need no cleanup.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * const RepositoryLive = Layer.gen(UserRepository, async function* () {
215
+ * const database = yield* Database
216
+ * return new UserRepository(database)
217
+ * })
218
+ * ```
219
+ */
70
220
  static gen(service, factory) {
71
221
  return Layer.make(service, () => runLayerGenerator(service, factory));
72
222
  }
223
+ /**
224
+ * Compose Layers without replacing providers.
225
+ *
226
+ * Each Service tag may appear only once. Duplicate tags are rejected at
227
+ * runtime; use `override` when replacement is intentional.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * const AppLive = Layer.merge(DatabaseLive, RepositoryLive)
232
+ * ```
233
+ */
73
234
  static merge(...layers) {
74
235
  const providers = /* @__PURE__ */ new Map();
75
236
  for (const layer of layers) for (const provider of layer.providers) {
76
237
  const service = provider.service;
77
- if (providers.has(service)) throw new DuplicateServiceError(service);
78
- providers.set(service, provider);
238
+ const existing = providers.get(service.serviceTag);
239
+ if (existing) {
240
+ if (existing.service !== service) throw new ServiceTagCollisionError(existing.service, service);
241
+ throw new DuplicateServiceError(service);
242
+ }
243
+ providers.set(service.serviceTag, provider);
79
244
  }
80
245
  return new Layer([...providers.values()]);
81
246
  }
247
+ /**
248
+ * Replace providers in a base Layer, using tag identity and compatible
249
+ * instance contracts.
250
+ *
251
+ * Overrides are applied from left to right; the last compatible provider for
252
+ * a tag wins. Incompatible same-tag replacements remain visible as a type
253
+ * diagnostic and cannot be passed as a complete Layer.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const TestLive = Layer.override(AppLive, Layer.succeed(Database, fakeDb))
258
+ * ```
259
+ */
82
260
  static override(base, ...overrides) {
83
261
  const providers = /* @__PURE__ */ new Map();
84
- for (const provider of base.providers) providers.set(provider.service, provider);
85
- for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service, provider);
262
+ for (const provider of base.providers) providers.set(provider.service.serviceTag, provider);
263
+ for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service.serviceTag, provider);
86
264
  return new Layer([...providers.values()]);
87
265
  }
88
266
  };
89
267
  //#endregion
90
268
  //#region src/scope/errors.ts
269
+ /** Thrown when Scope context is accessed outside an active Scope execution. */
91
270
  var ScopeRuntimeNotConfiguredError = class extends Error {
92
271
  constructor() {
93
272
  super("No Scope is available in the current execution context");
94
273
  this.name = "ScopeRuntimeNotConfiguredError";
95
274
  }
96
275
  };
276
+ /** Thrown when a resource or finalizer is added after Scope closure begins. */
97
277
  var ScopeClosedError = class extends Error {
98
278
  constructor() {
99
279
  super("Cannot add resources or finalizers to a closed Scope");
100
280
  this.name = "ScopeClosedError";
101
281
  }
102
282
  };
283
+ /** Aggregates finalizer failures encountered while closing a Scope. */
103
284
  var ScopeCloseError = class extends Error {
104
285
  causes;
105
286
  constructor(causes) {
@@ -108,6 +289,7 @@ var ScopeCloseError = class extends Error {
108
289
  this.name = "ScopeCloseError";
109
290
  }
110
291
  };
292
+ /** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
111
293
  var ResourceNotDisposableError = class extends Error {
112
294
  constructor() {
113
295
  super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
@@ -117,6 +299,7 @@ var ResourceNotDisposableError = class extends Error {
117
299
  //#endregion
118
300
  //#region src/scope/disposable.ts
119
301
  const SCOPE_SUCCESS$2 = { status: "success" };
302
+ /** Return a Scope finalizer for a value's async or sync disposal protocol. */
120
303
  const getDisposeFinalizer = (resource) => {
121
304
  const candidate = Object(resource);
122
305
  const asyncDispose = candidate[Symbol.asyncDispose];
@@ -124,16 +307,20 @@ const getDisposeFinalizer = (resource) => {
124
307
  const dispose = candidate[Symbol.dispose];
125
308
  if (typeof dispose === "function") return () => dispose.call(resource);
126
309
  };
310
+ /** Dispose a value immediately when it implements a disposal protocol. */
127
311
  const disposeResource = (resource) => {
128
312
  return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
129
313
  };
130
314
  //#endregion
131
315
  //#region src/scope/runtime.ts
132
316
  const storage = new AsyncLocalStorage();
317
+ /** Bridges the current Scope through async execution context. */
133
318
  var ScopeRuntime = class {
319
+ /** Supply a Scope while invoking a callback. */
134
320
  static run(scope, program) {
135
321
  return storage.run(scope, program);
136
322
  }
323
+ /** Return the Scope active in the current execution context. */
137
324
  static current() {
138
325
  const scope = storage.getStore();
139
326
  if (!scope) throw new ScopeRuntimeNotConfiguredError();
@@ -279,24 +466,219 @@ var ScopeImpl = class ScopeImpl {
279
466
  }
280
467
  };
281
468
  const Scope = {
469
+ /** Create an owned, initially open Scope. */
282
470
  make() {
283
471
  return new ScopeImpl();
284
472
  },
473
+ /** Return the non-owning Scope available in the current execution context. */
285
474
  current() {
286
475
  return ScopeRuntime.current();
287
476
  },
477
+ /** Run a callback with an existing Scope supplied as the current context. */
288
478
  provide(scope, program) {
289
479
  return ScopeRuntime.run(scope, program);
290
480
  },
481
+ /** Resolve the current Scope through `yield* Scope` inside an Effect. */
291
482
  *[Symbol.iterator]() {
292
483
  return ScopeRuntime.current();
293
484
  },
485
+ /**
486
+ * Run a program in a newly owned Scope.
487
+ *
488
+ * Scope is independent from `better-result`, so returned values—including
489
+ * `Result.err`—close this Scope with a successful outcome. Result-aware
490
+ * outcome classification belongs to `Runtime.run`.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * await Scope.run(async (scope) => {
495
+ * const connection = await scope.acquire(connect, (connection) => connection.close())
496
+ * return connection.query()
497
+ * })
498
+ * ```
499
+ */
294
500
  run(program) {
295
501
  const scope = new ScopeImpl();
296
502
  return runScoped(scope, () => program(scope), { classify: () => SCOPE_SUCCESS$1 });
297
503
  }
298
504
  };
299
505
  //#endregion
506
+ //#region src/effect/combinators.ts
507
+ const isPromiseLike = (value) => {
508
+ if (typeof value !== "object" && typeof value !== "function" || value === null) return false;
509
+ return "then" in value && typeof value.then === "function";
510
+ };
511
+ const mapResult = (result, fn) => Result.map(result, fn);
512
+ const mapErrorResult = (result, fn) => Result.mapError(result, fn);
513
+ const andThenResult = (result, next) => Result.andThen(result, next);
514
+ const andThenAsyncResult = (result, next) => Result.andThenAsync(result, (value) => Promise.resolve(next(value)));
515
+ function map(first, second) {
516
+ if (typeof first === "function" && second === void 0) return (effect) => map(effect, first);
517
+ const fn = second;
518
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapResult(result, fn));
519
+ return mapResult(first, fn);
520
+ }
521
+ function mapError(first, second) {
522
+ if (typeof first === "function" && second === void 0) return (effect) => mapError(effect, first);
523
+ const fn = second;
524
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapErrorResult(result, fn));
525
+ return mapErrorResult(first, fn);
526
+ }
527
+ function andThen(first, second) {
528
+ if (typeof first === "function" && second === void 0) return (effect) => andThen(effect, first);
529
+ return andThenResult(first, second);
530
+ }
531
+ function andThenAsync(first, second) {
532
+ if (typeof first === "function" && second === void 0) return (effect) => andThenAsync(effect, first);
533
+ const next = second;
534
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => andThenAsyncResult(result, next));
535
+ return andThenAsyncResult(first, next);
536
+ }
537
+ //#endregion
538
+ //#region src/effect/effect.ts
539
+ function gen(body) {
540
+ return Result.gen(body);
541
+ }
542
+ /**
543
+ * Acquire a resource in the current Scope and register its release callback.
544
+ *
545
+ * Acquisition failures are represented in the Effect Result error channel;
546
+ * release failures remain owned by Scope cleanup. The release callback
547
+ * receives the final outcome chosen by the enclosing execution boundary.
548
+ *
549
+ * @example
550
+ * ```ts
551
+ * const connection = yield* Effect.acquireRelease(
552
+ * () => pool.connect(),
553
+ * (connection, outcome) => connection.close(outcome)
554
+ * )
555
+ * ```
556
+ */
557
+ function acquireRelease(acquire, release) {
558
+ const scope = Scope.current();
559
+ return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
560
+ }
561
+ /**
562
+ * Register an already-acquired disposable resource in the current Scope.
563
+ *
564
+ * The resource is not acquired by this helper. Registration failures are
565
+ * represented in the Effect Result error channel; disposal failures remain
566
+ * owned by Scope cleanup.
567
+ *
568
+ * @example
569
+ * ```ts
570
+ * const file = yield* Effect.add(await openFile('notes.txt'))
571
+ * ```
572
+ */
573
+ function add(resource) {
574
+ const scope = Scope.current();
575
+ return Result.await(Result.tryPromise(() => scope.add(resource)));
576
+ }
577
+ /**
578
+ * Effect namespace containing generator, resource, and Result combinators.
579
+ *
580
+ * Prefer these helpers when a program needs typed Service requirements or
581
+ * Scope-aware acquisition and cleanup.
582
+ */
583
+ const Effect = {
584
+ /** Compose a generator-based Effect program. */
585
+ gen,
586
+ /** Acquire and register a resource in the current Scope. */
587
+ acquireRelease,
588
+ /** Register an already-acquired disposable in the current Scope. */
589
+ add,
590
+ /** Map a successful Effect result. */
591
+ map,
592
+ /** Map an Effect error. */
593
+ mapError,
594
+ /** Chain a synchronous Effect result. */
595
+ andThen,
596
+ /** Chain an asynchronous Effect result. */
597
+ andThenAsync
598
+ };
599
+ //#endregion
600
+ //#region src/function/pipe.ts
601
+ function pipe(value, ...operations) {
602
+ return operations.reduce((current, operation) => operation(current), value);
603
+ }
604
+ //#endregion
605
+ //#region src/resource/errors.ts
606
+ /** Describes a failure encountered while releasing a Resource. */
607
+ var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
608
+ //#endregion
609
+ //#region src/resource/internal.ts
610
+ const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
611
+ resource,
612
+ cause,
613
+ message: `Failed to release resource: ${resource}`
614
+ });
615
+ const runResult = async (operation) => {
616
+ return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
617
+ };
618
+ const normalizeReleaseOutcome = (name, outcome) => {
619
+ if (outcome === void 0) return Result.ok();
620
+ return outcome.mapError((cause) => toReleaseFailure(name, cause));
621
+ };
622
+ const runRelease = async (name, resource, release) => {
623
+ return (await Result.tryPromise({
624
+ try: () => Promise.resolve(release(resource)),
625
+ catch: (cause) => toReleaseFailure(name, cause)
626
+ })).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
627
+ };
628
+ const notifyReleaseFailure = async (observer, failure) => {
629
+ if (!observer) return;
630
+ try {
631
+ await observer(failure);
632
+ } catch {}
633
+ };
634
+ const combineUseAndRelease = async (used, released, onReleaseFailure) => {
635
+ if (Result.isError(used)) {
636
+ if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
637
+ return Result.err(used.error);
638
+ }
639
+ if (Result.isError(released)) {
640
+ await notifyReleaseFailure(onReleaseFailure, released.error);
641
+ return Result.err(released.error);
642
+ }
643
+ return Result.ok(used.value);
644
+ };
645
+ //#endregion
646
+ //#region src/resource/resource.ts
647
+ /**
648
+ * Acquire a resource, use it, and always attempt release afterward.
649
+ *
650
+ * Acquisition, use, and release may be synchronous or asynchronous Result
651
+ * operations. If both use and release fail, the use error remains primary and
652
+ * `onReleaseFailure` receives the cleanup failure as a diagnostic.
653
+ *
654
+ * When `release` is omitted, `Symbol.asyncDispose` is preferred over
655
+ * `Symbol.dispose`.
656
+ *
657
+ * @example
658
+ * ```ts
659
+ * const result = await Resource.acquireUseRelease({
660
+ * name: 'database connection',
661
+ * acquire: () => connect(),
662
+ * use: (connection) => query(connection),
663
+ * release: (connection) => connection.close()
664
+ * })
665
+ * ```
666
+ */
667
+ const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
668
+ const resource = yield* Result.await(runResult(acquire));
669
+ const scope = Scope.make();
670
+ let released = Result.ok();
671
+ scope.addFinalizer(async () => {
672
+ released = await runRelease(name, resource, release);
673
+ });
674
+ const used = await runResult(() => use(resource));
675
+ await scope.close();
676
+ return await combineUseAndRelease(used, released, onReleaseFailure);
677
+ });
678
+ const Resource = {
679
+ /** Acquire, use, and release a resource with deterministic error precedence. */
680
+ acquireUseRelease };
681
+ //#endregion
300
682
  //#region src/runtime/outcome.ts
301
683
  const isResultLike = (value) => typeof value === "object" && value !== null && "status" in value && (value.status === "ok" || value.status === "error");
302
684
  const classifyRuntimeOutcome = (value) => {
@@ -309,6 +691,12 @@ const classifyRuntimeOutcome = (value) => {
309
691
  //#endregion
310
692
  //#region src/layer/runtime.ts
311
693
  const SCOPE_SUCCESS = Object.freeze({ status: "success" });
694
+ var RuntimeHandleDisposedError = class extends Error {
695
+ constructor() {
696
+ super("Cannot run a program using a disposed Layer");
697
+ this.name = "RuntimeHandleDisposedError";
698
+ }
699
+ };
312
700
  const normalizeDisposeCauses = (cause) => {
313
701
  if (cause instanceof AggregateError) return [...cause.errors];
314
702
  return [cause];
@@ -326,7 +714,7 @@ const bindProviderToScope = (provider, rootScope) => ({
326
714
  return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
327
715
  })
328
716
  });
329
- var BuiltLayerImpl = class {
717
+ var RuntimeHandleImpl = class {
330
718
  backend;
331
719
  rootScope;
332
720
  onCleanupFailure;
@@ -402,10 +790,11 @@ var BuiltLayerImpl = class {
402
790
  }
403
791
  }
404
792
  assertActive() {
405
- if (this.state !== "active") throw new BuiltLayerDisposedError();
793
+ if (this.state !== "active") throw new RuntimeHandleDisposedError();
406
794
  }
407
795
  };
408
- const buildLayer = async (layer, backend, options = {}) => {
796
+ /** Build a Runtime handle for a complete Layer and register its providers. */
797
+ const createRuntimeHandle = async (layer, backend, options = {}) => {
409
798
  const rootScope = Scope.make();
410
799
  let current;
411
800
  try {
@@ -441,103 +830,50 @@ const buildLayer = async (layer, backend, options = {}) => {
441
830
  else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
442
831
  throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
443
832
  }
444
- return new BuiltLayerImpl(backend, rootScope, options.onCleanupFailure);
833
+ return new RuntimeHandleImpl(backend, rootScope, options.onCleanupFailure);
445
834
  };
446
835
  //#endregion
447
- //#region src/effect/effect.ts
448
- function gen(body) {
449
- return Result.gen(body);
450
- }
836
+ //#region src/runtime/runtime.ts
451
837
  /**
452
- * Acquire a resource in the current Scope and register its release callback.
838
+ * Long-lived execution environment backed by a complete Layer.
453
839
  *
454
- * Acquisition failures are represented in the Effect Result error channel;
455
- * release failures remain owned by Scope cleanup.
456
- */
457
- function acquireRelease(acquire, release) {
458
- const scope = Scope.current();
459
- return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
460
- }
461
- /**
462
- * Register an already-acquired disposable resource in the current Scope.
840
+ * A Runtime owns Layer resources until `dispose()` is called. Each `run()` is
841
+ * isolated in a child Scope, while Layer-scoped resources remain shared.
463
842
  *
464
- * Registration failures are represented in the Effect Result error channel;
465
- * disposal failures remain owned by Scope cleanup.
843
+ * @example
844
+ * ```ts
845
+ * const runtime = await Runtime.make(AppLive, new MemoryLayerBackend())
846
+ * const result = await runtime.run(loadUser('u1'))
847
+ * await runtime.dispose()
848
+ * ```
849
+ *
850
+ * @typeParam Provided The Service constructors supplied by the Layer.
466
851
  */
467
- function add(resource) {
468
- const scope = Scope.current();
469
- return Result.await(Result.tryPromise(() => scope.add(resource)));
470
- }
471
- const Effect = {
472
- gen,
473
- acquireRelease,
474
- add
475
- };
476
- //#endregion
477
- //#region src/resource/errors.ts
478
- var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
479
- //#endregion
480
- //#region src/resource/internal.ts
481
- const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
482
- resource,
483
- cause,
484
- message: `Failed to release resource: ${resource}`
485
- });
486
- const runResult = async (operation) => {
487
- return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
488
- };
489
- const normalizeReleaseOutcome = (name, outcome) => {
490
- if (outcome === void 0) return Result.ok();
491
- return outcome.mapError((cause) => toReleaseFailure(name, cause));
492
- };
493
- const runRelease = async (name, resource, release) => {
494
- return (await Result.tryPromise({
495
- try: () => Promise.resolve(release(resource)),
496
- catch: (cause) => toReleaseFailure(name, cause)
497
- })).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
498
- };
499
- const notifyReleaseFailure = async (observer, failure) => {
500
- if (!observer) return;
501
- try {
502
- await observer(failure);
503
- } catch {}
504
- };
505
- const combineUseAndRelease = async (used, released, onReleaseFailure) => {
506
- if (Result.isError(used)) {
507
- if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
508
- return Result.err(used.error);
509
- }
510
- if (Result.isError(released)) {
511
- await notifyReleaseFailure(onReleaseFailure, released.error);
512
- return Result.err(released.error);
513
- }
514
- return Result.ok(used.value);
515
- };
516
- //#endregion
517
- //#region src/resource/resource.ts
518
- const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
519
- const resource = yield* Result.await(runResult(acquire));
520
- const scope = Scope.make();
521
- let released = Result.ok();
522
- scope.addFinalizer(async () => {
523
- released = await runRelease(name, resource, release);
524
- });
525
- const used = await runResult(() => use(resource));
526
- await scope.close();
527
- return await combineUseAndRelease(used, released, onReleaseFailure);
528
- });
529
- const Resource = { acquireUseRelease };
530
- //#endregion
531
- //#region src/runtime/runtime.ts
532
852
  var Runtime = class Runtime {
533
- built;
534
- constructor(built) {
535
- this.built = built;
536
- }
853
+ handle;
854
+ constructor(handle) {
855
+ this.handle = handle;
856
+ }
857
+ /**
858
+ * Create a long-lived Runtime that owns its Layer resources.
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * const runtime = await Runtime.make(AppLive, backend)
863
+ * const result = await runtime.run(program)
864
+ * await runtime.dispose()
865
+ * ```
866
+ */
537
867
  static async make(layer, backend, options = {}) {
538
- const built = await buildLayer(layer, backend, options);
539
- return new Runtime(built);
540
- }
868
+ const handle = await createRuntimeHandle(layer, backend, options);
869
+ return new Runtime(handle);
870
+ }
871
+ /**
872
+ * Run one program and dispose its Layer resources before resolving.
873
+ *
874
+ * This is convenient for request-style or command-style execution where a
875
+ * Runtime should not outlive the operation.
876
+ */
541
877
  static async run(layer, backend, program, options = {}) {
542
878
  const runtime = await Runtime.make(layer, backend, options);
543
879
  let value;
@@ -574,20 +910,22 @@ var Runtime = class Runtime {
574
910
  if (executionFailed) throw executionFailure;
575
911
  return value;
576
912
  }
913
+ /** Run one execution in this Runtime's child Scope. */
577
914
  run(program) {
578
- return this.built.run(program);
915
+ return this.handle.run(program);
579
916
  }
580
917
  runUnchecked(program) {
581
- return this.built.run(program);
918
+ return this.handle.run(program);
582
919
  }
920
+ /** Stop new executions and release the Runtime's Layer resources. */
583
921
  dispose() {
584
- return this.built.dispose();
922
+ return this.handle.dispose();
585
923
  }
586
924
  disposeWithOutcome(outcome) {
587
- return this.built.dispose(outcome);
925
+ return this.handle.dispose(outcome);
588
926
  }
589
927
  };
590
928
  //#endregion
591
- export { BuiltLayerDisposedError, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, buildLayer };
929
+ export { DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
592
930
 
593
931
  //# sourceMappingURL=index.mjs.map