better-effect 0.7.0 → 0.9.1

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,226 +1,9 @@
1
- import { a as LayerRegistrationError, c as ServiceAcquisitionError, i as LayerGeneratorYieldError, l as ServiceNotFoundError, n as DuplicateServiceError, o as ServiceTagCollisionError, r as LayerDisposeError, s as CircularDependencyError, u as ServiceRuntimeNotConfiguredError } from "./internal-identity-Cm4-KIUj.mjs";
2
- import { a as runRuntimeContext, c as RuntimeContextNotConfiguredError, i as makeRuntimeContext, n as currentRuntimeContext, o as setDefaultRuntimeContextStorage, r as getRuntimeContext, t as activeRuntimeContextStorage } from "./context-B4yO5LaH.mjs";
3
- import { nodeRuntimeContextStorage } from "./runtime/node.mjs";
4
- import { t as MapLayerBackend } from "./map-layer-backend-BodcEeNA.mjs";
1
+ import { a as ServiceTagCollisionError, c as ServiceNotFoundError, i as LayerRegistrationError, l as ServiceRuntimeNotConfiguredError, n as LayerDisposeError, o as CircularDependencyError, r as LayerGeneratorYieldError, s as ServiceAcquisitionError, t as DuplicateServiceError } from "./errors-Dnjhzbt0.mjs";
2
+ import { a as runRuntimeContext, c as RuntimeContextNotConfiguredError, i as makeRuntimeContext, n as currentRuntimeContext, r as getRuntimeContext, t as activeRuntimeContextStorage } from "./context-B4yO5LaH.mjs";
3
+ import { a as ServiceRuntime, i as Service, n as linkAbortSignals, o as defaultRuntimeContextStorage, r as Layer, t as CurrentAbortSignal } from "./signal-BgUtPQj5.mjs";
4
+ import { t as MapLayerBackend } from "./map-layer-backend-gal-mcRv.mjs";
5
5
  import { t as isPromiseLike } from "./runtime-CDcCF5cb.mjs";
6
6
  import { Result, TaggedError } from "better-result";
7
- //#region src/runtime/default.ts
8
- /** The Node/Bun storage used by the main Runtime entrypoint. */
9
- const defaultRuntimeContextStorage = nodeRuntimeContextStorage;
10
- setDefaultRuntimeContextStorage(defaultRuntimeContextStorage);
11
- //#endregion
12
- //#region src/service/runtime.ts
13
- /** Provides the resolver context used by Service tokens during execution. */
14
- var ServiceRuntime = class ServiceRuntime {
15
- /**
16
- * Run a callback with a resolver available to `yield* Service` expressions.
17
- *
18
- * The context is scoped to the callback and is restored afterward.
19
- *
20
- * @example
21
- * ```ts
22
- * const value = ServiceRuntime.run(resolver, () => {
23
- * return ServiceRuntime.resolve(Database)
24
- * })
25
- * ```
26
- */
27
- static run(resolver, program, storage = defaultRuntimeContextStorage) {
28
- const current = getRuntimeContext(storage);
29
- const context = makeRuntimeContext(resolver, current?.scope, current?.resolver === resolver ? current.resolutionPath : [], current?.signal);
30
- return runRuntimeContext(storage, context, program);
31
- }
32
- /** Return the resolver active in the current execution context. */
33
- static current() {
34
- let context;
35
- try {
36
- context = currentRuntimeContext();
37
- } catch {
38
- throw new ServiceRuntimeNotConfiguredError();
39
- }
40
- if (!context.resolver) throw new ServiceRuntimeNotConfiguredError();
41
- return context.resolver;
42
- }
43
- /** Resolve a Service token using the active resolver. */
44
- static async resolve(token) {
45
- return await ServiceRuntime.current().resolve(token);
46
- }
47
- };
48
- //#endregion
49
- //#region src/service/service.ts
50
- /**
51
- * Declare a class-backed Service with a stable string-literal identity.
52
- *
53
- * The returned class is simultaneously the implementation type, the runtime
54
- * dependency token, and the value yielded by `yield*` in an Effect generator.
55
- * The explicit self type preserves exact instance inference, while the second
56
- * call captures the tag as a literal for Layer composition and diagnostics.
57
- *
58
- * @example
59
- * ```ts
60
- * class Database extends Service<Database>()('Database') {
61
- * query(): string {
62
- * return 'ok'
63
- * }
64
- * }
65
- *
66
- * const database = yield* Database
67
- * database.query()
68
- * ```
69
- *
70
- * @typeParam Self The instance type implemented by the declared Service.
71
- */
72
- function Service() {
73
- return function(tag) {
74
- if (tag.length === 0) throw new TypeError("Service tags must not be empty");
75
- class BaseService {
76
- /** The stable logical identity used by Layers and resolver backends. */
77
- static serviceTag = tag;
78
- /**
79
- * Type-check a structural implementation of this Service.
80
- *
81
- * This is an identity helper. It returns the supplied value unchanged
82
- * and does not invoke a constructor or modify its prototype.
83
- *
84
- * @example
85
- * ```ts
86
- * class Database extends Service<Database>()('Database') {
87
- * query(sql: string): string {
88
- * return sql
89
- * }
90
- * }
91
- *
92
- * const database = Database.of({
93
- * query: (sql) => `Result: ${sql}`
94
- * })
95
- *
96
- * database.query('SELECT 1')
97
- * // 'Result: SELECT 1'
98
- * // database is the original object, not an instance of Database
99
- * ```
100
- */
101
- static of(implementation) {
102
- return implementation;
103
- }
104
- /** Resolve this Service from the resolver active in the current runtime. */
105
- static async *[Symbol.asyncIterator]() {
106
- return await ServiceRuntime.resolve(this);
107
- }
108
- }
109
- return BaseService;
110
- };
111
- }
112
- //#endregion
113
- //#region src/layer/internal.ts
114
- const runLayerGenerator = async (service, factory) => {
115
- const iterator = factory();
116
- const state = await iterator.next();
117
- if (!state.done) try {
118
- await iterator.return(void 0);
119
- } finally {
120
- throw new LayerGeneratorYieldError(service);
121
- }
122
- return state.value;
123
- };
124
- //#endregion
125
- //#region src/layer/layer.ts
126
- /**
127
- * Declarative collection of Service providers.
128
- *
129
- * A Layer describes how to acquire implementations; it does not execute
130
- * providers until a `Runtime` is created. Use `merge` to compose distinct
131
- * providers and `override` when replacing an existing provider intentionally.
132
- *
133
- * @example
134
- * ```ts
135
- * const AppLive = Layer.merge(
136
- * Layer.succeed(Database, database),
137
- * Layer.make(UserRepository)
138
- * )
139
- *
140
- * const runtime = await Runtime.make(AppLive, backend)
141
- * ```
142
- */
143
- var Layer = class Layer {
144
- /** The provider registrations retained by this Layer. */
145
- providers;
146
- constructor(providers) {
147
- this.providers = Object.freeze([...providers]);
148
- }
149
- static make(service, acquire) {
150
- const defaultAcquire = () => {
151
- return new service();
152
- };
153
- const normalizedAcquire = normalizeAcquire(acquire ?? defaultAcquire);
154
- return new Layer([{
155
- service,
156
- acquire: normalizedAcquire
157
- }]);
158
- }
159
- /** Create a Layer from an already-constructed Service instance. */
160
- static succeed(service, instance) {
161
- const normalizedAcquire = normalizeAcquire(() => instance);
162
- return new Layer([{
163
- service,
164
- acquire: normalizedAcquire
165
- }]);
166
- }
167
- /** Define a provider with Runtime-root cleanup. */
168
- static scoped(service, acquire, release) {
169
- return new Layer([{
170
- service,
171
- acquire: normalizeAcquire(acquire),
172
- release: (instance, outcome) => {
173
- return release(instance, outcome);
174
- }
175
- }]);
176
- }
177
- /** Define a provider whose acquisition can yield contextual Services. */
178
- static scopedGen(service, factory, release) {
179
- return new Layer([{
180
- service,
181
- acquire: () => runLayerGenerator(service, factory),
182
- release: (instance, outcome) => {
183
- return release(instance, outcome);
184
- }
185
- }]);
186
- }
187
- /** Define a provider whose acquisition can yield contextual Services. */
188
- static gen(service, factory) {
189
- return new Layer([{
190
- service,
191
- acquire: () => runLayerGenerator(service, factory)
192
- }]);
193
- }
194
- /** Compose Layers without replacing providers. */
195
- static merge(...layers) {
196
- const providers = /* @__PURE__ */ new Map();
197
- for (const layer of layers) for (const provider of layer.providers) {
198
- const service = provider.service;
199
- const existing = providers.get(service.serviceTag);
200
- if (existing) {
201
- if (existing.service !== service) throw new ServiceTagCollisionError(existing.service, service);
202
- throw new DuplicateServiceError(service);
203
- }
204
- providers.set(service.serviceTag, provider);
205
- }
206
- return new Layer([...providers.values()]);
207
- }
208
- /** Mark a Layer composition root as complete without changing its runtime value. */
209
- static complete(layer) {
210
- return layer;
211
- }
212
- /** Replace providers in a base Layer, using tag identity and compatible contracts. */
213
- static override(base, ...overrides) {
214
- const providers = /* @__PURE__ */ new Map();
215
- for (const provider of base.providers) providers.set(provider.service.serviceTag, provider);
216
- for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service.serviceTag, provider);
217
- return new Layer([...providers.values()]);
218
- }
219
- };
220
- const normalizeAcquire = (acquire) => () => {
221
- return acquire();
222
- };
223
- //#endregion
224
7
  //#region src/scope/errors.ts
225
8
  /** Thrown when Scope context is accessed outside an active Scope execution. */
226
9
  var ScopeRuntimeNotConfiguredError = class extends Error {
@@ -473,6 +256,9 @@ const Scope = {
473
256
  };
474
257
  //#endregion
475
258
  //#region src/effect/combinators.ts
259
+ const asResult = (value) => {
260
+ return value;
261
+ };
476
262
  const mapResult = (result, fn) => {
477
263
  return Result.map(result, fn);
478
264
  };
@@ -537,6 +323,83 @@ function andThenAsync(first, second) {
537
323
  });
538
324
  return andThenAsyncResult(first, next);
539
325
  }
326
+ const tapResult = (result, fn) => Result.tap(result, fn);
327
+ const tapErrorResult = (result, fn) => Result.tapError(result, fn);
328
+ const tapBothResult = (result, handlers) => Result.tapBoth(result, handlers);
329
+ const recoverResult = (result, fn) => Result.tryRecover(result, fn);
330
+ const recoverAsyncResult = (result, fn) => Result.tryRecoverAsync(result, (error) => Promise.resolve(fn(error)));
331
+ const flattenResult = (result) => Result.flatten(result);
332
+ const matchResult = (result, handlers) => Result.match(result, handlers);
333
+ const allResult = (results) => Result.all(results);
334
+ function tap(first, second) {
335
+ if (first instanceof Function && second === void 0) {
336
+ const callback = first;
337
+ return (effect) => tap(effect, callback);
338
+ }
339
+ if (second === void 0) throw new TypeError("Effect.tap requires a callback");
340
+ const fn = second;
341
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => tapResult(asResult(result), fn));
342
+ return tapResult(asResult(first), fn);
343
+ }
344
+ function tapError(first, second) {
345
+ if (first instanceof Function && second === void 0) {
346
+ const callback = first;
347
+ return (effect) => tapError(effect, callback);
348
+ }
349
+ if (second === void 0) throw new TypeError("Effect.tapError requires a callback");
350
+ const fn = second;
351
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => tapErrorResult(asResult(result), fn));
352
+ return tapErrorResult(asResult(first), fn);
353
+ }
354
+ function tapBoth(first, second) {
355
+ if (second === void 0) return (effect) => tapBoth(effect, first);
356
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => tapBothResult(result, second));
357
+ return tapBothResult(asResult(first), second);
358
+ }
359
+ function recover(first, second) {
360
+ if (first instanceof Function && second === void 0) {
361
+ const callback = first;
362
+ return (effect) => recover(effect, callback);
363
+ }
364
+ if (second === void 0) throw new TypeError("Effect.recover requires a callback");
365
+ const fn = second;
366
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => recoverResult(asResult(result), fn));
367
+ return recoverResult(asResult(first), fn);
368
+ }
369
+ function recoverAsync(first, second) {
370
+ if (first instanceof Function && second === void 0) {
371
+ const callback = first;
372
+ return (effect) => recoverAsync(effect, callback);
373
+ }
374
+ if (second === void 0) throw new TypeError("Effect.recoverAsync requires a callback");
375
+ const fn = second;
376
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => recoverAsyncResult(asResult(result), fn));
377
+ return recoverAsyncResult(asResult(first), fn);
378
+ }
379
+ /** Remove one nested Result/Effect layer. */
380
+ function flatten(effect) {
381
+ return flattenResult(asResult(effect));
382
+ }
383
+ function as(first, second) {
384
+ if (arguments.length < 2) return (effect) => as(effect, first);
385
+ return mapResult(asResult(first), () => second);
386
+ }
387
+ /** Replace a successful value with void. */
388
+ function asVoid(effect) {
389
+ return mapResult(asResult(effect), () => void 0);
390
+ }
391
+ function match(first, second) {
392
+ if (isPromiseLike(first)) return Promise.resolve(first).then((result) => match(asResult(result), second));
393
+ return matchResult(asResult(first), second);
394
+ }
395
+ /** Collect already-created Effects in input order. */
396
+ function all(results) {
397
+ return allResult(results);
398
+ }
399
+ /** Combine two already-created Effects in input order. */
400
+ function zip(left, right) {
401
+ return Result.all([left, right]);
402
+ }
540
403
  //#endregion
541
404
  //#region src/effect/effect.ts
542
405
  const runResultGenerator = Result.gen;
@@ -547,6 +410,40 @@ function fn(body) {
547
410
  const program = () => runResultGenerator(body);
548
411
  return program;
549
412
  }
413
+ const validateProgramConcurrency = (concurrency) => {
414
+ if (concurrency !== void 0 && (!Number.isFinite(concurrency) || !Number.isInteger(concurrency) || concurrency <= 0)) throw new RangeError("Program.all concurrency must be a positive integer");
415
+ };
416
+ /** Build a lazy Program collection with optional bounded concurrency. */
417
+ function programAll(programs, options = {}) {
418
+ validateProgramConcurrency(options.concurrency);
419
+ const concurrency = options.concurrency;
420
+ const program = async () => {
421
+ const results = Array.from({ length: programs.length });
422
+ const failures = Array.from({ length: programs.length }, () => false);
423
+ const causes = Array.from({ length: programs.length });
424
+ let nextIndex = 0;
425
+ const worker = async () => {
426
+ while (true) {
427
+ const index = nextIndex++;
428
+ if (index >= programs.length) return;
429
+ try {
430
+ results[index] = await programs[index]();
431
+ } catch (cause) {
432
+ failures[index] = true;
433
+ causes[index] = cause;
434
+ }
435
+ }
436
+ };
437
+ const workers = Math.min(concurrency ?? programs.length, programs.length);
438
+ await Promise.all(Array.from({ length: workers }, () => worker()));
439
+ const failureIndex = failures.findIndex(Boolean);
440
+ if (failureIndex >= 0) throw causes[failureIndex];
441
+ return Result.all(results);
442
+ };
443
+ return program;
444
+ }
445
+ /** Value-level namespace for lazy Program combinators. */
446
+ const Program = { all: programAll };
550
447
  /**
551
448
  * Acquire a resource in the current Scope and register its release callback.
552
449
  *
@@ -582,12 +479,6 @@ function add(resource) {
582
479
  const scope = Scope.current();
583
480
  return Result.await(Result.tryPromise(() => scope.add(resource)));
584
481
  }
585
- /**
586
- * Effect namespace containing generator, resource, and Result combinators.
587
- *
588
- * Prefer these helpers when a program needs typed Service requirements or
589
- * Scope-aware acquisition and cleanup.
590
- */
591
482
  const Effect = {
592
483
  /** Compose a generator-based Effect program. */
593
484
  gen,
@@ -604,7 +495,29 @@ const Effect = {
604
495
  /** Chain a synchronous Effect result. */
605
496
  andThen,
606
497
  /** Chain an asynchronous Effect result. */
607
- andThenAsync
498
+ andThenAsync,
499
+ /** Observe successful values without changing the Result. */
500
+ tap,
501
+ /** Observe error values without changing the Result. */
502
+ tapError,
503
+ /** Observe the active Result branch without changing the Result. */
504
+ tapBoth,
505
+ /** Recover an error with another Effect. */
506
+ recover,
507
+ /** Recover an error asynchronously with another Effect. */
508
+ recoverAsync,
509
+ /** Remove one nested Effect layer. */
510
+ flatten,
511
+ /** Replace a successful value. */
512
+ as,
513
+ /** Replace a successful value with void. */
514
+ asVoid,
515
+ /** Match either Result branch. */
516
+ match,
517
+ /** Collect Effects in input order. */
518
+ all,
519
+ /** Zip two Effects in input order. */
520
+ zip
608
521
  };
609
522
  //#endregion
610
523
  //#region src/function/pipe.ts
@@ -702,24 +615,62 @@ const classifyRuntimeOutcome = (value) => {
702
615
  return { status: "success" };
703
616
  };
704
617
  //#endregion
618
+ //#region src/runtime/observer.ts
619
+ const notifyRuntimeObservers = (observers, select, event) => {
620
+ for (const observer of observers) {
621
+ const callback = select(observer);
622
+ if (!callback) continue;
623
+ try {
624
+ Promise.resolve(callback(event)).catch(() => {});
625
+ } catch {}
626
+ }
627
+ };
628
+ //#endregion
705
629
  //#region src/layer/resolution.ts
706
630
  const findCycleStart = (path, token) => path.findIndex((current) => current.serviceTag === token.serviceTag);
707
631
  const shouldPreserve = (cause) => cause instanceof CircularDependencyError || cause instanceof ServiceAcquisitionError || cause instanceof ServiceNotFoundError || cause instanceof ServiceTagCollisionError;
708
632
  /** Wrap a backend with Runtime-local resolution paths and acquisition errors. */
709
- const createResolutionResolver = (resolver, storage = defaultRuntimeContextStorage) => {
633
+ const createResolutionResolver = (resolver, storage = defaultRuntimeContextStorage, observers = []) => {
710
634
  const wrapped = { async resolve(token) {
711
635
  const context = getRuntimeContext(storage);
712
- const path = context?.resolver === wrapped ? context.resolutionPath : [];
636
+ const path = context?.resolutionPath ?? [];
713
637
  const cycleStart = findCycleStart(path, token);
714
- if (cycleStart >= 0) throw new CircularDependencyError([...path.slice(cycleStart), token]);
715
638
  const resolutionPath = [...path, token];
639
+ const notifyResolve = (outcome) => {
640
+ notifyRuntimeObservers(observers, (observer) => observer.onServiceResolve, {
641
+ service: token,
642
+ resolutionPath,
643
+ outcome
644
+ });
645
+ };
646
+ if (cycleStart >= 0) {
647
+ const error = new CircularDependencyError([...path.slice(cycleStart), token]);
648
+ notifyResolve({
649
+ status: "failure",
650
+ cause: error
651
+ });
652
+ throw error;
653
+ }
716
654
  const nextContext = makeRuntimeContext(wrapped, context?.scope, resolutionPath, context?.signal);
717
655
  return await runRuntimeContext(storage, nextContext, async () => {
718
656
  try {
719
- return await resolver.resolve(token);
657
+ const instance = await resolver.resolve(token);
658
+ notifyResolve({ status: "success" });
659
+ return instance;
720
660
  } catch (cause) {
721
- if (shouldPreserve(cause)) throw cause;
722
- throw new ServiceAcquisitionError(token, resolutionPath, cause);
661
+ if (shouldPreserve(cause)) {
662
+ notifyResolve({
663
+ status: "failure",
664
+ cause
665
+ });
666
+ throw cause;
667
+ }
668
+ const error = new ServiceAcquisitionError(token, resolutionPath, cause);
669
+ notifyResolve({
670
+ status: "failure",
671
+ cause: error
672
+ });
673
+ throw error;
723
674
  }
724
675
  });
725
676
  } };
@@ -738,19 +689,83 @@ const normalizeDisposeCauses = (cause) => {
738
689
  if (cause instanceof AggregateError) return [...cause.errors];
739
690
  return [cause];
740
691
  };
692
+ const isScopeOutcome = (input) => input !== void 0 && "status" in input;
693
+ const validateDisposeOptions = (options) => {
694
+ const { gracePeriod } = options;
695
+ if (gracePeriod !== void 0 && (!Number.isFinite(gracePeriod) || gracePeriod < 0)) throw new RangeError("Runtime dispose gracePeriod must be a finite non-negative number");
696
+ };
741
697
  const notifyShutdownFailure = async (observer, diagnostic) => {
742
698
  if (!observer) return;
743
699
  try {
744
700
  await observer(diagnostic);
745
701
  } catch {}
746
702
  };
747
- const bindProviderToScope = (provider, rootScope, contextStorage) => ({
703
+ const bindProviderToScope = (provider, scope, contextStorage, resolver, observers) => ({
748
704
  service: provider.service,
749
- acquire: () => ScopeRuntime.run(rootScope, async () => {
750
- if (!provider.release) return await provider.acquire();
751
- return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
752
- }, contextStorage)
705
+ acquire: () => {
706
+ const current = getRuntimeContext(contextStorage);
707
+ const context = makeRuntimeContext(resolver, scope, current?.resolutionPath ?? [], current?.signal);
708
+ return runRuntimeContext(contextStorage, context, () => ScopeRuntime.run(scope, async () => {
709
+ const resolutionPath = current?.resolutionPath ?? [provider.service];
710
+ try {
711
+ const instance = provider.release ? await scope.acquire(() => provider.acquire(), async (resource, outcome) => {
712
+ try {
713
+ await provider.release(resource, outcome);
714
+ notifyRuntimeObservers(observers, (observer) => observer.onResourceRelease, {
715
+ service: provider.service,
716
+ outcome
717
+ });
718
+ } catch (cause) {
719
+ notifyRuntimeObservers(observers, (observer) => observer.onResourceRelease, {
720
+ service: provider.service,
721
+ outcome,
722
+ error: cause
723
+ });
724
+ throw cause;
725
+ }
726
+ }) : await provider.acquire();
727
+ notifyRuntimeObservers(observers, (observer) => observer.onServiceAcquire, {
728
+ service: provider.service,
729
+ resolutionPath,
730
+ outcome: SCOPE_SUCCESS
731
+ });
732
+ return instance;
733
+ } catch (cause) {
734
+ notifyRuntimeObservers(observers, (observer) => observer.onServiceAcquire, {
735
+ service: provider.service,
736
+ resolutionPath,
737
+ outcome: {
738
+ status: "failure",
739
+ cause
740
+ }
741
+ });
742
+ throw cause;
743
+ }
744
+ }, contextStorage));
745
+ }
753
746
  });
747
+ /** Resolve request-local providers first, then fall back to the Runtime root. */
748
+ var ExecutionLayerBackend = class {
749
+ local;
750
+ root;
751
+ localTags = /* @__PURE__ */ new Set();
752
+ constructor(local, root) {
753
+ this.local = local;
754
+ this.root = root;
755
+ }
756
+ register(registration) {
757
+ this.localTags.add(registration.service.serviceTag);
758
+ this.local.register(registration);
759
+ }
760
+ async resolve(token) {
761
+ if (this.localTags.has(token.serviceTag)) return await this.local.resolve(token);
762
+ return await this.root.resolve(token);
763
+ }
764
+ async disposeAll() {
765
+ await this.local.disposeAll();
766
+ this.localTags.clear();
767
+ }
768
+ };
754
769
  var RuntimeHandleImpl = class {
755
770
  backend;
756
771
  resolver;
@@ -758,34 +773,79 @@ var RuntimeHandleImpl = class {
758
773
  onCleanupFailure;
759
774
  contextStorage;
760
775
  signal;
776
+ observers;
777
+ services;
761
778
  disposePromise;
779
+ warmupPromise;
762
780
  executions = /* @__PURE__ */ new Set();
781
+ shutdownController = new AbortController();
763
782
  state = "active";
764
- constructor(backend, resolver, rootScope, onCleanupFailure, contextStorage, signal) {
783
+ constructor(backend, resolver, rootScope, onCleanupFailure, contextStorage, signal, observers, services) {
765
784
  this.backend = backend;
766
785
  this.resolver = resolver;
767
786
  this.rootScope = rootScope;
768
787
  this.onCleanupFailure = onCleanupFailure;
769
788
  this.contextStorage = contextStorage;
770
789
  this.signal = signal;
790
+ this.observers = observers;
791
+ this.services = services;
771
792
  }
772
- run(program) {
793
+ run(program, options) {
773
794
  this.assertActive();
774
795
  const executionScope = this.rootScope.fork();
796
+ const signalLink = linkAbortSignals(this.signal, options?.signal, this.shutdownController.signal);
797
+ return this.startExecution(signalLink, () => this.runExecution(executionScope, program, this.resolver, signalLink.signal));
798
+ }
799
+ runWith(layer, program, options) {
800
+ this.assertActive();
801
+ const executionScope = this.rootScope.fork();
802
+ const localBackend = new MapLayerBackend();
803
+ const backend = new ExecutionLayerBackend(localBackend, this.backend);
804
+ const resolver = createResolutionResolver(backend, this.contextStorage, this.observers);
805
+ const signalLink = linkAbortSignals(this.signal, options?.signal, this.shutdownController.signal);
806
+ return this.startExecution(signalLink, async () => {
807
+ try {
808
+ return await this.runExecution(executionScope, async () => {
809
+ for (const provider of layer.providers) backend.register(bindProviderToScope(provider, executionScope, this.contextStorage, resolver, this.observers));
810
+ return await program();
811
+ }, resolver, signalLink.signal);
812
+ } finally {
813
+ await localBackend.disposeAll();
814
+ }
815
+ });
816
+ }
817
+ warmup() {
818
+ this.assertActive();
819
+ if (this.warmupPromise) return this.warmupPromise;
820
+ const warmup = runRuntimeContext(this.contextStorage, makeRuntimeContext(this.resolver, this.rootScope, [], this.signal), async () => {
821
+ for (const service of this.services) await this.resolver.resolve(service);
822
+ });
823
+ this.warmupPromise = warmup;
824
+ warmup.then(() => {
825
+ if (this.warmupPromise === warmup) this.warmupPromise = void 0;
826
+ }, () => {
827
+ if (this.warmupPromise === warmup) this.warmupPromise = void 0;
828
+ });
829
+ return warmup;
830
+ }
831
+ startExecution(signalLink, run) {
775
832
  let resolveExecution;
776
833
  let rejectExecution;
777
834
  const execution = new Promise((resolve, reject) => {
778
835
  resolveExecution = resolve;
779
836
  rejectExecution = reject;
780
837
  });
781
- this.executions.add(execution);
838
+ const activeExecution = { promise: execution };
839
+ this.executions.add(activeExecution);
782
840
  execution.then(() => {
783
- this.executions.delete(execution);
841
+ this.executions.delete(activeExecution);
842
+ signalLink.dispose();
784
843
  }, () => {
785
- this.executions.delete(execution);
844
+ this.executions.delete(activeExecution);
845
+ signalLink.dispose();
786
846
  });
787
847
  try {
788
- this.runExecution(executionScope, program).then((value) => {
848
+ run().then((value) => {
789
849
  resolveExecution(value);
790
850
  }, (cause) => {
791
851
  rejectExecution(cause);
@@ -795,29 +855,54 @@ var RuntimeHandleImpl = class {
795
855
  }
796
856
  return execution;
797
857
  }
798
- runExecution(executionScope, program) {
858
+ runExecution(executionScope, program, resolver = this.resolver, signal = this.shutdownController.signal) {
799
859
  const options = this.onCleanupFailure ? {
800
860
  classify: classifyRuntimeOutcome,
801
861
  onCleanupFailure: this.onCleanupFailure
802
862
  } : { classify: classifyRuntimeOutcome };
863
+ notifyRuntimeObservers(this.observers, (observer) => observer.onExecutionStart, { scope: executionScope });
803
864
  return runScoped(executionScope, program, {
804
865
  ...options,
805
866
  contextStorage: this.contextStorage,
806
- context: makeRuntimeContext(this.resolver, executionScope, [], this.signal)
867
+ context: makeRuntimeContext(resolver, executionScope, [], signal)
868
+ }).then((value) => {
869
+ notifyRuntimeObservers(this.observers, (observer) => observer.onExecutionEnd, {
870
+ scope: executionScope,
871
+ outcome: classifyRuntimeOutcome(value)
872
+ });
873
+ return value;
874
+ }, (cause) => {
875
+ notifyRuntimeObservers(this.observers, (observer) => observer.onExecutionEnd, {
876
+ scope: executionScope,
877
+ outcome: {
878
+ status: "failure",
879
+ cause
880
+ }
881
+ });
882
+ throw cause;
807
883
  });
808
884
  }
809
- dispose(outcome = SCOPE_SUCCESS) {
885
+ dispose(input) {
810
886
  if (this.disposePromise) return this.disposePromise;
887
+ const outcome = isScopeOutcome(input) || input === void 0 ? input ?? SCOPE_SUCCESS : SCOPE_SUCCESS;
888
+ const options = isScopeOutcome(input) || input === void 0 ? {} : input;
889
+ validateDisposeOptions(options);
811
890
  this.state = "disposing";
812
891
  const executions = [...this.executions];
813
- this.disposePromise = this.performDispose(executions, outcome);
892
+ this.disposePromise = this.performDispose(executions, outcome, options);
814
893
  return this.disposePromise;
815
894
  }
816
- async performDispose(executions, outcome) {
895
+ async performDispose(executions, outcome, options) {
817
896
  const failures = [];
818
- await Promise.allSettled(executions);
897
+ await Promise.allSettled(this.warmupPromise ? [this.warmupPromise] : []);
898
+ await this.waitForExecutions(executions, options);
819
899
  try {
820
- await runRuntimeContext(this.contextStorage, makeRuntimeContext(this.resolver, this.rootScope, [], this.signal), () => this.rootScope.close(outcome));
900
+ const signalLink = linkAbortSignals(this.signal, this.shutdownController.signal);
901
+ try {
902
+ await runRuntimeContext(this.contextStorage, makeRuntimeContext(this.resolver, this.rootScope, [], signalLink.signal), () => this.rootScope.close(outcome));
903
+ } finally {
904
+ signalLink.dispose();
905
+ }
821
906
  } catch (cause) {
822
907
  failures.push(cause);
823
908
  }
@@ -836,6 +921,21 @@ var RuntimeHandleImpl = class {
836
921
  throw error;
837
922
  }
838
923
  }
924
+ async waitForExecutions(executions, options) {
925
+ const settled = Promise.allSettled(executions.map((execution) => execution.promise));
926
+ if (options.abortAfterGracePeriod !== true || executions.length === 0) {
927
+ await settled;
928
+ return;
929
+ }
930
+ const gracePeriod = options.gracePeriod ?? 0;
931
+ let timer;
932
+ const timedOut = await Promise.race([settled.then(() => false), new Promise((resolve) => {
933
+ timer = setTimeout(() => resolve(true), gracePeriod);
934
+ })]);
935
+ if (timer !== void 0) clearTimeout(timer);
936
+ if (timedOut && !this.shutdownController.signal.aborted) this.shutdownController.abort(/* @__PURE__ */ new Error("Runtime shutdown grace period exceeded"));
937
+ await settled;
938
+ }
839
939
  assertActive() {
840
940
  if (this.state !== "active") throw new RuntimeHandleDisposedError();
841
941
  }
@@ -844,13 +944,14 @@ var RuntimeHandleImpl = class {
844
944
  const createRuntimeHandle = async (layer, backend, options = {}) => {
845
945
  const rootScope = Scope.make();
846
946
  const contextStorage = options.contextStorage ?? defaultRuntimeContextStorage;
847
- const resolver = createResolutionResolver(backend, contextStorage);
947
+ const observers = options.observers ?? [];
948
+ const resolver = createResolutionResolver(backend, contextStorage, observers);
848
949
  ScopeRuntime.bind(rootScope, contextStorage);
849
950
  let current;
850
951
  try {
851
952
  for (const provider of layer.providers) {
852
953
  current = provider;
853
- await backend.register(bindProviderToScope(provider, rootScope, contextStorage));
954
+ await backend.register(bindProviderToScope(provider, rootScope, contextStorage, resolver, observers));
854
955
  }
855
956
  } catch (registrationCause) {
856
957
  const outcome = {
@@ -878,7 +979,7 @@ const createRuntimeHandle = async (layer, backend, options = {}) => {
878
979
  const cleanupCause = cleanupCauses.length === 1 ? cleanupCauses[0] : cleanupCauses.length > 1 ? new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses)) : void 0;
879
980
  throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
880
981
  }
881
- return new RuntimeHandleImpl(backend, resolver, rootScope, options.onCleanupFailure, contextStorage, options.signal);
982
+ return new RuntimeHandleImpl(backend, resolver, rootScope, options.onCleanupFailure, contextStorage, options.signal, observers, layer.providers.map((provider) => provider.service));
882
983
  };
883
984
  //#endregion
884
985
  //#region src/runtime/runtime.ts
@@ -917,7 +1018,9 @@ var Runtime = class Runtime {
917
1018
  static async make(layer, backendOrOptions, legacyOptions) {
918
1019
  const { backend, options } = resolveRuntimeConfig(backendOrOptions, legacyOptions);
919
1020
  const handle = await createRuntimeHandle(layer, backend, options);
920
- return new Runtime(handle);
1021
+ const runtime = new Runtime(handle);
1022
+ if (options.warmup) await runtime.warmup();
1023
+ return runtime;
921
1024
  }
922
1025
  static async run(layer, backendOrProgramOrOptions, programOrOptions, legacyOptions) {
923
1026
  let program;
@@ -997,16 +1100,33 @@ var Runtime = class Runtime {
997
1100
  if (executionFailed) throw executionFailure;
998
1101
  return value;
999
1102
  }
1103
+ /** Resolve every Layer provider and dispose the Runtime if warmup fails. */
1104
+ async warmup() {
1105
+ try {
1106
+ await this.handle.warmup();
1107
+ } catch (cause) {
1108
+ try {
1109
+ await this.handle.dispose({
1110
+ status: "failure",
1111
+ cause
1112
+ });
1113
+ } catch {}
1114
+ throw cause;
1115
+ }
1116
+ }
1000
1117
  /** Run one execution in this Runtime's child Scope. */
1001
- run(program) {
1002
- return this.handle.run(program);
1118
+ run(program, options) {
1119
+ return this.handle.run(program, options);
1120
+ }
1121
+ /** Run one execution with a Layer owned by that execution's child Scope. */
1122
+ runWith(layer, program, options) {
1123
+ return this.handle.runWith(layer, program, options);
1003
1124
  }
1004
1125
  runUnchecked(program) {
1005
1126
  return this.handle.run(program);
1006
1127
  }
1007
- /** Stop new executions and release the Runtime's Layer resources. */
1008
- dispose() {
1009
- return this.handle.dispose();
1128
+ dispose(optionsOrOutcome) {
1129
+ return this.handle.dispose(optionsOrOutcome);
1010
1130
  }
1011
1131
  /** Release Runtime-owned resources through JavaScript's async disposal protocol. */
1012
1132
  async [Symbol.asyncDispose]() {
@@ -1017,6 +1137,6 @@ var Runtime = class Runtime {
1017
1137
  }
1018
1138
  };
1019
1139
  //#endregion
1020
- export { CircularDependencyError, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, MapLayerBackend, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, RuntimeContextNotConfiguredError, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceAcquisitionError, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
1140
+ export { CircularDependencyError, CurrentAbortSignal, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, MapLayerBackend, Program, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, RuntimeContextNotConfiguredError, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceAcquisitionError, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
1021
1141
 
1022
1142
  //# sourceMappingURL=index.mjs.map