better-effect 0.7.0 → 0.8.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
@@ -702,24 +702,110 @@ const classifyRuntimeOutcome = (value) => {
702
702
  return { status: "success" };
703
703
  };
704
704
  //#endregion
705
+ //#region src/runtime/signal.ts
706
+ const neverAbortedSignal = new AbortController().signal;
707
+ /** Link caller, Runtime and shutdown signals without owning the caller's controller. */
708
+ const linkAbortSignals = (...signals) => {
709
+ const active = signals.filter((signal) => signal !== void 0);
710
+ if (active.length === 0) return {
711
+ signal: neverAbortedSignal,
712
+ dispose: () => {}
713
+ };
714
+ if (active.length === 1) return {
715
+ signal: active[0],
716
+ dispose: () => {}
717
+ };
718
+ const controller = new AbortController();
719
+ const listeners = [];
720
+ let disposed = false;
721
+ const dispose = () => {
722
+ if (disposed) return;
723
+ disposed = true;
724
+ for (const [source, listener] of listeners) source.removeEventListener("abort", listener);
725
+ listeners.length = 0;
726
+ };
727
+ const abortFrom = (source) => {
728
+ if (controller.signal.aborted) return;
729
+ controller.abort(source.reason);
730
+ dispose();
731
+ };
732
+ for (const source of active) {
733
+ if (source.aborted) {
734
+ abortFrom(source);
735
+ break;
736
+ }
737
+ const listener = () => abortFrom(source);
738
+ listeners.push([source, listener]);
739
+ source.addEventListener("abort", listener, { once: true });
740
+ }
741
+ return {
742
+ signal: controller.signal,
743
+ dispose
744
+ };
745
+ };
746
+ /** Return the current cooperative-cancellation signal. */
747
+ const currentAbortSignal = () => currentRuntimeContext().signal ?? neverAbortedSignal;
748
+ /** Yieldable access to the signal of the current Runtime execution. */
749
+ const CurrentAbortSignal = { *[Symbol.iterator]() {
750
+ return currentAbortSignal();
751
+ } };
752
+ //#endregion
753
+ //#region src/runtime/observer.ts
754
+ const notifyRuntimeObservers = (observers, select, event) => {
755
+ for (const observer of observers) {
756
+ const callback = select(observer);
757
+ if (!callback) continue;
758
+ try {
759
+ Promise.resolve(callback(event)).catch(() => {});
760
+ } catch {}
761
+ }
762
+ };
763
+ //#endregion
705
764
  //#region src/layer/resolution.ts
706
765
  const findCycleStart = (path, token) => path.findIndex((current) => current.serviceTag === token.serviceTag);
707
766
  const shouldPreserve = (cause) => cause instanceof CircularDependencyError || cause instanceof ServiceAcquisitionError || cause instanceof ServiceNotFoundError || cause instanceof ServiceTagCollisionError;
708
767
  /** Wrap a backend with Runtime-local resolution paths and acquisition errors. */
709
- const createResolutionResolver = (resolver, storage = defaultRuntimeContextStorage) => {
768
+ const createResolutionResolver = (resolver, storage = defaultRuntimeContextStorage, observers = []) => {
710
769
  const wrapped = { async resolve(token) {
711
770
  const context = getRuntimeContext(storage);
712
- const path = context?.resolver === wrapped ? context.resolutionPath : [];
771
+ const path = context?.resolutionPath ?? [];
713
772
  const cycleStart = findCycleStart(path, token);
714
- if (cycleStart >= 0) throw new CircularDependencyError([...path.slice(cycleStart), token]);
715
773
  const resolutionPath = [...path, token];
774
+ const notifyResolve = (outcome) => {
775
+ notifyRuntimeObservers(observers, (observer) => observer.onServiceResolve, {
776
+ service: token,
777
+ resolutionPath,
778
+ outcome
779
+ });
780
+ };
781
+ if (cycleStart >= 0) {
782
+ const error = new CircularDependencyError([...path.slice(cycleStart), token]);
783
+ notifyResolve({
784
+ status: "failure",
785
+ cause: error
786
+ });
787
+ throw error;
788
+ }
716
789
  const nextContext = makeRuntimeContext(wrapped, context?.scope, resolutionPath, context?.signal);
717
790
  return await runRuntimeContext(storage, nextContext, async () => {
718
791
  try {
719
- return await resolver.resolve(token);
792
+ const instance = await resolver.resolve(token);
793
+ notifyResolve({ status: "success" });
794
+ return instance;
720
795
  } catch (cause) {
721
- if (shouldPreserve(cause)) throw cause;
722
- throw new ServiceAcquisitionError(token, resolutionPath, cause);
796
+ if (shouldPreserve(cause)) {
797
+ notifyResolve({
798
+ status: "failure",
799
+ cause
800
+ });
801
+ throw cause;
802
+ }
803
+ const error = new ServiceAcquisitionError(token, resolutionPath, cause);
804
+ notifyResolve({
805
+ status: "failure",
806
+ cause: error
807
+ });
808
+ throw error;
723
809
  }
724
810
  });
725
811
  } };
@@ -738,19 +824,83 @@ const normalizeDisposeCauses = (cause) => {
738
824
  if (cause instanceof AggregateError) return [...cause.errors];
739
825
  return [cause];
740
826
  };
827
+ const isScopeOutcome = (input) => input !== void 0 && "status" in input;
828
+ const validateDisposeOptions = (options) => {
829
+ const { gracePeriod } = options;
830
+ if (gracePeriod !== void 0 && (!Number.isFinite(gracePeriod) || gracePeriod < 0)) throw new RangeError("Runtime dispose gracePeriod must be a finite non-negative number");
831
+ };
741
832
  const notifyShutdownFailure = async (observer, diagnostic) => {
742
833
  if (!observer) return;
743
834
  try {
744
835
  await observer(diagnostic);
745
836
  } catch {}
746
837
  };
747
- const bindProviderToScope = (provider, rootScope, contextStorage) => ({
838
+ const bindProviderToScope = (provider, scope, contextStorage, resolver, observers) => ({
748
839
  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)
840
+ acquire: () => {
841
+ const current = getRuntimeContext(contextStorage);
842
+ const context = makeRuntimeContext(resolver, scope, current?.resolutionPath ?? [], current?.signal);
843
+ return runRuntimeContext(contextStorage, context, () => ScopeRuntime.run(scope, async () => {
844
+ const resolutionPath = current?.resolutionPath ?? [provider.service];
845
+ try {
846
+ const instance = provider.release ? await scope.acquire(() => provider.acquire(), async (resource, outcome) => {
847
+ try {
848
+ await provider.release(resource, outcome);
849
+ notifyRuntimeObservers(observers, (observer) => observer.onResourceRelease, {
850
+ service: provider.service,
851
+ outcome
852
+ });
853
+ } catch (cause) {
854
+ notifyRuntimeObservers(observers, (observer) => observer.onResourceRelease, {
855
+ service: provider.service,
856
+ outcome,
857
+ error: cause
858
+ });
859
+ throw cause;
860
+ }
861
+ }) : await provider.acquire();
862
+ notifyRuntimeObservers(observers, (observer) => observer.onServiceAcquire, {
863
+ service: provider.service,
864
+ resolutionPath,
865
+ outcome: SCOPE_SUCCESS
866
+ });
867
+ return instance;
868
+ } catch (cause) {
869
+ notifyRuntimeObservers(observers, (observer) => observer.onServiceAcquire, {
870
+ service: provider.service,
871
+ resolutionPath,
872
+ outcome: {
873
+ status: "failure",
874
+ cause
875
+ }
876
+ });
877
+ throw cause;
878
+ }
879
+ }, contextStorage));
880
+ }
753
881
  });
882
+ /** Resolve request-local providers first, then fall back to the Runtime root. */
883
+ var ExecutionLayerBackend = class {
884
+ local;
885
+ root;
886
+ localTags = /* @__PURE__ */ new Set();
887
+ constructor(local, root) {
888
+ this.local = local;
889
+ this.root = root;
890
+ }
891
+ register(registration) {
892
+ this.localTags.add(registration.service.serviceTag);
893
+ this.local.register(registration);
894
+ }
895
+ async resolve(token) {
896
+ if (this.localTags.has(token.serviceTag)) return await this.local.resolve(token);
897
+ return await this.root.resolve(token);
898
+ }
899
+ async disposeAll() {
900
+ await this.local.disposeAll();
901
+ this.localTags.clear();
902
+ }
903
+ };
754
904
  var RuntimeHandleImpl = class {
755
905
  backend;
756
906
  resolver;
@@ -758,34 +908,79 @@ var RuntimeHandleImpl = class {
758
908
  onCleanupFailure;
759
909
  contextStorage;
760
910
  signal;
911
+ observers;
912
+ services;
761
913
  disposePromise;
914
+ warmupPromise;
762
915
  executions = /* @__PURE__ */ new Set();
916
+ shutdownController = new AbortController();
763
917
  state = "active";
764
- constructor(backend, resolver, rootScope, onCleanupFailure, contextStorage, signal) {
918
+ constructor(backend, resolver, rootScope, onCleanupFailure, contextStorage, signal, observers, services) {
765
919
  this.backend = backend;
766
920
  this.resolver = resolver;
767
921
  this.rootScope = rootScope;
768
922
  this.onCleanupFailure = onCleanupFailure;
769
923
  this.contextStorage = contextStorage;
770
924
  this.signal = signal;
925
+ this.observers = observers;
926
+ this.services = services;
771
927
  }
772
- run(program) {
928
+ run(program, options) {
929
+ this.assertActive();
930
+ const executionScope = this.rootScope.fork();
931
+ const signalLink = linkAbortSignals(this.signal, options?.signal, this.shutdownController.signal);
932
+ return this.startExecution(signalLink, () => this.runExecution(executionScope, program, this.resolver, signalLink.signal));
933
+ }
934
+ runWith(layer, program, options) {
773
935
  this.assertActive();
774
936
  const executionScope = this.rootScope.fork();
937
+ const localBackend = new MapLayerBackend();
938
+ const backend = new ExecutionLayerBackend(localBackend, this.backend);
939
+ const resolver = createResolutionResolver(backend, this.contextStorage, this.observers);
940
+ const signalLink = linkAbortSignals(this.signal, options?.signal, this.shutdownController.signal);
941
+ return this.startExecution(signalLink, async () => {
942
+ try {
943
+ return await this.runExecution(executionScope, async () => {
944
+ for (const provider of layer.providers) backend.register(bindProviderToScope(provider, executionScope, this.contextStorage, resolver, this.observers));
945
+ return await program();
946
+ }, resolver, signalLink.signal);
947
+ } finally {
948
+ await localBackend.disposeAll();
949
+ }
950
+ });
951
+ }
952
+ warmup() {
953
+ this.assertActive();
954
+ if (this.warmupPromise) return this.warmupPromise;
955
+ const warmup = runRuntimeContext(this.contextStorage, makeRuntimeContext(this.resolver, this.rootScope, [], this.signal), async () => {
956
+ for (const service of this.services) await this.resolver.resolve(service);
957
+ });
958
+ this.warmupPromise = warmup;
959
+ warmup.then(() => {
960
+ if (this.warmupPromise === warmup) this.warmupPromise = void 0;
961
+ }, () => {
962
+ if (this.warmupPromise === warmup) this.warmupPromise = void 0;
963
+ });
964
+ return warmup;
965
+ }
966
+ startExecution(signalLink, run) {
775
967
  let resolveExecution;
776
968
  let rejectExecution;
777
969
  const execution = new Promise((resolve, reject) => {
778
970
  resolveExecution = resolve;
779
971
  rejectExecution = reject;
780
972
  });
781
- this.executions.add(execution);
973
+ const activeExecution = { promise: execution };
974
+ this.executions.add(activeExecution);
782
975
  execution.then(() => {
783
- this.executions.delete(execution);
976
+ this.executions.delete(activeExecution);
977
+ signalLink.dispose();
784
978
  }, () => {
785
- this.executions.delete(execution);
979
+ this.executions.delete(activeExecution);
980
+ signalLink.dispose();
786
981
  });
787
982
  try {
788
- this.runExecution(executionScope, program).then((value) => {
983
+ run().then((value) => {
789
984
  resolveExecution(value);
790
985
  }, (cause) => {
791
986
  rejectExecution(cause);
@@ -795,29 +990,54 @@ var RuntimeHandleImpl = class {
795
990
  }
796
991
  return execution;
797
992
  }
798
- runExecution(executionScope, program) {
993
+ runExecution(executionScope, program, resolver = this.resolver, signal = this.shutdownController.signal) {
799
994
  const options = this.onCleanupFailure ? {
800
995
  classify: classifyRuntimeOutcome,
801
996
  onCleanupFailure: this.onCleanupFailure
802
997
  } : { classify: classifyRuntimeOutcome };
998
+ notifyRuntimeObservers(this.observers, (observer) => observer.onExecutionStart, { scope: executionScope });
803
999
  return runScoped(executionScope, program, {
804
1000
  ...options,
805
1001
  contextStorage: this.contextStorage,
806
- context: makeRuntimeContext(this.resolver, executionScope, [], this.signal)
1002
+ context: makeRuntimeContext(resolver, executionScope, [], signal)
1003
+ }).then((value) => {
1004
+ notifyRuntimeObservers(this.observers, (observer) => observer.onExecutionEnd, {
1005
+ scope: executionScope,
1006
+ outcome: classifyRuntimeOutcome(value)
1007
+ });
1008
+ return value;
1009
+ }, (cause) => {
1010
+ notifyRuntimeObservers(this.observers, (observer) => observer.onExecutionEnd, {
1011
+ scope: executionScope,
1012
+ outcome: {
1013
+ status: "failure",
1014
+ cause
1015
+ }
1016
+ });
1017
+ throw cause;
807
1018
  });
808
1019
  }
809
- dispose(outcome = SCOPE_SUCCESS) {
1020
+ dispose(input) {
810
1021
  if (this.disposePromise) return this.disposePromise;
1022
+ const outcome = isScopeOutcome(input) || input === void 0 ? input ?? SCOPE_SUCCESS : SCOPE_SUCCESS;
1023
+ const options = isScopeOutcome(input) || input === void 0 ? {} : input;
1024
+ validateDisposeOptions(options);
811
1025
  this.state = "disposing";
812
1026
  const executions = [...this.executions];
813
- this.disposePromise = this.performDispose(executions, outcome);
1027
+ this.disposePromise = this.performDispose(executions, outcome, options);
814
1028
  return this.disposePromise;
815
1029
  }
816
- async performDispose(executions, outcome) {
1030
+ async performDispose(executions, outcome, options) {
817
1031
  const failures = [];
818
- await Promise.allSettled(executions);
1032
+ await Promise.allSettled(this.warmupPromise ? [this.warmupPromise] : []);
1033
+ await this.waitForExecutions(executions, options);
819
1034
  try {
820
- await runRuntimeContext(this.contextStorage, makeRuntimeContext(this.resolver, this.rootScope, [], this.signal), () => this.rootScope.close(outcome));
1035
+ const signalLink = linkAbortSignals(this.signal, this.shutdownController.signal);
1036
+ try {
1037
+ await runRuntimeContext(this.contextStorage, makeRuntimeContext(this.resolver, this.rootScope, [], signalLink.signal), () => this.rootScope.close(outcome));
1038
+ } finally {
1039
+ signalLink.dispose();
1040
+ }
821
1041
  } catch (cause) {
822
1042
  failures.push(cause);
823
1043
  }
@@ -836,6 +1056,21 @@ var RuntimeHandleImpl = class {
836
1056
  throw error;
837
1057
  }
838
1058
  }
1059
+ async waitForExecutions(executions, options) {
1060
+ const settled = Promise.allSettled(executions.map((execution) => execution.promise));
1061
+ if (options.abortAfterGracePeriod !== true || executions.length === 0) {
1062
+ await settled;
1063
+ return;
1064
+ }
1065
+ const gracePeriod = options.gracePeriod ?? 0;
1066
+ let timer;
1067
+ const timedOut = await Promise.race([settled.then(() => false), new Promise((resolve) => {
1068
+ timer = setTimeout(() => resolve(true), gracePeriod);
1069
+ })]);
1070
+ if (timer !== void 0) clearTimeout(timer);
1071
+ if (timedOut && !this.shutdownController.signal.aborted) this.shutdownController.abort(/* @__PURE__ */ new Error("Runtime shutdown grace period exceeded"));
1072
+ await settled;
1073
+ }
839
1074
  assertActive() {
840
1075
  if (this.state !== "active") throw new RuntimeHandleDisposedError();
841
1076
  }
@@ -844,13 +1079,14 @@ var RuntimeHandleImpl = class {
844
1079
  const createRuntimeHandle = async (layer, backend, options = {}) => {
845
1080
  const rootScope = Scope.make();
846
1081
  const contextStorage = options.contextStorage ?? defaultRuntimeContextStorage;
847
- const resolver = createResolutionResolver(backend, contextStorage);
1082
+ const observers = options.observers ?? [];
1083
+ const resolver = createResolutionResolver(backend, contextStorage, observers);
848
1084
  ScopeRuntime.bind(rootScope, contextStorage);
849
1085
  let current;
850
1086
  try {
851
1087
  for (const provider of layer.providers) {
852
1088
  current = provider;
853
- await backend.register(bindProviderToScope(provider, rootScope, contextStorage));
1089
+ await backend.register(bindProviderToScope(provider, rootScope, contextStorage, resolver, observers));
854
1090
  }
855
1091
  } catch (registrationCause) {
856
1092
  const outcome = {
@@ -878,7 +1114,7 @@ const createRuntimeHandle = async (layer, backend, options = {}) => {
878
1114
  const cleanupCause = cleanupCauses.length === 1 ? cleanupCauses[0] : cleanupCauses.length > 1 ? new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses)) : void 0;
879
1115
  throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
880
1116
  }
881
- return new RuntimeHandleImpl(backend, resolver, rootScope, options.onCleanupFailure, contextStorage, options.signal);
1117
+ return new RuntimeHandleImpl(backend, resolver, rootScope, options.onCleanupFailure, contextStorage, options.signal, observers, layer.providers.map((provider) => provider.service));
882
1118
  };
883
1119
  //#endregion
884
1120
  //#region src/runtime/runtime.ts
@@ -917,7 +1153,9 @@ var Runtime = class Runtime {
917
1153
  static async make(layer, backendOrOptions, legacyOptions) {
918
1154
  const { backend, options } = resolveRuntimeConfig(backendOrOptions, legacyOptions);
919
1155
  const handle = await createRuntimeHandle(layer, backend, options);
920
- return new Runtime(handle);
1156
+ const runtime = new Runtime(handle);
1157
+ if (options.warmup) await runtime.warmup();
1158
+ return runtime;
921
1159
  }
922
1160
  static async run(layer, backendOrProgramOrOptions, programOrOptions, legacyOptions) {
923
1161
  let program;
@@ -997,16 +1235,33 @@ var Runtime = class Runtime {
997
1235
  if (executionFailed) throw executionFailure;
998
1236
  return value;
999
1237
  }
1238
+ /** Resolve every Layer provider and dispose the Runtime if warmup fails. */
1239
+ async warmup() {
1240
+ try {
1241
+ await this.handle.warmup();
1242
+ } catch (cause) {
1243
+ try {
1244
+ await this.handle.dispose({
1245
+ status: "failure",
1246
+ cause
1247
+ });
1248
+ } catch {}
1249
+ throw cause;
1250
+ }
1251
+ }
1000
1252
  /** Run one execution in this Runtime's child Scope. */
1001
- run(program) {
1002
- return this.handle.run(program);
1253
+ run(program, options) {
1254
+ return this.handle.run(program, options);
1255
+ }
1256
+ /** Run one execution with a Layer owned by that execution's child Scope. */
1257
+ runWith(layer, program, options) {
1258
+ return this.handle.runWith(layer, program, options);
1003
1259
  }
1004
1260
  runUnchecked(program) {
1005
1261
  return this.handle.run(program);
1006
1262
  }
1007
- /** Stop new executions and release the Runtime's Layer resources. */
1008
- dispose() {
1009
- return this.handle.dispose();
1263
+ dispose(optionsOrOutcome) {
1264
+ return this.handle.dispose(optionsOrOutcome);
1010
1265
  }
1011
1266
  /** Release Runtime-owned resources through JavaScript's async disposal protocol. */
1012
1267
  async [Symbol.asyncDispose]() {
@@ -1017,6 +1272,6 @@ var Runtime = class Runtime {
1017
1272
  }
1018
1273
  };
1019
1274
  //#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 };
1275
+ export { CircularDependencyError, CurrentAbortSignal, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, MapLayerBackend, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, RuntimeContextNotConfiguredError, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceAcquisitionError, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
1021
1276
 
1022
1277
  //# sourceMappingURL=index.mjs.map