@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znight/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Znight core runtime: compile-time DI container, lifecycle, plugins and marker decorators",
5
5
  "license": "MIT",
6
6
  "author": "Ugho STEPHAN",
package/src/container.ts CHANGED
@@ -93,6 +93,30 @@ export interface ManagedInstance {
93
93
  /** Seed values of a request scope: token-keyed pairs (aliases and known classes are accepted). */
94
94
  export type ScopeValues = Iterable<readonly [ProviderKey, unknown]>
95
95
 
96
+ /**
97
+ * A request scope opened with {@link Container.openScope}: its owner runs code in it with
98
+ * {@link OpenedScope.run} and ends it with {@link OpenedScope.close}. The callback-free form of
99
+ * {@link Container.runInScope}, for hot paths that stay synchronous as long as their work is (the
100
+ * HTTP pipeline): no promise is created unless something is asynchronous.
101
+ */
102
+ export interface OpenedScope {
103
+ /** The values of the scope (what `currentScope()` returns in {@link run}). */
104
+ readonly context: ScopeContext
105
+ /**
106
+ * Calls `fn(...args)` in the scope and returns its result. The code it runs and the asynchronous
107
+ * continuations it creates (awaits, promise callbacks, timers) see the scope, like the callback
108
+ * of `runInScope`. May be called several times until the scope is closed.
109
+ */
110
+ run<A extends readonly unknown[], R>(fn: (...args: A) => R, ...args: A): R
111
+ /**
112
+ * Closes the scope (a later resolution of a request-scoped provider in it throws ZNIGHT013) and
113
+ * disposes the instances created in it, in reverse creation order. Returns `undefined` when no
114
+ * instance was created (nothing to wait for), otherwise a promise rejecting with ZNIGHT021 when a
115
+ * disposal fails. Idempotent: later calls return the first result.
116
+ */
117
+ close(): Promise<void> | undefined
118
+ }
119
+
96
120
  type Status = 'open' | 'initializing' | 'ready' | 'failed' | 'disposed'
97
121
 
98
122
  interface InitRun {
@@ -706,11 +730,27 @@ export class Container {
706
730
  return result
707
731
  }
708
732
 
733
+ /**
734
+ * Opens a request scope seeded with `values`, like {@link runInScope}, but without running
735
+ * anything: the caller runs code in it (`run`) and closes it (`close`) when the work it started
736
+ * is over. Request-scoped providers resolved in it are disposed by `close`, which must be called.
737
+ */
738
+ openScope(values?: ScopeValues): OpenedScope {
739
+ let seeds: unknown[] | undefined
740
+ if (values !== undefined) {
741
+ seeds = []
742
+ for (const [key, value] of values) seeds.push(this.#canonical(key), value)
743
+ }
744
+ return new ScopeHandle(this.#storage, new RequestScope(this.#canonical, seeds), this.#closeScopeFn)
745
+ }
746
+
709
747
  /** The active request scope, if any. */
710
748
  currentScope(): ScopeContext | undefined {
711
749
  return this.#storage.getStore()
712
750
  }
713
751
 
752
+ readonly #closeScopeFn = (scope: RequestScope): Promise<unknown[]> => this.#closeScope(scope)
753
+
714
754
  async #closeScope(scope: RequestScope): Promise<unknown[]> {
715
755
  scope.closed = true
716
756
  const errors: unknown[] = []
@@ -1011,3 +1051,50 @@ function disposeQuietly(instance: unknown, method: string | undefined): void {
1011
1051
  // best effort
1012
1052
  }
1013
1053
  }
1054
+
1055
+ /** Implementation of {@link OpenedScope}. */
1056
+ class ScopeHandle implements OpenedScope {
1057
+ readonly #storage: AsyncLocalStorage<RequestScope>
1058
+ readonly #scope: RequestScope
1059
+ readonly #dispose: (scope: RequestScope) => Promise<unknown[]>
1060
+ #closed = false
1061
+ #closing: Promise<void> | undefined
1062
+
1063
+ constructor(
1064
+ storage: AsyncLocalStorage<RequestScope>,
1065
+ scope: RequestScope,
1066
+ dispose: (scope: RequestScope) => Promise<unknown[]>,
1067
+ ) {
1068
+ this.#storage = storage
1069
+ this.#scope = scope
1070
+ this.#dispose = dispose
1071
+ }
1072
+
1073
+ get context(): ScopeContext {
1074
+ return this.#scope
1075
+ }
1076
+
1077
+ run<A extends readonly unknown[], R>(fn: (...args: A) => R, ...args: A): R {
1078
+ return this.#storage.run(this.#scope, fn, ...args)
1079
+ }
1080
+
1081
+ close(): Promise<void> | undefined {
1082
+ if (this.#closed) return this.#closing
1083
+ this.#closed = true
1084
+ const scope = this.#scope
1085
+ if (scope.created.length === 0) {
1086
+ scope.closed = true
1087
+ return undefined
1088
+ }
1089
+ this.#closing = this.#dispose(scope).then(errors => {
1090
+ if (errors.length > 0) {
1091
+ throw new ZnightAggregateError(
1092
+ ErrorCode.LifecycleFailed,
1093
+ `Failed to dispose ${errors.length} request-scoped instance(s)`,
1094
+ errors,
1095
+ )
1096
+ }
1097
+ })
1098
+ return this.#closing
1099
+ }
1100
+ }
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export {
17
17
  type ProviderValues,
18
18
  type ShutdownOptions,
19
19
  } from './application.ts'
20
- export { Container, type InitOptions, type ManagedInstance, type ScopeValues } from './container.ts'
20
+ export { Container, type InitOptions, type ManagedInstance, type OpenedScope, type ScopeValues } from './container.ts'
21
21
  export {
22
22
  type ClassMarker,
23
23
  Factory,
package/src/scope.ts CHANGED
@@ -26,13 +26,32 @@ export interface ScopedInstance {
26
26
 
27
27
  /** Implementation of {@link ScopeContext} used by the container (internal). */
28
28
  export class RequestScope implements ScopeContext {
29
- readonly values: Map<Token, unknown> = new Map()
30
29
  readonly created: ScopedInstance[] = []
31
30
  closed = false
32
31
  readonly #canonical: (key: ProviderKey) => Token
32
+ #values: Map<Token, unknown> | undefined
33
+ /** Seed values not yet in {@link values}: canonical tokens and values, alternately. */
34
+ #seeds: readonly unknown[] | undefined
33
35
 
34
- constructor(canonical: (key: ProviderKey) => Token) {
36
+ /** `seeds`: canonical tokens and their values, alternately (copied into {@link values} on first use). */
37
+ constructor(canonical: (key: ProviderKey) => Token, seeds?: readonly unknown[]) {
35
38
  this.#canonical = canonical
39
+ this.#seeds = seeds
40
+ }
41
+
42
+ /** The values of the scope by canonical token: the seeds, then what was set or created in it. */
43
+ get values(): Map<Token, unknown> {
44
+ let values = this.#values
45
+ if (values === undefined) {
46
+ values = new Map()
47
+ const seeds = this.#seeds
48
+ if (seeds !== undefined) {
49
+ for (let index = 0; index < seeds.length; index += 2) values.set(seeds[index] as Token, seeds[index + 1])
50
+ this.#seeds = undefined
51
+ }
52
+ this.#values = values
53
+ }
54
+ return values
36
55
  }
37
56
 
38
57
  get<T = unknown>(key: ProviderKey<T>): T | undefined {