@setu-ts/kernel 0.0.0-bootstrap.0 → 0.9.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -2
  3. package/dist/application/application.d.ts +172 -0
  4. package/dist/application/application.d.ts.map +1 -0
  5. package/dist/application/application.js +1862 -0
  6. package/dist/application/application.js.map +1 -0
  7. package/dist/application/inject-body.d.ts +58 -0
  8. package/dist/application/inject-body.d.ts.map +1 -0
  9. package/dist/application/inject-body.js +82 -0
  10. package/dist/application/inject-body.js.map +1 -0
  11. package/dist/context/request-context.d.ts +32 -0
  12. package/dist/context/request-context.d.ts.map +1 -0
  13. package/dist/context/request-context.js +136 -0
  14. package/dist/context/request-context.js.map +1 -0
  15. package/dist/context/response.d.ts +60 -0
  16. package/dist/context/response.d.ts.map +1 -0
  17. package/dist/context/response.js +193 -0
  18. package/dist/context/response.js.map +1 -0
  19. package/dist/diagnostics/buffer.d.ts +143 -0
  20. package/dist/diagnostics/buffer.d.ts.map +1 -0
  21. package/dist/diagnostics/buffer.js +185 -0
  22. package/dist/diagnostics/buffer.js.map +1 -0
  23. package/dist/diagnostics/collector.d.ts +284 -0
  24. package/dist/diagnostics/collector.d.ts.map +1 -0
  25. package/dist/diagnostics/collector.js +812 -0
  26. package/dist/diagnostics/collector.js.map +1 -0
  27. package/dist/diagnostics/projection.d.ts +153 -0
  28. package/dist/diagnostics/projection.d.ts.map +1 -0
  29. package/dist/diagnostics/projection.js +226 -0
  30. package/dist/diagnostics/projection.js.map +1 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +11 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lifecycle/lifecycle-manager.d.ts +136 -0
  36. package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
  37. package/dist/lifecycle/lifecycle-manager.js +206 -0
  38. package/dist/lifecycle/lifecycle-manager.js.map +1 -0
  39. package/dist/pipeline/execute-chain.d.ts +71 -0
  40. package/dist/pipeline/execute-chain.d.ts.map +1 -0
  41. package/dist/pipeline/execute-chain.js +104 -0
  42. package/dist/pipeline/execute-chain.js.map +1 -0
  43. package/dist/pipeline/middleware-pipeline.d.ts +68 -0
  44. package/dist/pipeline/middleware-pipeline.d.ts.map +1 -0
  45. package/dist/pipeline/middleware-pipeline.js +82 -0
  46. package/dist/pipeline/middleware-pipeline.js.map +1 -0
  47. package/dist/registry/plugin-resolver.d.ts +42 -0
  48. package/dist/registry/plugin-resolver.d.ts.map +1 -0
  49. package/dist/registry/plugin-resolver.js +140 -0
  50. package/dist/registry/plugin-resolver.js.map +1 -0
  51. package/dist/registry/service-registry.d.ts +110 -0
  52. package/dist/registry/service-registry.d.ts.map +1 -0
  53. package/dist/registry/service-registry.js +197 -0
  54. package/dist/registry/service-registry.js.map +1 -0
  55. package/dist/router/route-matcher.d.ts +85 -0
  56. package/dist/router/route-matcher.d.ts.map +1 -0
  57. package/dist/router/route-matcher.js +102 -0
  58. package/dist/router/route-matcher.js.map +1 -0
  59. package/dist/router/router.d.ts +130 -0
  60. package/dist/router/router.d.ts.map +1 -0
  61. package/dist/router/router.js +350 -0
  62. package/dist/router/router.js.map +1 -0
  63. package/package.json +40 -3
  64. package/src/application/application.ts +2299 -0
  65. package/src/application/inject-body.ts +131 -0
  66. package/src/context/request-context.ts +169 -0
  67. package/src/context/response.ts +254 -0
  68. package/src/diagnostics/buffer.ts +241 -0
  69. package/src/diagnostics/collector.ts +1031 -0
  70. package/src/diagnostics/projection.ts +322 -0
  71. package/src/index.ts +20 -0
  72. package/src/lifecycle/lifecycle-manager.ts +287 -0
  73. package/src/pipeline/execute-chain.ts +164 -0
  74. package/src/pipeline/middleware-pipeline.ts +131 -0
  75. package/src/registry/plugin-resolver.ts +185 -0
  76. package/src/registry/service-registry.ts +267 -0
  77. package/src/router/route-matcher.ts +131 -0
  78. package/src/router/router.ts +485 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Plugin dependency resolution: topological sort with cycle detection,
3
+ * priority ordering, and the mandatory-runtime-first rule
4
+ * (ARCHITECTURE.md §5, §7).
5
+ *
6
+ * @module
7
+ */
8
+ import { CAPABILITIES } from '@setu-ts/common';
9
+ /**
10
+ * Resolves plugin registration order.
11
+ *
12
+ * Order is determined by: dependency edges first (a plugin's dependencies
13
+ * register before it), then declared priority (lower first), then original
14
+ * registration order. The plugin providing the `runtime` capability always
15
+ * registers first, regardless of priority.
16
+ */
17
+ export function resolvePluginOrder(plugins) {
18
+ assertUniqueNames(plugins);
19
+ const providers = buildProviderIndex(plugins);
20
+ const runtimeProvider = providers.get(CAPABILITIES.RUNTIME);
21
+ if (runtimeProvider === undefined) {
22
+ throw new Error(`No plugin provides the mandatory '${CAPABILITIES.RUNTIME}' capability. ` +
23
+ `Register a runtime plugin (e.g. RuntimePlugin from @setu-ts/runtime).`);
24
+ }
25
+ const dependenciesOf = (plugin) => {
26
+ const edges = [];
27
+ // Every plugin implicitly depends on the runtime provider so it can rely
28
+ // on ctx.runtime during registration.
29
+ if (plugin !== runtimeProvider) {
30
+ edges.push(runtimeProvider);
31
+ }
32
+ for (const token of plugin.dependencies ?? []) {
33
+ const provider = providers.get(token);
34
+ if (provider === undefined) {
35
+ throw new Error(`Plugin '${plugin.name}' depends on capability '${token}', but no registered plugin provides it.`);
36
+ }
37
+ if (provider !== plugin) {
38
+ edges.push(provider);
39
+ }
40
+ }
41
+ for (const token of plugin.optionalDependencies ?? []) {
42
+ const provider = providers.get(token);
43
+ if (provider !== undefined && provider !== plugin) {
44
+ edges.push(provider);
45
+ }
46
+ }
47
+ return edges;
48
+ };
49
+ return topologicalSort(plugins, dependenciesOf);
50
+ }
51
+ /**
52
+ * Finds every `consumes` capability, across all plugins, that `isProvided`
53
+ * reports as absent.
54
+ *
55
+ * Pure by design: the caller supplies the provider check — in the kernel, the
56
+ * live service registry's `has` after all plugins have registered, so
57
+ * capabilities registered imperatively (not declared in `provides`) still
58
+ * count as satisfied and never produce a false warning.
59
+ *
60
+ * @param plugins - All registered plugins
61
+ * @param isProvided - Predicate: does a provider exist for this capability?
62
+ * @returns One entry per unsatisfied (plugin, capability) pair, in plugin then
63
+ * declaration order
64
+ */
65
+ export function findUnsatisfiedConsumers(plugins, isProvided) {
66
+ const unsatisfied = [];
67
+ for (const plugin of plugins) {
68
+ for (const capability of plugin.consumes ?? []) {
69
+ if (!isProvided(capability)) {
70
+ unsatisfied.push({ plugin: plugin.name, capability });
71
+ }
72
+ }
73
+ }
74
+ return unsatisfied;
75
+ }
76
+ function assertUniqueNames(plugins) {
77
+ const seen = new Set();
78
+ for (const plugin of plugins) {
79
+ if (seen.has(plugin.name)) {
80
+ throw new Error(`Duplicate plugin name '${plugin.name}'. To replace a plugin, register the ` +
81
+ `replacement's services with { override: true } instead of reusing the name.`);
82
+ }
83
+ seen.add(plugin.name);
84
+ }
85
+ }
86
+ /**
87
+ * Indexes which plugin provides each capability token. A plugin implicitly
88
+ * provides its own name, so `dependencies: ['logger-plugin']` also works.
89
+ */
90
+ function buildProviderIndex(plugins) {
91
+ const providers = new Map();
92
+ for (const plugin of plugins) {
93
+ for (const token of [plugin.name, ...(plugin.provides ?? [])]) {
94
+ const existing = providers.get(token);
95
+ if (existing !== undefined && existing !== plugin) {
96
+ throw new Error(`Capability '${token}' is provided by both '${existing.name}' and '${plugin.name}'. ` +
97
+ `Multi-provider capabilities must be registered with { multi: true } at the service level.`);
98
+ }
99
+ providers.set(token, plugin);
100
+ }
101
+ }
102
+ return providers;
103
+ }
104
+ /**
105
+ * Depth-first topological sort. Within the same dependency level, plugins
106
+ * are visited by (priority, registration order), which the DFS preserves in
107
+ * the output.
108
+ */
109
+ function topologicalSort(plugins, dependenciesOf) {
110
+ const DEFAULT_PRIORITY = 500;
111
+ const seeds = plugins
112
+ .map((plugin, index) => ({ plugin, index }))
113
+ .sort((a, b) => (a.plugin.priority ?? DEFAULT_PRIORITY) - (b.plugin.priority ?? DEFAULT_PRIORITY) ||
114
+ a.index - b.index)
115
+ .map(({ plugin }) => plugin);
116
+ const ordered = [];
117
+ const done = new Set();
118
+ const inProgress = new Set();
119
+ const visit = (plugin, chain) => {
120
+ if (done.has(plugin)) {
121
+ return;
122
+ }
123
+ if (inProgress.has(plugin)) {
124
+ const cycle = [...chain.slice(chain.indexOf(plugin.name)), plugin.name].join(' -> ');
125
+ throw new Error(`Circular plugin dependency detected: ${cycle}`);
126
+ }
127
+ inProgress.add(plugin);
128
+ for (const dependency of dependenciesOf(plugin)) {
129
+ visit(dependency, [...chain, plugin.name]);
130
+ }
131
+ inProgress.delete(plugin);
132
+ done.add(plugin);
133
+ ordered.push(plugin);
134
+ };
135
+ for (const plugin of seeds) {
136
+ visit(plugin, []);
137
+ }
138
+ return ordered;
139
+ }
140
+ //# sourceMappingURL=plugin-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-resolver.js","sourceRoot":"","sources":["../../src/registry/plugin-resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Capability-token service registry — the framework's primary service
3
+ * resolution mechanism (ARCHITECTURE.md §6).
4
+ *
5
+ * @module
6
+ */
7
+ import type { CapabilityToken, IServiceRegistry, RegisterOptions, ServiceFactory } from '@setu-ts/common';
8
+ /**
9
+ * A successful registry mutation reported to the diagnostics collector, using
10
+ * token metadata alone. Duplicate-registration REFUSALS never produce an
11
+ * event: they throw before any mutation happens, and a refused registration
12
+ * is not a successful one.
13
+ *
14
+ * @since 0.8.0
15
+ */
16
+ export type RegistryDiagnosticEvent = {
17
+ readonly kind: 'register-single' | 'register-multi' | 'unregister';
18
+ readonly token: CapabilityToken;
19
+ };
20
+ /**
21
+ * The collector-facing sink for successful registry mutations. Distinct from
22
+ * the startup logging observer, which keeps its own shape and behavior.
23
+ *
24
+ * @since 0.8.0
25
+ */
26
+ export type RegistryDiagnosticsSink = (event: RegistryDiagnosticEvent) => void;
27
+ /**
28
+ * Default {@linkcode IServiceRegistry} implementation. Request scopes are
29
+ * modeled as child registries that fall back to their parent for lookups
30
+ * while keeping their own registrations isolated.
31
+ */
32
+ export declare class ServiceRegistry implements IServiceRegistry {
33
+ #private;
34
+ constructor(parent?: ServiceRegistry);
35
+ /** Creates a request-scoped child registry that falls back to this one. */
36
+ createChild(): ServiceRegistry;
37
+ /** Prevents future application-scoped registrations. */
38
+ seal(): void;
39
+ /**
40
+ * Installs the diagnostics sink that receives successful registration and
41
+ * removal events. Pass `undefined` to detach (teardown).
42
+ *
43
+ * @param sink - The collector-facing sink, or `undefined`
44
+ * @since 0.8.0
45
+ */
46
+ setDiagnosticsSink(sink: RegistryDiagnosticsSink | undefined): void;
47
+ /**
48
+ * Installs the explanation {@linkcode ServiceRegistry.get} gives for a token
49
+ * it cannot find. The application knows what the registry cannot: whether
50
+ * its plugins have registered yet, and which plugin provides a token. When
51
+ * the function returns a string, it replaces the generic advice in the
52
+ * error; `undefined` keeps that advice.
53
+ *
54
+ * @param explain - Explains a missing token, or answers `undefined`
55
+ */
56
+ setMissExplanation(explain: (token: CapabilityToken) => string | undefined): void;
57
+ /**
58
+ * Returns the ALREADY-RESOLVED instance for a token without ever executing
59
+ * a factory, using the same single-registration lookup precedence as
60
+ * {@linkcode ServiceRegistry.get} — own single registration, then own first
61
+ * multi-provider, then the parent. An unresolved LOCAL registration answers
62
+ * `undefined` and does NOT fall past to the parent: the answer "present but
63
+ * never constructed" must not turn into a foreign instance. Multi-providers
64
+ * are never enumerated, nothing is constructed, and the method is absent
65
+ * from {@linkcode IServiceRegistry} by design — it exists so kernel
66
+ * diagnostics can read an optional capability (telemetry identifiers) on the
67
+ * execution path without forcing a lazy service into existence, and it is
68
+ * not part of the public barrel.
69
+ *
70
+ * @typeParam T - The service's interface type
71
+ * @param token - The capability token to peek
72
+ * @returns The cached instance, or `undefined` when absent or unresolved
73
+ * @since 0.8.0
74
+ */
75
+ peekResolved<T extends object>(token: CapabilityToken): T | undefined;
76
+ /**
77
+ * Reports whether `instance` is the CURRENT provider a resolving
78
+ * {@linkcode ServiceRegistry.get} would select for `token` — WITHOUT ever
79
+ * executing a factory.
80
+ *
81
+ * The lookup precedence is IDENTICAL to `get`'s: own single registration,
82
+ * then own first multi-provider, then the parent. The comparison is an
83
+ * identity check against the registration's already-resolved instance. A
84
+ * registration whose instance has not been constructed yet (a lazy factory
85
+ * that has not run) answers `false` — it does NOT fall past to the parent,
86
+ * because the answer "present but never constructed" must not be mistaken
87
+ * for a foreign instance. Nothing is constructed, no multi-provider list is
88
+ * enumerated, and the current value is never returned.
89
+ *
90
+ * This is the non-resolving identity predicate of
91
+ * {@linkcode IServiceRegistry.isCurrent}: a consumer can verify it still
92
+ * owns a known provider without forcing a lazy replacement into existence.
93
+ *
94
+ * @typeParam T - The service's interface type
95
+ * @param token - The capability token to check
96
+ * @param instance - The instance to compare against the current registration
97
+ * @returns `true` only when `instance` IS the registration `get` would select
98
+ * @since 0.8.0
99
+ */
100
+ isCurrent<T extends object>(token: CapabilityToken, instance: T): boolean;
101
+ /** Installs the application-owned callback for startup-time mutations. */
102
+ setObserver(observer: (kind: 'override' | 'unregister', token: CapabilityToken) => void): void;
103
+ register<T extends object>(token: CapabilityToken, service: T, options?: RegisterOptions): void;
104
+ registerFactory<T extends object>(token: CapabilityToken, factory: ServiceFactory<T>, options?: RegisterOptions): void;
105
+ get<T extends object>(token: CapabilityToken): T;
106
+ getAll<T extends object>(token: CapabilityToken): readonly T[];
107
+ has(token: CapabilityToken): boolean;
108
+ unregister(token: CapabilityToken): boolean;
109
+ }
110
+ //# sourceMappingURL=service-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-registry.d.ts","sourceRoot":"","sources":["../../src/registry/service-registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,iBAAiB,CAAC;AAOzB;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,YAAY,CAAC;IACnE,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;AAU/E;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,gBAAgB;;gBAS1C,MAAM,CAAC,EAAE,eAAe;IAIpC,2EAA2E;IAC3E,WAAW,IAAI,eAAe;IAI9B,wDAAwD;IACxD,IAAI,IAAI,IAAI;IAIZ;;;;;;OAMG;IACH,kBAAkB,CAAC,IAAI,EAAE,uBAAuB,GAAG,SAAS,GAAG,IAAI;IAInE;;;;;;;;OAQG;IACH,kBAAkB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;IAIjF;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,CAAC,GAAG,SAAS;IAWrE;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO;IAWzE,0EAA0E;IAC1E,WAAW,CACT,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,EAAE,KAAK,EAAE,eAAe,KAAK,IAAI,GAC1E,IAAI;IAIP,QAAQ,CAAC,CAAC,SAAS,MAAM,EACvB,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,eAAe,GACxB,IAAI;IAIP,eAAe,CAAC,CAAC,SAAS,MAAM,EAC9B,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAC1B,OAAO,CAAC,EAAE,eAAe,GACxB,IAAI;IAIP,GAAG,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,CAAC;IAUhD,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,SAAS,CAAC,EAAE;IAW9D,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;IAIpC,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;CAuD5C"}
@@ -0,0 +1,197 @@
1
+ function resolveRegistration(registration) {
2
+ if (registration.instance === undefined) {
3
+ // Lazy factory: instantiate on first lookup, cache for subsequent ones.
4
+ registration.instance = registration.factory();
5
+ }
6
+ return registration.instance;
7
+ }
8
+ /**
9
+ * Default {@linkcode IServiceRegistry} implementation. Request scopes are
10
+ * modeled as child registries that fall back to their parent for lookups
11
+ * while keeping their own registrations isolated.
12
+ */
13
+ export class ServiceRegistry {
14
+ #single = new Map();
15
+ #multi = new Map();
16
+ #parent;
17
+ #sealed = false;
18
+ #observer;
19
+ #diagnosticsSink;
20
+ #missExplanation;
21
+ constructor(parent) {
22
+ this.#parent = parent;
23
+ }
24
+ /** Creates a request-scoped child registry that falls back to this one. */
25
+ createChild() {
26
+ return new ServiceRegistry(this);
27
+ }
28
+ /** Prevents future application-scoped registrations. */
29
+ seal() {
30
+ this.#sealed = true;
31
+ }
32
+ /**
33
+ * Installs the diagnostics sink that receives successful registration and
34
+ * removal events. Pass `undefined` to detach (teardown).
35
+ *
36
+ * @param sink - The collector-facing sink, or `undefined`
37
+ * @since 0.8.0
38
+ */
39
+ setDiagnosticsSink(sink) {
40
+ this.#diagnosticsSink = sink;
41
+ }
42
+ /**
43
+ * Installs the explanation {@linkcode ServiceRegistry.get} gives for a token
44
+ * it cannot find. The application knows what the registry cannot: whether
45
+ * its plugins have registered yet, and which plugin provides a token. When
46
+ * the function returns a string, it replaces the generic advice in the
47
+ * error; `undefined` keeps that advice.
48
+ *
49
+ * @param explain - Explains a missing token, or answers `undefined`
50
+ */
51
+ setMissExplanation(explain) {
52
+ this.#missExplanation = explain;
53
+ }
54
+ /**
55
+ * Returns the ALREADY-RESOLVED instance for a token without ever executing
56
+ * a factory, using the same single-registration lookup precedence as
57
+ * {@linkcode ServiceRegistry.get} — own single registration, then own first
58
+ * multi-provider, then the parent. An unresolved LOCAL registration answers
59
+ * `undefined` and does NOT fall past to the parent: the answer "present but
60
+ * never constructed" must not turn into a foreign instance. Multi-providers
61
+ * are never enumerated, nothing is constructed, and the method is absent
62
+ * from {@linkcode IServiceRegistry} by design — it exists so kernel
63
+ * diagnostics can read an optional capability (telemetry identifiers) on the
64
+ * execution path without forcing a lazy service into existence, and it is
65
+ * not part of the public barrel.
66
+ *
67
+ * @typeParam T - The service's interface type
68
+ * @param token - The capability token to peek
69
+ * @returns The cached instance, or `undefined` when absent or unresolved
70
+ * @since 0.8.0
71
+ */
72
+ peekResolved(token) {
73
+ const own = this.#single.get(token) ?? this.#multi.get(token)?.[0];
74
+ if (own !== undefined) {
75
+ return own.instance === undefined ? undefined : own.instance;
76
+ }
77
+ if (this.#parent === undefined) {
78
+ return undefined;
79
+ }
80
+ return this.#parent.peekResolved(token);
81
+ }
82
+ /**
83
+ * Reports whether `instance` is the CURRENT provider a resolving
84
+ * {@linkcode ServiceRegistry.get} would select for `token` — WITHOUT ever
85
+ * executing a factory.
86
+ *
87
+ * The lookup precedence is IDENTICAL to `get`'s: own single registration,
88
+ * then own first multi-provider, then the parent. The comparison is an
89
+ * identity check against the registration's already-resolved instance. A
90
+ * registration whose instance has not been constructed yet (a lazy factory
91
+ * that has not run) answers `false` — it does NOT fall past to the parent,
92
+ * because the answer "present but never constructed" must not be mistaken
93
+ * for a foreign instance. Nothing is constructed, no multi-provider list is
94
+ * enumerated, and the current value is never returned.
95
+ *
96
+ * This is the non-resolving identity predicate of
97
+ * {@linkcode IServiceRegistry.isCurrent}: a consumer can verify it still
98
+ * owns a known provider without forcing a lazy replacement into existence.
99
+ *
100
+ * @typeParam T - The service's interface type
101
+ * @param token - The capability token to check
102
+ * @param instance - The instance to compare against the current registration
103
+ * @returns `true` only when `instance` IS the registration `get` would select
104
+ * @since 0.8.0
105
+ */
106
+ isCurrent(token, instance) {
107
+ const own = this.#single.get(token) ?? this.#multi.get(token)?.[0];
108
+ if (own !== undefined) {
109
+ return own.instance === instance;
110
+ }
111
+ if (this.#parent === undefined) {
112
+ return false;
113
+ }
114
+ return this.#parent.isCurrent(token, instance);
115
+ }
116
+ /** Installs the application-owned callback for startup-time mutations. */
117
+ setObserver(observer) {
118
+ this.#observer = observer;
119
+ }
120
+ register(token, service, options) {
121
+ this.#store(token, { instance: service }, options);
122
+ }
123
+ registerFactory(token, factory, options) {
124
+ this.#store(token, { factory }, options);
125
+ }
126
+ get(token) {
127
+ const registration = this.#lookup(token);
128
+ if (registration === undefined) {
129
+ const advice = this.#missExplanation?.(token) ??
130
+ 'Register a plugin that provides it, or check the token spelling against CAPABILITIES.';
131
+ throw new Error(`No service registered for capability '${token}'. ${advice}`);
132
+ }
133
+ return resolveRegistration(registration);
134
+ }
135
+ getAll(token) {
136
+ const own = this.#multi.get(token) ?? [];
137
+ const inherited = this.#parent?.getAll(token) ?? [];
138
+ const single = this.#single.get(token);
139
+ return [
140
+ ...inherited,
141
+ ...(single ? [resolveRegistration(single)] : []),
142
+ ...own.map((registration) => resolveRegistration(registration)),
143
+ ];
144
+ }
145
+ has(token) {
146
+ return this.#single.has(token) || this.#multi.has(token) || (this.#parent?.has(token) ?? false);
147
+ }
148
+ unregister(token) {
149
+ this.#assertMutable(token, 'unregister');
150
+ const hadSingle = this.#single.delete(token);
151
+ const hadMulti = this.#multi.delete(token);
152
+ const removed = hadSingle || hadMulti;
153
+ if (removed) {
154
+ this.#observer?.('unregister', token);
155
+ this.#diagnosticsSink?.({ kind: 'unregister', token });
156
+ }
157
+ return removed;
158
+ }
159
+ #lookup(token) {
160
+ const own = this.#single.get(token) ?? this.#multi.get(token)?.[0];
161
+ if (own !== undefined) {
162
+ return own;
163
+ }
164
+ if (this.#parent === undefined) {
165
+ return undefined;
166
+ }
167
+ return this.#parent.#lookup(token);
168
+ }
169
+ #store(token, registration, options) {
170
+ this.#assertMutable(token, 'register');
171
+ if (options?.multi) {
172
+ const providers = this.#multi.get(token) ?? [];
173
+ providers.push(registration);
174
+ this.#multi.set(token, providers);
175
+ this.#diagnosticsSink?.({ kind: 'register-multi', token });
176
+ return;
177
+ }
178
+ // Conflicts are checked against this registry only: a request-scoped
179
+ // child may deliberately shadow an application-scoped service.
180
+ if (this.#single.has(token) && !options?.override) {
181
+ throw new Error(`Capability '${token}' is already registered. Use { override: true } to replace it.`);
182
+ }
183
+ if (this.#single.has(token) && options?.override) {
184
+ this.#observer?.('override', token);
185
+ }
186
+ this.#single.set(token, registration);
187
+ this.#diagnosticsSink?.({ kind: 'register-single', token });
188
+ }
189
+ #assertMutable(token, operation) {
190
+ if (this.#sealed) {
191
+ throw new Error(`Cannot ${operation} capability '${token}' after runBootstrap() has completed. ` +
192
+ `Register during a plugin's register(), onInit, or onBootstrap hook, or use ` +
193
+ `request-scoped ctx.services inside middleware.`);
194
+ }
195
+ }
196
+ }
197
+ //# sourceMappingURL=service-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-registry.js","sourceRoot":"","sources":["../../src/registry/service-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Route matcher utilities — path decoding guard and pattern-parsing helpers
3
+ * for the kernel router.
4
+ *
5
+ * As of Milestone 22, route *matching* is delegated to Hono inside the
6
+ * [`Router`](./router.ts). This module exports:
7
+ *
8
+ * - {@linkcode isPathDecodable} — used by the application to reject malformed
9
+ * percent-escapes with a 400 **before** routing.
10
+ * - {@linkcode Segment}, {@linkcode parsePattern}, {@linkcode staticSegmentCount},
11
+ * {@linkcode wildcardSegmentCount} — parsing primitives shared with the Router's
12
+ * tie-break logic.
13
+ *
14
+ * @module
15
+ */
16
+ interface StaticSegment {
17
+ type: 'static';
18
+ value: string;
19
+ }
20
+ interface ParamSegment {
21
+ type: 'param';
22
+ name: string;
23
+ }
24
+ interface WildcardSegment {
25
+ type: 'wildcard';
26
+ }
27
+ /**
28
+ * A parsed route segment — a static path component, a `:name` parameter
29
+ * placeholder, or the `*` wildcard.
30
+ *
31
+ * The wildcard arm was added in M70g. Before it, `*` parsed as a STATIC segment,
32
+ * so `/*` scored the same specificity as `/openapi.json` and the tie was decided
33
+ * by registration order alone — which silently removed every single-segment
34
+ * plugin route registered after an application catch-all.
35
+ *
36
+ * @since 0.1.0
37
+ */
38
+ export type Segment = StaticSegment | ParamSegment | WildcardSegment;
39
+ /**
40
+ * Parses a route pattern like `/users/:id` into segments.
41
+ *
42
+ * @param pattern - The route pattern to parse
43
+ * @returns An array of `Segment` objects
44
+ * @internal Used only at registration time for tie-break statics counting.
45
+ */
46
+ export declare function parsePattern(pattern: string): readonly Segment[];
47
+ /**
48
+ * Counts the literal path components in a pattern.
49
+ *
50
+ * Neither a `:param` nor a `*` wildcard counts: both match text the pattern does
51
+ * not name, so neither makes the route more specific.
52
+ *
53
+ * @param segments - The parsed segments array
54
+ * @returns The count of static segments
55
+ * @internal Used only at registration time for tie-break specificity.
56
+ */
57
+ export declare function staticSegmentCount(segments: readonly Segment[]): number;
58
+ /**
59
+ * Counts the `*` wildcard segments in a pattern.
60
+ *
61
+ * The router's second tie-break key, read ASCENDING: between two routes with the
62
+ * same number of literal components, the one matching less arbitrary text is the
63
+ * more specific. That is what puts `/a/:id` ahead of `/a/*`, which before M70g
64
+ * lost in both registration orders because `*` was counted as a literal.
65
+ *
66
+ * @param segments - The parsed segments array
67
+ * @returns The count of wildcard segments
68
+ * @internal Used only at registration time for tie-break specificity.
69
+ */
70
+ export declare function wildcardSegmentCount(segments: readonly Segment[]): number;
71
+ /**
72
+ * Reports whether a path can be percent-decoded without error.
73
+ *
74
+ * A malformed percent-escape (e.g. `%zz`, a truncated `%2`, or a bare `%`)
75
+ * makes {@linkcode decodeURIComponent} throw. The application uses this to
76
+ * reject a malformed request path as a `400` before it reaches routing,
77
+ * where an unguarded decode would otherwise surface as a `500`.
78
+ *
79
+ * @param path - The raw (still percent-encoded) request path
80
+ * @returns `true` when the path decodes cleanly, `false` when it is malformed
81
+ * @since 0.1.0
82
+ */
83
+ export declare function isPathDecodable(path: string): boolean;
84
+ export {};
85
+ //# sourceMappingURL=route-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/router/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,UAAU,aAAa;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,YAAY,GAAG,eAAe,CAAC;AAErE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,OAAO,EAAE,CAiBhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAQvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAQzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOrD"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Route matcher utilities — path decoding guard and pattern-parsing helpers
3
+ * for the kernel router.
4
+ *
5
+ * As of Milestone 22, route *matching* is delegated to Hono inside the
6
+ * [`Router`](./router.ts). This module exports:
7
+ *
8
+ * - {@linkcode isPathDecodable} — used by the application to reject malformed
9
+ * percent-escapes with a 400 **before** routing.
10
+ * - {@linkcode Segment}, {@linkcode parsePattern}, {@linkcode staticSegmentCount},
11
+ * {@linkcode wildcardSegmentCount} — parsing primitives shared with the Router's
12
+ * tie-break logic.
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * Parses a route pattern like `/users/:id` into segments.
18
+ *
19
+ * @param pattern - The route pattern to parse
20
+ * @returns An array of `Segment` objects
21
+ * @internal Used only at registration time for tie-break statics counting.
22
+ */
23
+ export function parsePattern(pattern) {
24
+ const normalized = pattern === '/' ? '/' : pattern.replace(/\/+$/, '');
25
+ if (normalized === '/') {
26
+ return [{ type: 'static', value: '' }];
27
+ }
28
+ return normalized.slice(1).split('/').map((part) => {
29
+ if (part.startsWith(':')) {
30
+ return { type: 'param', name: part.slice(1) };
31
+ }
32
+ // Exactly `*`, never a segment merely CONTAINING one: `/a*` is not a pattern
33
+ // any first-party plugin registers, and treating it as a wildcard would lower
34
+ // the specificity of a path that matches literally.
35
+ if (part === '*') {
36
+ return { type: 'wildcard' };
37
+ }
38
+ return { type: 'static', value: part };
39
+ });
40
+ }
41
+ /**
42
+ * Counts the literal path components in a pattern.
43
+ *
44
+ * Neither a `:param` nor a `*` wildcard counts: both match text the pattern does
45
+ * not name, so neither makes the route more specific.
46
+ *
47
+ * @param segments - The parsed segments array
48
+ * @returns The count of static segments
49
+ * @internal Used only at registration time for tie-break specificity.
50
+ */
51
+ export function staticSegmentCount(segments) {
52
+ let count = 0;
53
+ for (const segment of segments) {
54
+ if (segment.type === 'static') {
55
+ count++;
56
+ }
57
+ }
58
+ return count;
59
+ }
60
+ /**
61
+ * Counts the `*` wildcard segments in a pattern.
62
+ *
63
+ * The router's second tie-break key, read ASCENDING: between two routes with the
64
+ * same number of literal components, the one matching less arbitrary text is the
65
+ * more specific. That is what puts `/a/:id` ahead of `/a/*`, which before M70g
66
+ * lost in both registration orders because `*` was counted as a literal.
67
+ *
68
+ * @param segments - The parsed segments array
69
+ * @returns The count of wildcard segments
70
+ * @internal Used only at registration time for tie-break specificity.
71
+ */
72
+ export function wildcardSegmentCount(segments) {
73
+ let count = 0;
74
+ for (const segment of segments) {
75
+ if (segment.type === 'wildcard') {
76
+ count++;
77
+ }
78
+ }
79
+ return count;
80
+ }
81
+ /**
82
+ * Reports whether a path can be percent-decoded without error.
83
+ *
84
+ * A malformed percent-escape (e.g. `%zz`, a truncated `%2`, or a bare `%`)
85
+ * makes {@linkcode decodeURIComponent} throw. The application uses this to
86
+ * reject a malformed request path as a `400` before it reaches routing,
87
+ * where an unguarded decode would otherwise surface as a `500`.
88
+ *
89
+ * @param path - The raw (still percent-encoded) request path
90
+ * @returns `true` when the path decodes cleanly, `false` when it is malformed
91
+ * @since 0.1.0
92
+ */
93
+ export function isPathDecodable(path) {
94
+ try {
95
+ decodeURIComponent(path);
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ //# sourceMappingURL=route-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-matcher.js","sourceRoot":"","sources":["../../src/router/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA6BH;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,UAAU,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvE,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACjD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,CAAC;QACD,6EAA6E;QAC7E,8EAA8E;QAC9E,oDAAoD;QACpD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QAC9B,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAA4B;IAC7D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC9B,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAA4B;IAC/D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAChC,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,CAAC;QACH,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}