di-bag 0.2.0 → 0.4.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.
Files changed (46) hide show
  1. package/AGENTS.md +150 -0
  2. package/README.md +68 -51
  3. package/dist/acquisition-context.d.ts +80 -5
  4. package/dist/acquisition-context.js +30 -2
  5. package/dist/acquisition-mode.d.ts +17 -4
  6. package/dist/acquisition-mode.js +33 -7
  7. package/dist/acquisition.d.ts +13 -2
  8. package/dist/acquisition.js +44 -8
  9. package/dist/alias-types.d.ts +12 -3
  10. package/dist/composition-report.d.ts +3 -2
  11. package/dist/composition.d.ts +8 -2
  12. package/dist/contribution-types.d.ts +26 -18
  13. package/dist/dependency-references.d.ts +16 -4
  14. package/dist/di-bag.d.ts +344 -42
  15. package/dist/di-bag.js +87 -15
  16. package/dist/errors.d.ts +115 -8
  17. package/dist/errors.js +113 -12
  18. package/dist/index.d.ts +6 -6
  19. package/dist/index.js +2 -1
  20. package/dist/inspection.d.ts +21 -5
  21. package/dist/lifetime-types.d.ts +281 -180
  22. package/dist/lifetime.d.ts +4 -1
  23. package/dist/module-types.d.ts +87 -30
  24. package/dist/module.d.ts +18 -4
  25. package/dist/module.js +31 -7
  26. package/dist/observers.d.ts +25 -6
  27. package/dist/plugins.d.ts +17 -4
  28. package/dist/provider-execution.d.ts +38 -2
  29. package/dist/provider-execution.js +145 -17
  30. package/dist/provider.d.ts +41 -10
  31. package/dist/provider.js +1 -0
  32. package/dist/registration.d.ts +8 -2
  33. package/dist/registration.js +4 -1
  34. package/dist/runtime.d.ts +13 -3
  35. package/dist/runtime.js +37 -9
  36. package/dist/scope-types.d.ts +20 -5
  37. package/dist/startup.d.ts +19 -1
  38. package/dist/startup.js +81 -11
  39. package/dist/token-types.d.ts +26 -8
  40. package/dist/tokens.d.ts +10 -2
  41. package/dist/tokens.js +2 -0
  42. package/dist/types.d.ts +69 -23
  43. package/docs/agent/api-card.md +354 -0
  44. package/docs/agent/errors.md +1138 -0
  45. package/docs/agent/recipes.md +403 -0
  46. package/package.json +18 -5
@@ -5,6 +5,19 @@ const errors_1 = require("./errors");
5
5
  const errors_2 = require("./errors");
6
6
  const provider_execution_1 = require("./provider-execution");
7
7
  const acquisition_family_1 = require("./acquisition-family");
8
+ /**
9
+ * The reason a close() without a cause aborts with. Built once at load: an error
10
+ * created inside close() keeps an unformatted stack whose frames retain the
11
+ * closing callbacks, and through them the scope and its graph, on every signal an
12
+ * application kept after the bag closed. It stays a plain `AbortError`, so its
13
+ * legacy numeric `code` is what an automatic abort reason had; the message names
14
+ * the diagnostic.
15
+ */
16
+ const closingReason = (() => {
17
+ const reason = new DOMException((0, errors_1.diagnosticMessage)('DI_BAG_CLOSING', 'bag is closing'), 'AbortError');
18
+ void reason.stack; // format the load-time frames now, so nothing is retained lazily
19
+ return Object.freeze(reason);
20
+ })();
8
21
  /** Mutable, runtime-local attempts. Binding descriptions never carry ownership. */
9
22
  class ScopeAcquisitions {
10
23
  graph;
@@ -21,7 +34,6 @@ class ScopeAcquisitions {
21
34
  state = 'open';
22
35
  closing;
23
36
  controller;
24
- acquisitionContext;
25
37
  cancellationStarted = false;
26
38
  cancellationCause;
27
39
  shared;
@@ -132,20 +144,43 @@ class ScopeAcquisitions {
132
144
  this.closing = Promise.resolve().then(() => {
133
145
  // Every descendant admission gate is closed before abort listeners run.
134
146
  this.cancellationStarted = true;
135
- this.cancellationCause = cause;
136
- this.controller?.abort(cause);
147
+ this.cancellationCause = cause === undefined ? closingReason : cause;
148
+ this.controller?.abort(this.cancellationCause);
137
149
  return this.disposeAll(beforeDispose);
138
150
  });
139
151
  return this.closing;
140
152
  }
141
- getContext() {
142
- if (!this.acquisitionContext) {
153
+ /** Labels of this scope's running disposers and of acquisitions close is still draining. */
154
+ collectProgress(pending, acquiring) {
155
+ for (const attempt of this.attempts.values()) {
156
+ if (attempt.state === 'disposing' || this.retired.has(attempt.id) || attempt.execution.rollingBack)
157
+ pending.push(attempt.label);
158
+ else if (attempt.state === 'creating' || attempt.state === 'pending')
159
+ acquiring.push(attempt.label);
160
+ }
161
+ }
162
+ /** One controller per scope; every acquisition observes the same cancellation. */
163
+ cancellationSignal() {
164
+ if (!this.controller) {
143
165
  this.controller = new AbortController();
144
166
  if (this.cancellationStarted)
145
167
  this.controller.abort(this.cancellationCause);
146
- this.acquisitionContext = Object.freeze({ signal: this.controller.signal });
147
168
  }
148
- return this.acquisitionContext;
169
+ return this.controller.signal;
170
+ }
171
+ /**
172
+ * The signal is scope-wide; deferred cleanup is local to this attempt, so each
173
+ * contextual acquisition receives its own frozen context. The context captures
174
+ * only its disposer stack, never the execution or this scope, so an
175
+ * application that retains it past `close()` retains nothing else. Built here
176
+ * rather than in `resolveBinding` for the same reason: every closure of a
177
+ * function shares one scope, and a factory can retain the dependency proxy.
178
+ */
179
+ acquisitionContext(disposers) {
180
+ return Object.freeze({
181
+ signal: this.cancellationSignal(),
182
+ pushDisposer: (disposer) => { disposers.push(disposer); },
183
+ });
149
184
  }
150
185
  resolveBinding(bindingId, from, path = []) {
151
186
  // Sharing an alias borrows its lexical parent graph before following targets.
@@ -262,6 +297,7 @@ class ScopeAcquisitions {
262
297
  this.observeAttempt(attempt, 'acquisition-started');
263
298
  this.family.enter(attempt);
264
299
  const directSource = !description.contextual && !description.operations.length;
300
+ const { disposers } = execution;
265
301
  try {
266
302
  let value;
267
303
  if (directSource) {
@@ -270,7 +306,7 @@ class ScopeAcquisitions {
270
306
  execution.publishSource(value, description);
271
307
  }
272
308
  else {
273
- value = execution.evaluate(description, deps, () => this.getContext());
309
+ value = execution.evaluate(description, deps, disposers && this.acquisitionContext(disposers));
274
310
  }
275
311
  attempt.exposed = value;
276
312
  attempt.state = attempt.execution.state;
@@ -6,14 +6,23 @@ import type { Singleton, Unsatisfied } from './types';
6
6
  export type AliasSelection = string | TokenBase;
7
7
  export type AliasAdmission<T> = Singleton<T> extends true ? unknown : ValidToken<T> extends true ? unknown : Unsatisfied<'alias requires one singleton name or genuine token', {}>;
8
8
  export type AliasTarget<R extends Registrations, T> = T extends string ? T extends keyof R ? unknown : Unsatisfied<'alias requires an existing named target', {}> : [WrongToken<T, R>] extends [never] ? unknown : Unsatisfied<'token dependency has an incompatible or opaque contract', {}>;
9
- /** Resolve the service type exposed by a possible alias target. */
9
+ /**
10
+ * Resolve the service type exposed by a possible alias target.
11
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#give-a-dependency-another-lookup-name
12
+ */
10
13
  export type AliasOutput<R extends Registrations, T> = T extends string ? T extends keyof R ? ProviderOutput<R[T]> : never : TokenService<T>;
11
14
  export type AliasDestination<R extends Registrations, D, T> = D extends TokenBase ? [AliasOutput<R, T>] extends [TokenService<D>] ? unknown : Unsatisfied<'alias output is not assignable to destination service', {}> : unknown;
12
- /** A provider contract that forwards a destination to a canonical target acquisition. */
15
+ /**
16
+ * A provider contract that forwards a destination to a canonical target acquisition.
17
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#give-a-dependency-another-lookup-name
18
+ */
13
19
  export type AliasRegistration<R extends Registrations, D, T> = Provider<(this: void, deps: T extends string ? Record<T, AliasOutput<R, T>> : Record<never, never>) => AliasOutput<R, T>, Readonly<object>, readonly unknown[], TokenDependencyContract<T extends TokenBase ? readonly [T] : readonly [], D extends TokenBase ? D : never> & {
14
20
  readonly alias: SelectionKey<T>;
15
21
  }, unknown>;
16
- /** The single registration-map entry introduced by an alias operation. */
22
+ /**
23
+ * The single registration-map entry introduced by an alias operation.
24
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#give-a-dependency-another-lookup-name
25
+ */
17
26
  export type AliasEntries<R extends Registrations, D, T> = Record<SelectionKey<D>, AliasRegistration<R, D, T>>;
18
27
  /** Reflected generic methods cannot introduce a checked singleton destination. */
19
28
  export type AliasEntry<R extends Registrations, D, T> = unknown extends AliasAdmission<D> & AliasAdmission<T> ? {
@@ -1,13 +1,14 @@
1
1
  import type { Builder } from './di-bag';
2
2
  import type { CheckedLifetimes } from './lifetime-types';
3
3
  import type { CheckedConstraints, CompleteConstraints, NeedConstraint } from './module-types';
4
- import type { CheckDependencyCompatibility, CheckDependencyCompleteness, Entry, RegistrationsFromEntries } from './types';
4
+ import type { CheckDependencyCompatibility, CheckDependencyCompleteness, ConsumerReport, Entry, RegistrationsFromEntries } from './types';
5
5
  type ReportOf<Check> = unknown extends Check ? never : Check;
6
- type Reports<E extends Entry, C extends NeedConstraint> = ReportOf<CheckDependencyCompatibility<RegistrationsFromEntries<E>>> | ReportOf<CheckDependencyCompleteness<RegistrationsFromEntries<E>>> | ReportOf<CheckedConstraints<C, RegistrationsFromEntries<E>>> | ReportOf<CompleteConstraints<C, RegistrationsFromEntries<E>>> | ReportOf<CheckedLifetimes<RegistrationsFromEntries<E>, C>>;
6
+ type Reports<E extends Entry, C extends NeedConstraint> = ReportOf<ConsumerReport<CheckDependencyCompatibility<RegistrationsFromEntries<E>>>> | ReportOf<CheckDependencyCompleteness<RegistrationsFromEntries<E>>> | ReportOf<ConsumerReport<CheckedConstraints<C, RegistrationsFromEntries<E>>>> | ReportOf<CompleteConstraints<C, RegistrationsFromEntries<E>>> | ReportOf<CheckedLifetimes<RegistrationsFromEntries<E>, C>>;
7
7
  /**
8
8
  * The compile-time verdict for a builder: `void` when `build()` would be accepted,
9
9
  * otherwise the same failure `build()` reports, including its details.
10
10
  * Read it through `builder.verifyGraph() satisfies void;` or as `CompositionReport<typeof builder>`.
11
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#read-compile-time-rejections
11
12
  */
12
13
  export type CompositionReport<B> = B extends Builder<infer E, infer C> ? [Reports<E, C>] extends [never] ? void : Reports<E, C> : never;
13
14
  export {};
@@ -4,12 +4,18 @@ import type { DependencyReference } from './dependency-references';
4
4
  import type { TokenArguments, ReferenceGraph, DependencyTupleAdmission } from './token-types';
5
5
  import type { Unsatisfied } from './types';
6
6
  type OutputFactory<O> = () => O;
7
- /** Compile-time admission that checks supplied token values against a callable's parameter tuple. */
7
+ /**
8
+ * Compile-time admission that checks supplied token values against a callable's parameter tuple.
9
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#adapt-classes-and-positional-functions
10
+ */
8
11
  export type CompositionArguments<A extends readonly unknown[], P extends readonly unknown[]> = [A] extends [P] ? unknown : Unsatisfied<'composition arguments must match the declared parameter tuple', {
9
12
  supplied: A;
10
13
  parameters: P;
11
14
  }>;
12
- /** A receiver-free positional callback matching the values supplied by a dependency tuple. */
15
+ /**
16
+ * A receiver-free positional callback matching the values supplied by a dependency tuple.
17
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#adapt-classes-and-positional-functions
18
+ */
13
19
  export type CompositionFunction<T extends readonly DependencyReference[], O = unknown> = TokenArguments<T> extends [...infer A] ? (this: void, ...args: A) => O : never;
14
20
  /**
15
21
  * Adapt a positional function without awaiting its arguments or return value.
@@ -1,21 +1,25 @@
1
1
  import type { Registration, Registrations } from './registration';
2
2
  import type { TokenBase, TokenKey, TokenService } from './tokens';
3
3
  import type { ValidToken, TokenTupleAdmission, BindingOutput } from './token-types';
4
- import type { CheckDependencyCompatibility, CheckDependencyCompleteness, Unsatisfied, Entry, RegistrationsFromEntries } from './types';
4
+ import type { CheckDependencyCompatibility, CheckDependencyCompleteness, SeeErrors, Unsatisfied, Entry, RegistrationsFromEntries } from './types';
5
5
  import type { ProviderCollectionTokens } from './provider';
6
6
  import type { Module } from './module';
7
7
  import type { RegistrationConstraints, PublicProvider, NeedConstraint, CheckedConstraints } from './module-types';
8
- import type { LexicalContext, ModuleScope, Enclosed, RenamedContext } from './lifetime-types';
9
8
  declare const contributionSite: unique symbol;
10
- /** Each union member retains one independently checked provider and its group. */
11
- export type Contribution<T extends TokenBase = TokenBase, V extends Registration = Registration, L = undefined> = {
9
+ /**
10
+ * Each union member retains one independently checked provider and its group.
11
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#compose-an-ordered-collection
12
+ */
13
+ export type Contribution<T extends TokenBase = TokenBase, V extends Registration = Registration> = {
12
14
  readonly kind: 'contribution';
13
15
  readonly token: T;
14
16
  readonly registration: V;
15
- readonly context: L;
16
17
  };
17
- /** The erased contribution contract retained by checked builders and modules. */
18
- export type ContributionConstraint = Contribution<TokenBase, Registration, unknown>;
18
+ /**
19
+ * The erased contribution contract retained by checked builders and modules.
20
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#compose-an-ordered-collection
21
+ */
22
+ export type ContributionConstraint = Contribution<TokenBase, Registration>;
19
23
  type Groups<C> = Extract<C, ContributionConstraint>;
20
24
  type Same<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false;
21
25
  type WrongMember<T, G> = G extends ContributionConstraint ? TokenKey<T> extends TokenKey<G['token']> ? Same<T, G['token']> extends true ? never : TokenKey<T> : never : never;
@@ -33,25 +37,29 @@ type AllNeeds<C, A extends Registrations> = ProviderCollectionTokens<A[keyof A]>
33
37
  token: infer T;
34
38
  } ? T : never);
35
39
  type GroupErrors<C, A extends Registrations> = WrongGroup<Groups<C>['token'] | AllNeeds<C, A>, C>;
36
- export type CheckedContributions<C, A extends Registrations> = [Groups<C>] extends [never] ? unknown : [GroupErrors<C, A>] extends [never] ? [WrongProvider<C, A>] extends [never] ? unknown : Unsatisfied<'contribution service is incompatible with its consumer dependency contract', {
40
+ export type CheckedContributions<C, A extends Registrations> = [Groups<C>] extends [never] ? unknown : [GroupErrors<C, A>] extends [never] ? [WrongProvider<C, A>] extends [never] ? unknown : Unsatisfied<`contribution service is incompatible with its consumer dependency contract${SeeErrors<'unsatisfied-consumer'>}`, {
37
41
  readonly failures: ContributionFailures<WrongProvider<C, A>, A>;
38
42
  }> : Unsatisfied<'collection token has an incompatible or opaque contract', {}>;
39
- export type CompleteContributions<C, A extends Registrations> = [MissingProvider<C, A>] extends [never] ? unknown : Unsatisfied<'required service registrations are missing', {
43
+ export type CompleteContributions<C, A extends Registrations> = [MissingProvider<C, A>] extends [never] ? unknown : Unsatisfied<`required service registrations are missing${SeeErrors<'missing-service'>}`, {
40
44
  readonly contributions: MissingProvider<C, A>;
41
45
  }>;
42
46
  /**
43
- * Retain a contribution's provider checks and lexical private-service context when
44
- * its builder seals. A contribution retained from an inner installation is already
45
- * projected; sealing only encloses its scope in this module's scope.
47
+ * Retain a contribution's projected provider and its checked needs when its builder seals.
48
+ * Lifetime reach is retained separately as compact obligations. A contribution retained
49
+ * from an inner installation is already projected and has no needs left to re-scope.
50
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#compose-an-ordered-collection
51
+ */
52
+ export type ModuleContributionConstraints<C, R extends Registrations, P extends keyof R> = C extends ContributionConstraint ? Contribution<C['token'], PublicProvider<C['registration']>> | RegistrationConstraints<C['registration'], R, P> : never;
53
+ /**
54
+ * Project a module's typed-token collections as readonly service arrays.
55
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#compose-an-ordered-collection
46
56
  */
47
- export type ModuleContributionConstraints<C, R extends Registrations, P extends keyof R> = C extends ContributionConstraint ? C['context'] extends LexicalContext ? Contribution<C['token'], C['registration'], Enclosed<C['context'], ModuleScope<R, P>>> : Contribution<C['token'], PublicProvider<C['registration']>, ModuleScope<R, P> & {
48
- readonly registration: C['registration'];
49
- }> | RegistrationConstraints<C['registration'], R, P> : never;
50
- export type RenamedContribution<C extends ContributionConstraint, Old extends string, New extends string> = C['context'] extends LexicalContext ? Contribution<C['token'], C['registration'], RenamedContext<C['context'], Old, New>> : C;
51
- /** Project a module's typed-token collections as readonly service arrays. */
52
57
  export type ModuleContributions<M> = M extends Module<infer _P, infer _R, infer C, infer _D> ? Readonly<{
53
58
  [T in Groups<C>['token'] as TokenKey<T>]: ReadonlyArray<TokenService<T>>;
54
59
  }> : never;
55
- /** The checked generic `contribute` callable exposed by a builder. */
60
+ /**
61
+ * The checked generic `contribute` callable exposed by a builder.
62
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#compose-an-ordered-collection
63
+ */
56
64
  export type BuilderContribute<E extends Entry, C extends NeedConstraint> = <T extends TokenBase, V extends Registration>(token: T & TokenTupleAdmission<readonly [T]>, registration: V & Registration & BindingOutput<NoInfer<T>, NoInfer<V>> & CheckedConstraints<C | Contribution<NoInfer<T>, NoInfer<V>>, RegistrationsFromEntries<E>>, ...invalid: [T] extends [never] ? [never] : [V] extends [never] ? [never] : []) => import('./di-bag').Builder<E, C | Contribution<T, V>>;
57
65
  export {};
@@ -7,13 +7,25 @@ declare class ReferenceBase {
7
7
  declare class DependencyHandle<T extends TokenBase, K extends 'optional' | 'lazy' | 'all'> extends ReferenceBase {
8
8
  readonly [referenceInvariant]: (value: [T, K]) => [T, K];
9
9
  }
10
- /** A positional dependency that yields the token service or `undefined` when unbound. */
10
+ /**
11
+ * A positional dependency that yields the token service or `undefined` when unbound.
12
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#declare-optional-and-lazy-dependencies
13
+ */
11
14
  export type OptionalDependency<T extends TokenBase> = DependencyHandle<T, 'optional'>;
12
- /** A positional dependency that yields all contributions for a token as a readonly array. */
15
+ /**
16
+ * A positional dependency that yields all contributions for a token as a readonly array.
17
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#compose-an-ordered-collection
18
+ */
13
19
  export type CollectionDependency<T extends TokenBase> = DependencyHandle<T, 'all'>;
14
- /** A positional dependency that yields a function which resolves the token on demand. */
20
+ /**
21
+ * A positional dependency that yields a function which resolves the token on demand.
22
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#declare-optional-and-lazy-dependencies
23
+ */
15
24
  export type LazyDependency<T extends TokenBase> = DependencyHandle<T, 'lazy'>;
16
- /** A typed token or one of the positional dependency-reference handles. */
25
+ /**
26
+ * A typed token or one of the positional dependency-reference handles.
27
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#adapt-classes-and-positional-functions
28
+ */
17
29
  export type DependencyReference = TokenBase | ReferenceBase;
18
30
  type ReferenceParts<R> = R extends {
19
31
  readonly [referenceInvariant]: (...args: never[]) => [infer T extends TokenBase, infer K];