better-effect 0.4.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-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
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,25 +109,67 @@ 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
  }
56
157
  /**
57
- * Define a dependency-free provider with Runtime-root cleanup.
158
+ * Define a provider with Runtime-root cleanup.
58
159
  *
59
160
  * The release callback intentionally keeps its compatibility-friendly
60
- * one-argument shape. Use `scopedGen` when cleanup needs `ScopeOutcome`.
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
+ * ```
61
173
  */
62
174
  static scoped(service, acquire, release) {
63
175
  return new Layer([{
@@ -66,7 +178,24 @@ var Layer = class Layer {
66
178
  release: (instance) => release(instance)
67
179
  }]);
68
180
  }
69
- /** Define a contextual provider with Runtime-root, outcome-aware cleanup. */
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
+ */
70
199
  static scopedGen(service, factory, release) {
71
200
  return new Layer([{
72
201
  service,
@@ -74,39 +203,84 @@ var Layer = class Layer {
74
203
  release: (instance, outcome) => release(instance, outcome)
75
204
  }]);
76
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
+ */
77
220
  static gen(service, factory) {
78
221
  return Layer.make(service, () => runLayerGenerator(service, factory));
79
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
+ */
80
234
  static merge(...layers) {
81
235
  const providers = /* @__PURE__ */ new Map();
82
236
  for (const layer of layers) for (const provider of layer.providers) {
83
237
  const service = provider.service;
84
- if (providers.has(service)) throw new DuplicateServiceError(service);
85
- 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);
86
244
  }
87
245
  return new Layer([...providers.values()]);
88
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
+ */
89
260
  static override(base, ...overrides) {
90
261
  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);
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);
93
264
  return new Layer([...providers.values()]);
94
265
  }
95
266
  };
96
267
  //#endregion
97
268
  //#region src/scope/errors.ts
269
+ /** Thrown when Scope context is accessed outside an active Scope execution. */
98
270
  var ScopeRuntimeNotConfiguredError = class extends Error {
99
271
  constructor() {
100
272
  super("No Scope is available in the current execution context");
101
273
  this.name = "ScopeRuntimeNotConfiguredError";
102
274
  }
103
275
  };
276
+ /** Thrown when a resource or finalizer is added after Scope closure begins. */
104
277
  var ScopeClosedError = class extends Error {
105
278
  constructor() {
106
279
  super("Cannot add resources or finalizers to a closed Scope");
107
280
  this.name = "ScopeClosedError";
108
281
  }
109
282
  };
283
+ /** Aggregates finalizer failures encountered while closing a Scope. */
110
284
  var ScopeCloseError = class extends Error {
111
285
  causes;
112
286
  constructor(causes) {
@@ -115,6 +289,7 @@ var ScopeCloseError = class extends Error {
115
289
  this.name = "ScopeCloseError";
116
290
  }
117
291
  };
292
+ /** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
118
293
  var ResourceNotDisposableError = class extends Error {
119
294
  constructor() {
120
295
  super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
@@ -124,6 +299,7 @@ var ResourceNotDisposableError = class extends Error {
124
299
  //#endregion
125
300
  //#region src/scope/disposable.ts
126
301
  const SCOPE_SUCCESS$2 = { status: "success" };
302
+ /** Return a Scope finalizer for a value's async or sync disposal protocol. */
127
303
  const getDisposeFinalizer = (resource) => {
128
304
  const candidate = Object(resource);
129
305
  const asyncDispose = candidate[Symbol.asyncDispose];
@@ -131,16 +307,20 @@ const getDisposeFinalizer = (resource) => {
131
307
  const dispose = candidate[Symbol.dispose];
132
308
  if (typeof dispose === "function") return () => dispose.call(resource);
133
309
  };
310
+ /** Dispose a value immediately when it implements a disposal protocol. */
134
311
  const disposeResource = (resource) => {
135
312
  return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
136
313
  };
137
314
  //#endregion
138
315
  //#region src/scope/runtime.ts
139
316
  const storage = new AsyncLocalStorage();
317
+ /** Bridges the current Scope through async execution context. */
140
318
  var ScopeRuntime = class {
319
+ /** Supply a Scope while invoking a callback. */
141
320
  static run(scope, program) {
142
321
  return storage.run(scope, program);
143
322
  }
323
+ /** Return the Scope active in the current execution context. */
144
324
  static current() {
145
325
  const scope = storage.getStore();
146
326
  if (!scope) throw new ScopeRuntimeNotConfiguredError();
@@ -286,15 +466,19 @@ var ScopeImpl = class ScopeImpl {
286
466
  }
287
467
  };
288
468
  const Scope = {
469
+ /** Create an owned, initially open Scope. */
289
470
  make() {
290
471
  return new ScopeImpl();
291
472
  },
473
+ /** Return the non-owning Scope available in the current execution context. */
292
474
  current() {
293
475
  return ScopeRuntime.current();
294
476
  },
477
+ /** Run a callback with an existing Scope supplied as the current context. */
295
478
  provide(scope, program) {
296
479
  return ScopeRuntime.run(scope, program);
297
480
  },
481
+ /** Resolve the current Scope through `yield* Scope` inside an Effect. */
298
482
  *[Symbol.iterator]() {
299
483
  return ScopeRuntime.current();
300
484
  },
@@ -304,6 +488,14 @@ const Scope = {
304
488
  * Scope is independent from `better-result`, so returned values—including
305
489
  * `Result.err`—close this Scope with a successful outcome. Result-aware
306
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
+ * ```
307
499
  */
308
500
  run(program) {
309
501
  const scope = new ScopeImpl();
@@ -311,6 +503,182 @@ const Scope = {
311
503
  }
312
504
  };
313
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
314
682
  //#region src/runtime/outcome.ts
315
683
  const isResultLike = (value) => typeof value === "object" && value !== null && "status" in value && (value.status === "ok" || value.status === "error");
316
684
  const classifyRuntimeOutcome = (value) => {
@@ -323,6 +691,12 @@ const classifyRuntimeOutcome = (value) => {
323
691
  //#endregion
324
692
  //#region src/layer/runtime.ts
325
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
+ };
326
700
  const normalizeDisposeCauses = (cause) => {
327
701
  if (cause instanceof AggregateError) return [...cause.errors];
328
702
  return [cause];
@@ -340,7 +714,7 @@ const bindProviderToScope = (provider, rootScope) => ({
340
714
  return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
341
715
  })
342
716
  });
343
- var BuiltLayerImpl = class {
717
+ var RuntimeHandleImpl = class {
344
718
  backend;
345
719
  rootScope;
346
720
  onCleanupFailure;
@@ -416,15 +790,11 @@ var BuiltLayerImpl = class {
416
790
  }
417
791
  }
418
792
  assertActive() {
419
- if (this.state !== "active") throw new BuiltLayerDisposedError();
793
+ if (this.state !== "active") throw new RuntimeHandleDisposedError();
420
794
  }
421
795
  };
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 = {}) => {
796
+ /** Build a Runtime handle for a complete Layer and register its providers. */
797
+ const createRuntimeHandle = async (layer, backend, options = {}) => {
428
798
  const rootScope = Scope.make();
429
799
  let current;
430
800
  try {
@@ -460,144 +830,50 @@ const buildLayer = async (layer, backend, options = {}) => {
460
830
  else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
461
831
  throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
462
832
  }
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";
833
+ return new RuntimeHandleImpl(backend, rootScope, options.onCleanupFailure);
470
834
  };
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));
477
- };
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
835
  //#endregion
497
- //#region src/effect/effect.ts
498
- function gen(body) {
499
- return Result.gen(body);
500
- }
836
+ //#region src/runtime/runtime.ts
501
837
  /**
502
- * Acquire a resource in the current Scope and register its release callback.
838
+ * Long-lived execution environment backed by a complete Layer.
503
839
  *
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.
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.
842
+ *
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
+ * ```
513
849
  *
514
- * Registration failures are represented in the Effect Result error channel;
515
- * disposal failures remain owned by Scope cleanup.
850
+ * @typeParam Provided The Service constructors supplied by the Layer.
516
851
  */
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
852
  var Runtime = class Runtime {
591
- built;
592
- constructor(built) {
593
- this.built = built;
853
+ handle;
854
+ constructor(handle) {
855
+ this.handle = handle;
594
856
  }
595
- /** Create a long-lived Runtime that owns its Layer resources. */
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
+ */
596
867
  static async make(layer, backend, options = {}) {
597
- const built = await buildLayer(layer, backend, options);
598
- return new Runtime(built);
868
+ const handle = await createRuntimeHandle(layer, backend, options);
869
+ return new Runtime(handle);
599
870
  }
600
- /** Run one program and dispose its Layer resources before resolving. */
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
+ */
601
877
  static async run(layer, backend, program, options = {}) {
602
878
  const runtime = await Runtime.make(layer, backend, options);
603
879
  let value;
@@ -636,20 +912,20 @@ var Runtime = class Runtime {
636
912
  }
637
913
  /** Run one execution in this Runtime's child Scope. */
638
914
  run(program) {
639
- return this.built.run(program);
915
+ return this.handle.run(program);
640
916
  }
641
917
  runUnchecked(program) {
642
- return this.built.run(program);
918
+ return this.handle.run(program);
643
919
  }
644
920
  /** Stop new executions and release the Runtime's Layer resources. */
645
921
  dispose() {
646
- return this.built.dispose();
922
+ return this.handle.dispose();
647
923
  }
648
924
  disposeWithOutcome(outcome) {
649
- return this.built.dispose(outcome);
925
+ return this.handle.dispose(outcome);
650
926
  }
651
927
  };
652
928
  //#endregion
653
- export { BuiltLayerDisposedError, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, pipe };
929
+ export { DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
654
930
 
655
931
  //# sourceMappingURL=index.mjs.map