@znight/core 0.6.1 → 0.6.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/README.md +20 -11
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +42 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/application.ts +20 -11
- package/src/container.ts +26 -10
- package/src/scope.ts +4 -1
package/package.json
CHANGED
package/src/application.ts
CHANGED
|
@@ -339,11 +339,12 @@ export class Application {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
|
-
* Stops the application (idempotent; waits for an in-progress `start()
|
|
343
|
-
*
|
|
344
|
-
* plugins' `
|
|
345
|
-
*
|
|
346
|
-
*
|
|
342
|
+
* Stops the application (idempotent; waits for an in-progress `start()`, as a step bounded like
|
|
343
|
+
* the others: past it, the remaining start hooks are skipped and what completed its `onStart` is
|
|
344
|
+
* stopped): plugins' `onStop` (reverse) → providers' `onStop` + disposal (reverse resolution
|
|
345
|
+
* order) → plugins' `onDispose` (reverse). Every step is attempted, each within `hookTimeoutMs`
|
|
346
|
+
* and all within `timeoutMs`; failures are collected into a ZnightAggregateError (ZNIGHT021).
|
|
347
|
+
* Signal handlers are removed.
|
|
347
348
|
*
|
|
348
349
|
* Called from a start hook (`onStart`/`onReady` of a provider or plugin, or work it awaits), it
|
|
349
350
|
* does not wait for the start, which waits for that hook: the shutdown runs right away, stopping
|
|
@@ -359,13 +360,23 @@ export class Application {
|
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
async #runStop(): Promise<void> {
|
|
362
|
-
|
|
363
|
+
const errors: unknown[] = []
|
|
364
|
+
const step = this.#stepRunner(errors)
|
|
365
|
+
if (this.#state === 'starting' && !this.#startAborted) {
|
|
366
|
+
// a start hook may never settle: waiting for the start is a step of the shutdown
|
|
367
|
+
await step('Waiting for start()', () => this.#waitForStart())
|
|
368
|
+
if (this.#state === 'starting') {
|
|
369
|
+
// timed out: the remaining start hooks are skipped, what completed its onStart is stopped
|
|
370
|
+
this.#startAborted = true
|
|
371
|
+
this.#releaseStopWait = undefined
|
|
372
|
+
}
|
|
373
|
+
}
|
|
363
374
|
if (this.#state === 'failed' || this.#state === 'stopped') {
|
|
364
375
|
this.#removeSignals()
|
|
365
376
|
return
|
|
366
377
|
}
|
|
367
378
|
this.#state = 'stopping'
|
|
368
|
-
|
|
379
|
+
await this.#shutdown(errors, step)
|
|
369
380
|
this.#removeSignals()
|
|
370
381
|
this.#state = 'stopped'
|
|
371
382
|
if (errors.length > 0) {
|
|
@@ -395,11 +406,9 @@ export class Application {
|
|
|
395
406
|
|
|
396
407
|
/**
|
|
397
408
|
* The three shutdown phases (also used to roll back a failed create/start: only what was started
|
|
398
|
-
* is stopped). Never throws: returns the errors of the failed steps.
|
|
409
|
+
* is stopped). Never throws: returns the errors of the failed steps (appended to `errors`).
|
|
399
410
|
*/
|
|
400
|
-
async #shutdown(): Promise<unknown[]> {
|
|
401
|
-
const errors: unknown[] = []
|
|
402
|
-
const step = this.#stepRunner(errors)
|
|
411
|
+
async #shutdown(errors: unknown[] = [], step = this.#stepRunner(errors)): Promise<unknown[]> {
|
|
403
412
|
for (const plugin of [...this.#startedPlugins].reverse()) {
|
|
404
413
|
if (typeof plugin.onStop === 'function') {
|
|
405
414
|
await step(`Plugin "${plugin.name}" onStop()`, () => plugin.onStop?.(this.#context))
|
package/src/container.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
isMissing,
|
|
32
32
|
missingProvidersError,
|
|
33
33
|
} from './graph.ts'
|
|
34
|
-
import { disposeInstance } from './lifecycle.ts'
|
|
34
|
+
import { disposeInstance, isDisposable } from './lifecycle.ts'
|
|
35
35
|
import {
|
|
36
36
|
type ExternalDefinition,
|
|
37
37
|
type Lazy,
|
|
@@ -160,6 +160,11 @@ export class Container {
|
|
|
160
160
|
readonly #storage = new AsyncLocalStorage<RequestScope>()
|
|
161
161
|
/** Instantiated singletons, in creation order. */
|
|
162
162
|
readonly #created: Slot[] = []
|
|
163
|
+
/**
|
|
164
|
+
* Request-scoped and transient slots whose factory is running. Creation is synchronous, so a slot
|
|
165
|
+
* found here again was requested by its own construction (a `Lazy<T>.get()` in a constructor).
|
|
166
|
+
*/
|
|
167
|
+
readonly #constructing = new Set<Slot>()
|
|
163
168
|
#order: readonly Slot[] = []
|
|
164
169
|
#names: Map<string, Slot[]> | undefined
|
|
165
170
|
#status: Status = 'open'
|
|
@@ -574,22 +579,31 @@ export class Container {
|
|
|
574
579
|
/** Instantiates a request-scoped or transient provider (synchronous factories only). */
|
|
575
580
|
#create(slot: Slot): unknown {
|
|
576
581
|
const definition = slot.definition as ProviderDefinition
|
|
582
|
+
const constructing = this.#constructing
|
|
583
|
+
if (constructing.has(slot)) throw reentrantCreationError(slot)
|
|
584
|
+
constructing.add(slot)
|
|
577
585
|
let deps: unknown[]
|
|
578
|
-
try {
|
|
579
|
-
deps = this.#resolveDeps(slot)
|
|
580
|
-
} catch (error) {
|
|
581
|
-
throw error instanceof InstantiationError ? error.withParents([slot.token]) : error
|
|
582
|
-
}
|
|
583
586
|
let instance: unknown
|
|
584
587
|
try {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
+
try {
|
|
589
|
+
deps = this.#resolveDeps(slot)
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw error instanceof InstantiationError ? error.withParents([slot.token]) : error
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
instance = definition.factory(deps)
|
|
595
|
+
} catch (error) {
|
|
596
|
+
throw new InstantiationError(slot.token, [slot.token], error, definition.source)
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
constructing.delete(slot)
|
|
588
600
|
}
|
|
589
601
|
if (definition.interceptors !== undefined) {
|
|
590
602
|
try {
|
|
591
603
|
applyInterceptors(instance as object, definition.interceptors, deps, slot.displayName)
|
|
592
604
|
} catch (error) {
|
|
605
|
+
// the instance is never handed out nor tracked by a scope: release it now
|
|
606
|
+
disposeQuietly(instance, definition.dispose)
|
|
593
607
|
throw new InstantiationError(slot.token, [slot.token], error, definition.source)
|
|
594
608
|
}
|
|
595
609
|
}
|
|
@@ -606,7 +620,9 @@ export class Container {
|
|
|
606
620
|
this.#assertResolvable(slot)
|
|
607
621
|
const instance = this.#create(slot)
|
|
608
622
|
values.set(slot.token, instance)
|
|
609
|
-
|
|
623
|
+
// only what has something to dispose is tracked: a scope without any closes synchronously
|
|
624
|
+
const dispose = slot.definition?.dispose
|
|
625
|
+
if (isDisposable(instance, dispose)) scope.created.push({ token: slot.token, instance, dispose })
|
|
610
626
|
return instance
|
|
611
627
|
}
|
|
612
628
|
|
package/src/scope.ts
CHANGED
|
@@ -17,7 +17,10 @@ export interface ScopeContext {
|
|
|
17
17
|
readonly closed: boolean
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* A request-scoped instance created by the container that has something to dispose (a dispose
|
|
22
|
+
* method or `[Symbol.asyncDispose]`/`[Symbol.dispose]`), disposed when its scope closes.
|
|
23
|
+
*/
|
|
21
24
|
export interface ScopedInstance {
|
|
22
25
|
readonly token: Token
|
|
23
26
|
readonly instance: unknown
|