@znight/core 0.1.1 → 0.1.3

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Ugho STEPHAN
3
+ Copyright (c) 2026 Ugho STEPHAN
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -238,6 +238,18 @@ Scopes are isolated with `AsyncLocalStorage` (concurrent requests never share in
238
238
  request-scoped provider outside of a scope throws `ZNIGHT013`. `container.currentScope()` exposes the
239
239
  active scope (`get`, `set`, `has`). Plugins (HTTP, messaging) open the scopes for you.
240
240
 
241
+ `container.openScope(values?)` opens the same scope without a callback, for hot paths that stay
242
+ synchronous as long as their work is (the HTTP pipeline): `run(fn, ...args)` calls `fn` in the scope
243
+ and returns its result (its continuations stay in the scope), `close()` disposes the instances created
244
+ in it. `close()` returns `undefined` when there is nothing to dispose, otherwise a promise that
245
+ rejects with `ZNIGHT021` when a disposal fails; it must be called once the work is over.
246
+
247
+ ```ts
248
+ const scope = container.openScope([['@znight/http#HttpContext', ctx]])
249
+ const user = scope.run(() => container.get(CurrentUser).name)
250
+ await scope.close()
251
+ ```
252
+
241
253
  ## Lazy dependencies
242
254
 
243
255
  `Lazy<T>` injects a handle resolved at call time, in the current scope. It breaks cycles and lets a
package/dist/index.d.mts CHANGED
@@ -399,6 +399,29 @@ interface ManagedInstance {
399
399
  }
400
400
  /** Seed values of a request scope: token-keyed pairs (aliases and known classes are accepted). */
401
401
  type ScopeValues = Iterable<readonly [ProviderKey, unknown]>;
402
+ /**
403
+ * A request scope opened with {@link Container.openScope}: its owner runs code in it with
404
+ * {@link OpenedScope.run} and ends it with {@link OpenedScope.close}. The callback-free form of
405
+ * {@link Container.runInScope}, for hot paths that stay synchronous as long as their work is (the
406
+ * HTTP pipeline): no promise is created unless something is asynchronous.
407
+ */
408
+ interface OpenedScope {
409
+ /** The values of the scope (what `currentScope()` returns in {@link run}). */
410
+ readonly context: ScopeContext;
411
+ /**
412
+ * Calls `fn(...args)` in the scope and returns its result. The code it runs and the asynchronous
413
+ * continuations it creates (awaits, promise callbacks, timers) see the scope, like the callback
414
+ * of `runInScope`. May be called several times until the scope is closed.
415
+ */
416
+ run<A extends readonly unknown[], R>(fn: (...args: A) => R, ...args: A): R;
417
+ /**
418
+ * Closes the scope (a later resolution of a request-scoped provider in it throws ZNIGHT013) and
419
+ * disposes the instances created in it, in reverse creation order. Returns `undefined` when no
420
+ * instance was created (nothing to wait for), otherwise a promise rejecting with ZNIGHT021 when a
421
+ * disposal fails. Idempotent: later calls return the first result.
422
+ */
423
+ close(): Promise<void> | undefined;
424
+ }
402
425
  /**
403
426
  * The dependency injection container. It consumes the metadata generated by `@znight/compiler`:
404
427
  * no reflection happens at runtime, factories are called in a precomputed order and resolving a
@@ -500,6 +523,12 @@ export declare class Container {
500
523
  * disposal failed.
501
524
  */
502
525
  runInScope<R>(fn: () => R | Promise<R>, values?: ScopeValues): Promise<R>;
526
+ /**
527
+ * Opens a request scope seeded with `values`, like {@link runInScope}, but without running
528
+ * anything: the caller runs code in it (`run`) and closes it (`close`) when the work it started
529
+ * is over. Request-scoped providers resolved in it are disposed by `close`, which must be called.
530
+ */
531
+ openScope(values?: ScopeValues): OpenedScope;
503
532
  /** The active request scope, if any. */
504
533
  currentScope(): ScopeContext | undefined;
505
534
  /**
@@ -1065,5 +1094,5 @@ export declare class ZnightAggregateError extends ZnightError {
1065
1094
  * that were attached to it instead of being thrown. Empty when there are none.
1066
1095
  */
1067
1096
  export declare function getSuppressedErrors(error: unknown): readonly unknown[];
1068
- export type { ApplicationOptions, ApplicationState, ClassKey, ClassMarker, ConsoleLike, ConsoleLoggerOptions, CoreLogger, HookEvent, HookListener, HookName, HookWrapper, HooksOptions, HotContext, HttpRequestEvent, InitOptions, InjectableOptions, InterceptMethodOptions, InterceptOptionsArgs, InterceptorClass, InterceptorLink, LogContext, LogLevel, ManagedInstance, MessageDeliveryEvent, MessagePublishEvent, MethodInterceptor, MethodInvocation, MethodMarker, OnReady, OnStart, OnStop, ParameterMarker, Plugin, PluginContext, PluginHook, PluginOptions, PluginRootsContext, ProcessLike, ProviderKey, ProviderValues, ProvidesOptions, ScheduledRunAbortReason, ScheduledRunEvent, ScheduledRunOutcome, ScheduledRunTrigger, ScopeContext, ScopeValues, ShutdownOptions, ShutdownSignal, TypedToken, ZnightHooks };
1097
+ export type { ApplicationOptions, ApplicationState, ClassKey, ClassMarker, ConsoleLike, ConsoleLoggerOptions, CoreLogger, HookEvent, HookListener, HookName, HookWrapper, HooksOptions, HotContext, HttpRequestEvent, InitOptions, InjectableOptions, InterceptMethodOptions, InterceptOptionsArgs, InterceptorClass, InterceptorLink, LogContext, LogLevel, ManagedInstance, MessageDeliveryEvent, MessagePublishEvent, MethodInterceptor, MethodInvocation, MethodMarker, OnReady, OnStart, OnStop, OpenedScope, ParameterMarker, Plugin, PluginContext, PluginHook, PluginOptions, PluginRootsContext, ProcessLike, ProviderKey, ProviderValues, ProvidesOptions, ScheduledRunAbortReason, ScheduledRunEvent, ScheduledRunOutcome, ScheduledRunTrigger, ScopeContext, ScopeValues, ShutdownOptions, ShutdownSignal, TypedToken, ZnightHooks };
1069
1098
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -809,12 +809,30 @@ function lifecycleError(step, error) {
809
809
  // src/scope.ts
810
810
  /** Implementation of {@link ScopeContext} used by the container (internal). */
811
811
  var RequestScope = class {
812
- values = /* @__PURE__ */ new Map();
813
812
  created = [];
814
813
  closed = false;
815
814
  #canonical;
816
- constructor(canonical) {
815
+ #values;
816
+ /** Seed values not yet in {@link values}: canonical tokens and values, alternately. */
817
+ #seeds;
818
+ /** `seeds`: canonical tokens and their values, alternately (copied into {@link values} on first use). */
819
+ constructor(canonical, seeds) {
817
820
  this.#canonical = canonical;
821
+ this.#seeds = seeds;
822
+ }
823
+ /** The values of the scope by canonical token: the seeds, then what was set or created in it. */
824
+ get values() {
825
+ let values = this.#values;
826
+ if (values === void 0) {
827
+ values = /* @__PURE__ */ new Map();
828
+ const seeds = this.#seeds;
829
+ if (seeds !== void 0) {
830
+ for (let index = 0; index < seeds.length; index += 2) values.set(seeds[index], seeds[index + 1]);
831
+ this.#seeds = void 0;
832
+ }
833
+ this.#values = values;
834
+ }
835
+ return values;
818
836
  }
819
837
  get(key) {
820
838
  return this.values.get(this.#canonical(key));
@@ -1309,10 +1327,24 @@ var Container = class Container {
1309
1327
  if (errors.length > 0) throw new ZnightAggregateError(ErrorCode.LifecycleFailed, `Failed to dispose ${errors.length} request-scoped instance(s)`, errors);
1310
1328
  return result;
1311
1329
  }
1330
+ /**
1331
+ * Opens a request scope seeded with `values`, like {@link runInScope}, but without running
1332
+ * anything: the caller runs code in it (`run`) and closes it (`close`) when the work it started
1333
+ * is over. Request-scoped providers resolved in it are disposed by `close`, which must be called.
1334
+ */
1335
+ openScope(values) {
1336
+ let seeds;
1337
+ if (values !== void 0) {
1338
+ seeds = [];
1339
+ for (const [key, value] of values) seeds.push(this.#canonical(key), value);
1340
+ }
1341
+ return new ScopeHandle(this.#storage, new RequestScope(this.#canonical, seeds), this.#closeScopeFn);
1342
+ }
1312
1343
  /** The active request scope, if any. */
1313
1344
  currentScope() {
1314
1345
  return this.#storage.getStore();
1315
1346
  }
1347
+ #closeScopeFn = (scope) => this.#closeScope(scope);
1316
1348
  async #closeScope(scope) {
1317
1349
  scope.closed = true;
1318
1350
  const errors = [];
@@ -1551,6 +1583,38 @@ function disposeQuietly(instance, method) {
1551
1583
  if (isThenable(result)) result.then(void 0, () => void 0);
1552
1584
  } catch {}
1553
1585
  }
1586
+ /** Implementation of {@link OpenedScope}. */
1587
+ var ScopeHandle = class {
1588
+ #storage;
1589
+ #scope;
1590
+ #dispose;
1591
+ #closed = false;
1592
+ #closing;
1593
+ constructor(storage, scope, dispose) {
1594
+ this.#storage = storage;
1595
+ this.#scope = scope;
1596
+ this.#dispose = dispose;
1597
+ }
1598
+ get context() {
1599
+ return this.#scope;
1600
+ }
1601
+ run(fn, ...args) {
1602
+ return this.#storage.run(this.#scope, fn, ...args);
1603
+ }
1604
+ close() {
1605
+ if (this.#closed) return this.#closing;
1606
+ this.#closed = true;
1607
+ const scope = this.#scope;
1608
+ if (scope.created.length === 0) {
1609
+ scope.closed = true;
1610
+ return;
1611
+ }
1612
+ this.#closing = this.#dispose(scope).then((errors) => {
1613
+ if (errors.length > 0) throw new ZnightAggregateError(ErrorCode.LifecycleFailed, `Failed to dispose ${errors.length} request-scoped instance(s)`, errors);
1614
+ });
1615
+ return this.#closing;
1616
+ }
1617
+ };
1554
1618
  // src/hooks.ts
1555
1619
  const EMPTY = Object.freeze([]);
1556
1620
  /**