@jskit-ai/kernel 0.1.4
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 +24 -0
- package/_testable/index.js +4 -0
- package/client/appConfig.js +33 -0
- package/client/componentInteraction.js +51 -0
- package/client/componentInteraction.test.js +111 -0
- package/client/descriptorSections.js +75 -0
- package/client/index.d.ts +70 -0
- package/client/index.js +3 -0
- package/client/logging.js +38 -0
- package/client/moduleBootstrap.js +670 -0
- package/client/moduleBootstrap.test.js +403 -0
- package/client/shellBootstrap.js +233 -0
- package/client/shellBootstrap.test.js +185 -0
- package/client/shellRouting.js +321 -0
- package/client/shellRouting.test.js +113 -0
- package/client/vite/clientBootstrapPlugin.js +259 -0
- package/client/vite/clientBootstrapPlugin.test.js +563 -0
- package/client/vite/index.js +3 -0
- package/internal/node/fileSystem.js +21 -0
- package/internal/node/installedPackageDescriptor.js +104 -0
- package/package.json +43 -0
- package/server/actions/ActionRuntimeServiceProvider.js +309 -0
- package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
- package/server/actions/index.js +8 -0
- package/server/container/ContainerCoreServiceProvider.js +27 -0
- package/server/container/index.js +10 -0
- package/server/exportPolicy.test.js +68 -0
- package/server/http/HttpFastifyServiceProvider.js +25 -0
- package/server/http/_testable/index.js +2 -0
- package/server/http/index.js +1 -0
- package/server/http/lib/controller.js +183 -0
- package/server/http/lib/controller.test.js +143 -0
- package/server/http/lib/errors.js +12 -0
- package/server/http/lib/httpRuntime.js +82 -0
- package/server/http/lib/index.js +18 -0
- package/server/http/lib/kernel.js +15 -0
- package/server/http/lib/kernel.test.js +880 -0
- package/server/http/lib/middlewareRuntime.js +149 -0
- package/server/http/lib/requestActionExecutor.js +258 -0
- package/server/http/lib/requestScope.js +59 -0
- package/server/http/lib/routeRegistration.js +165 -0
- package/server/http/lib/routeSupport.js +45 -0
- package/server/http/lib/routeValidator.js +469 -0
- package/server/http/lib/routeValidator.test.js +474 -0
- package/server/http/lib/router.js +206 -0
- package/server/kernel/KernelCoreServiceProvider.js +27 -0
- package/server/kernel/index.js +10 -0
- package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
- package/server/platform/index.js +5 -0
- package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
- package/server/platform/providerRuntime/helpers.js +45 -0
- package/server/platform/providerRuntime/lockfile.js +27 -0
- package/server/platform/providerRuntime/providerLoader.js +283 -0
- package/server/platform/providerRuntime.js +142 -0
- package/server/platform/providerRuntime.test.js +217 -0
- package/server/platform/runtime.js +40 -0
- package/server/platform/surfaceRuntime.js +150 -0
- package/server/platform/surfaceRuntime.test.js +136 -0
- package/server/registries/actionSurfaceSourceRegistry.js +150 -0
- package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
- package/server/registries/domainEventListenerRegistry.js +61 -0
- package/server/registries/index.js +36 -0
- package/server/registries/primitives.js +63 -0
- package/server/registries/routeVisibilityResolverRegistry.js +87 -0
- package/server/registries/serviceRegistrationRegistry.js +431 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
- package/server/runtime/apiRoutePolicyParity.test.js +109 -0
- package/server/runtime/apiRouteRegistration.js +65 -0
- package/server/runtime/bootBootstrapRoutes.js +46 -0
- package/server/runtime/bootBootstrapRoutes.test.js +79 -0
- package/server/runtime/bootstrapContributors.test.js +114 -0
- package/server/runtime/canonicalJson.js +74 -0
- package/server/runtime/composition.js +142 -0
- package/server/runtime/domainEvents.test.js +114 -0
- package/server/runtime/domainRules.js +50 -0
- package/server/runtime/domainRules.test.js +87 -0
- package/server/runtime/entityChangeEvents.js +182 -0
- package/server/runtime/entityChangeEvents.test.js +211 -0
- package/server/runtime/errors.js +68 -0
- package/server/runtime/errors.test.js +73 -0
- package/server/runtime/fastifyBootstrap.js +372 -0
- package/server/runtime/fastifyBootstrap.test.js +194 -0
- package/server/runtime/index.js +6 -0
- package/server/runtime/integers.js +13 -0
- package/server/runtime/moduleConfig.js +269 -0
- package/server/runtime/moduleConfig.test.js +141 -0
- package/server/runtime/pagination.js +13 -0
- package/server/runtime/realtimeNormalization.js +21 -0
- package/server/runtime/requestUrl.js +38 -0
- package/server/runtime/routeUtils.js +20 -0
- package/server/runtime/runtimeAssembly.js +113 -0
- package/server/runtime/runtimeKernel.js +55 -0
- package/server/runtime/securityAudit.js +269 -0
- package/server/runtime/securityAudit.test.js +41 -0
- package/server/runtime/serviceAuthorization.js +113 -0
- package/server/runtime/serviceAuthorization.test.js +100 -0
- package/server/runtime/serviceRegistration.test.js +197 -0
- package/server/support/SupportCoreServiceProvider.js +25 -0
- package/server/support/appConfig.js +37 -0
- package/server/support/appConfig.test.js +94 -0
- package/server/support/defaultMissingHandler.js +7 -0
- package/server/support/index.js +2 -0
- package/server/support/routePolicyConfig.js +51 -0
- package/server/support/symlinkSafeRequire.js +78 -0
- package/server/support/symlinkSafeRequire.test.js +27 -0
- package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
- package/server/surface/index.js +19 -0
- package/shared/actions/actionContributorHelpers.js +34 -0
- package/shared/actions/actionContributorHelpers.test.js +16 -0
- package/shared/actions/actionDefinitions.js +488 -0
- package/shared/actions/actionDefinitions.test.js +212 -0
- package/shared/actions/audit.js +7 -0
- package/shared/actions/executionContext.js +97 -0
- package/shared/actions/executionContext.test.js +66 -0
- package/shared/actions/idempotency.js +62 -0
- package/shared/actions/index.js +2 -0
- package/shared/actions/observability.js +10 -0
- package/shared/actions/pipeline.js +287 -0
- package/shared/actions/policies.js +342 -0
- package/shared/actions/policies.test.js +233 -0
- package/shared/actions/registry.js +187 -0
- package/shared/actions/registry.test.js +381 -0
- package/shared/actions/requestMeta.js +36 -0
- package/shared/actions/textNormalization.js +3 -0
- package/shared/actions/withActionDefaults.js +34 -0
- package/shared/index.js +2 -0
- package/shared/runtime/application.js +323 -0
- package/shared/runtime/container.js +261 -0
- package/shared/runtime/containerErrors.js +22 -0
- package/shared/runtime/index.js +18 -0
- package/shared/runtime/kernelErrors.js +20 -0
- package/shared/runtime/serviceProvider.js +13 -0
- package/shared/support/formatDateTime.js +10 -0
- package/shared/support/formatDateTime.test.js +15 -0
- package/shared/support/index.js +14 -0
- package/shared/support/linkPath.js +67 -0
- package/shared/support/linkPath.test.js +35 -0
- package/shared/support/normalize.js +116 -0
- package/shared/support/normalize.test.js +48 -0
- package/shared/support/packageDescriptor.test.js +121 -0
- package/shared/support/permissions.js +50 -0
- package/shared/support/pickOwnProperties.js +17 -0
- package/shared/support/pickOwnProperties.test.js +25 -0
- package/shared/support/policies.js +11 -0
- package/shared/support/queryPath.js +33 -0
- package/shared/support/queryPath.test.js +19 -0
- package/shared/support/queryResilience.js +34 -0
- package/shared/support/queryResilience.test.js +33 -0
- package/shared/support/returnToPath.js +153 -0
- package/shared/support/returnToPath.test.js +123 -0
- package/shared/support/sorting.js +15 -0
- package/shared/support/tokens.js +23 -0
- package/shared/support/tokens.test.js +17 -0
- package/shared/support/visibility.js +56 -0
- package/shared/support/visibility.test.js +45 -0
- package/shared/surface/apiPaths.js +84 -0
- package/shared/surface/escapeRegExp.js +5 -0
- package/shared/surface/index.js +6 -0
- package/shared/surface/paths.js +273 -0
- package/shared/surface/registry.js +135 -0
- package/shared/surface/registry.test.js +44 -0
- package/shared/surface/runtime.js +357 -0
- package/shared/surface/runtime.test.js +319 -0
- package/shared/validators/createCursorListValidator.js +42 -0
- package/shared/validators/createCursorListValidator.test.js +34 -0
- package/shared/validators/cursorPaginationQueryValidator.js +31 -0
- package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
- package/shared/validators/index.js +12 -0
- package/shared/validators/inputNormalization.js +13 -0
- package/shared/validators/mergeObjectSchemas.js +31 -0
- package/shared/validators/mergeObjectSchemas.test.js +67 -0
- package/shared/validators/mergeValidators.js +89 -0
- package/shared/validators/mergeValidators.test.js +116 -0
- package/shared/validators/nestValidator.js +53 -0
- package/shared/validators/nestValidator.test.js +60 -0
- package/shared/validators/recordIdParamsValidator.js +36 -0
- package/shared/validators/recordIdParamsValidator.test.js +20 -0
- package/shared/validators/resourceRequiredMetadata.js +41 -0
- package/shared/validators/resourceRequiredMetadata.test.js +49 -0
- package/test/barrelExposure.test.js +106 -0
- package/test/dynamicImportPolicy.test.js +89 -0
- package/test/exportsContract.test.js +168 -0
- package/test/routeInputContractGuard.test.js +78 -0
- package/test/surfaceIndependence.test.js +109 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { createContainer } from "./container.js";
|
|
2
|
+
import {
|
|
3
|
+
DuplicateProviderError,
|
|
4
|
+
ProviderDependencyError,
|
|
5
|
+
ProviderLifecycleError,
|
|
6
|
+
ProviderNormalizationError
|
|
7
|
+
} from "./kernelErrors.js";
|
|
8
|
+
import { ServiceProvider } from "./serviceProvider.js";
|
|
9
|
+
import { normalizeText } from "../support/normalize.js";
|
|
10
|
+
|
|
11
|
+
function normalizeStringArray(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return [...new Set(value.map((entry) => String(entry || "").trim()).filter(Boolean))].sort((left, right) =>
|
|
17
|
+
left.localeCompare(right)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nowMilliseconds() {
|
|
22
|
+
return Date.now();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class Application {
|
|
26
|
+
constructor({ profile = "", strict = true, container = null } = {}) {
|
|
27
|
+
this.profile = normalizeText(profile);
|
|
28
|
+
this.strict = strict !== false;
|
|
29
|
+
this.container = container || createContainer({ scopeId: "root" });
|
|
30
|
+
|
|
31
|
+
this.providerEntries = [];
|
|
32
|
+
this.registeredProviders = [];
|
|
33
|
+
this.bootedProviders = [];
|
|
34
|
+
|
|
35
|
+
this.diagnostics = {
|
|
36
|
+
profile: this.profile,
|
|
37
|
+
providerOrder: [],
|
|
38
|
+
registeredOrder: [],
|
|
39
|
+
bootedOrder: [],
|
|
40
|
+
timings: {
|
|
41
|
+
register: {},
|
|
42
|
+
boot: {},
|
|
43
|
+
shutdown: {}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bind(token, factory) {
|
|
49
|
+
this.container.bind(token, factory);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
singleton(token, factory) {
|
|
54
|
+
this.container.singleton(token, factory);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
scoped(token, factory) {
|
|
59
|
+
this.container.scoped(token, factory);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
instance(token, value) {
|
|
64
|
+
this.container.instance(token, value);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
make(token) {
|
|
69
|
+
return this.container.make(token);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
has(token) {
|
|
73
|
+
return this.container.has(token);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
createScope(scopeId) {
|
|
77
|
+
return this.container.createScope(scopeId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
tag(token, tagName) {
|
|
81
|
+
this.container.tag(token, tagName);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resolveTag(tagName) {
|
|
86
|
+
return this.container.resolveTag(tagName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
normalizeProviderEntries(providers = []) {
|
|
90
|
+
if (!Array.isArray(providers)) {
|
|
91
|
+
throw new ProviderNormalizationError("Providers must be an array.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const entries = [];
|
|
95
|
+
const seenIds = new Set();
|
|
96
|
+
|
|
97
|
+
for (const rawProvider of providers) {
|
|
98
|
+
const entry = this.normalizeProviderEntry(rawProvider);
|
|
99
|
+
if (seenIds.has(entry.id)) {
|
|
100
|
+
throw new DuplicateProviderError(`Provider \"${entry.id}\" is duplicated.`);
|
|
101
|
+
}
|
|
102
|
+
seenIds.add(entry.id);
|
|
103
|
+
entries.push(entry);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return entries.sort((left, right) => left.id.localeCompare(right.id));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
normalizeProviderEntry(rawProvider) {
|
|
110
|
+
if (!rawProvider) {
|
|
111
|
+
throw new ProviderNormalizationError("Provider entry is required.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof rawProvider === "function") {
|
|
115
|
+
const providerInstance = new rawProvider(this);
|
|
116
|
+
const providerId = normalizeText(rawProvider.id || providerInstance.id || rawProvider.name);
|
|
117
|
+
if (!providerId) {
|
|
118
|
+
throw new ProviderNormalizationError("Provider class must define a stable id.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
id: providerId,
|
|
123
|
+
dependsOn: normalizeStringArray(rawProvider.dependsOn || providerInstance.dependsOn),
|
|
124
|
+
provider: providerInstance
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof rawProvider === "object") {
|
|
129
|
+
const provider = rawProvider;
|
|
130
|
+
const providerId = normalizeText(provider.id || provider.constructor?.id || provider.constructor?.name);
|
|
131
|
+
if (!providerId) {
|
|
132
|
+
throw new ProviderNormalizationError("Provider object must define id.");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
id: providerId,
|
|
137
|
+
dependsOn: normalizeStringArray(provider.dependsOn || provider.constructor?.dependsOn),
|
|
138
|
+
provider
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new ProviderNormalizationError("Provider entry must be a class or object instance.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
sortProviderGraph(entries = []) {
|
|
146
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
147
|
+
const visited = new Set();
|
|
148
|
+
const visiting = new Set();
|
|
149
|
+
const ordered = [];
|
|
150
|
+
|
|
151
|
+
const visit = (providerId, lineage = []) => {
|
|
152
|
+
if (visited.has(providerId)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (visiting.has(providerId)) {
|
|
156
|
+
throw new ProviderDependencyError(`Provider dependency cycle detected: ${[...lineage, providerId].join(" -> ")}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entry = byId.get(providerId);
|
|
160
|
+
if (!entry) {
|
|
161
|
+
throw new ProviderDependencyError(`Provider \"${lineage[lineage.length - 1] || "<unknown>"}\" depends on missing provider \"${providerId}\".`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
visiting.add(providerId);
|
|
165
|
+
for (const dependencyId of entry.dependsOn) {
|
|
166
|
+
visit(dependencyId, [...lineage, providerId]);
|
|
167
|
+
}
|
|
168
|
+
visiting.delete(providerId);
|
|
169
|
+
visited.add(providerId);
|
|
170
|
+
ordered.push(entry);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
for (const entry of [...entries].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
174
|
+
visit(entry.id, []);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return ordered;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
configureProviders(providers = []) {
|
|
181
|
+
const normalized = this.normalizeProviderEntries(providers);
|
|
182
|
+
const ordered = this.sortProviderGraph(normalized);
|
|
183
|
+
|
|
184
|
+
this.providerEntries = ordered;
|
|
185
|
+
this.diagnostics.providerOrder = ordered.map((entry) => entry.id);
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async registerProviders() {
|
|
190
|
+
this.registeredProviders = [];
|
|
191
|
+
|
|
192
|
+
for (const entry of this.providerEntries) {
|
|
193
|
+
const startedAt = nowMilliseconds();
|
|
194
|
+
try {
|
|
195
|
+
if (typeof entry.provider.register === "function") {
|
|
196
|
+
await entry.provider.register(this);
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new ProviderLifecycleError(`Provider \"${entry.id}\" failed during register().`, {
|
|
200
|
+
providerId: entry.id,
|
|
201
|
+
phase: "register",
|
|
202
|
+
cause: error
|
|
203
|
+
});
|
|
204
|
+
} finally {
|
|
205
|
+
this.diagnostics.timings.register[entry.id] = nowMilliseconds() - startedAt;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.registeredProviders.push(entry);
|
|
209
|
+
this.diagnostics.registeredOrder.push(entry.id);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async bootProviders() {
|
|
216
|
+
this.bootedProviders = [];
|
|
217
|
+
|
|
218
|
+
for (const entry of this.providerEntries) {
|
|
219
|
+
const startedAt = nowMilliseconds();
|
|
220
|
+
try {
|
|
221
|
+
if (typeof entry.provider.boot === "function") {
|
|
222
|
+
await entry.provider.boot(this);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new ProviderLifecycleError(`Provider \"${entry.id}\" failed during boot().`, {
|
|
226
|
+
providerId: entry.id,
|
|
227
|
+
phase: "boot",
|
|
228
|
+
cause: error
|
|
229
|
+
});
|
|
230
|
+
} finally {
|
|
231
|
+
this.diagnostics.timings.boot[entry.id] = nowMilliseconds() - startedAt;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.bootedProviders.push(entry);
|
|
235
|
+
this.diagnostics.bootedOrder.push(entry.id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async start({ providers = [] } = {}) {
|
|
242
|
+
this.configureProviders(providers);
|
|
243
|
+
await this.registerProviders();
|
|
244
|
+
await this.bootProviders();
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async shutdown() {
|
|
249
|
+
const shutdownOrder = [...this.bootedProviders].reverse();
|
|
250
|
+
|
|
251
|
+
for (const entry of shutdownOrder) {
|
|
252
|
+
const startedAt = nowMilliseconds();
|
|
253
|
+
try {
|
|
254
|
+
if (typeof entry.provider.shutdown === "function") {
|
|
255
|
+
await entry.provider.shutdown(this);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new ProviderLifecycleError(`Provider \"${entry.id}\" failed during shutdown().`, {
|
|
259
|
+
providerId: entry.id,
|
|
260
|
+
phase: "shutdown",
|
|
261
|
+
cause: error
|
|
262
|
+
});
|
|
263
|
+
} finally {
|
|
264
|
+
this.diagnostics.timings.shutdown[entry.id] = nowMilliseconds() - startedAt;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return shutdownOrder.map((entry) => entry.id);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getDiagnostics() {
|
|
272
|
+
return Object.freeze({
|
|
273
|
+
profile: this.diagnostics.profile,
|
|
274
|
+
providerOrder: Object.freeze([...this.diagnostics.providerOrder]),
|
|
275
|
+
registeredOrder: Object.freeze([...this.diagnostics.registeredOrder]),
|
|
276
|
+
bootedOrder: Object.freeze([...this.diagnostics.bootedOrder]),
|
|
277
|
+
timings: Object.freeze({
|
|
278
|
+
register: Object.freeze({ ...this.diagnostics.timings.register }),
|
|
279
|
+
boot: Object.freeze({ ...this.diagnostics.timings.boot }),
|
|
280
|
+
shutdown: Object.freeze({ ...this.diagnostics.timings.shutdown })
|
|
281
|
+
})
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createApplication(options = {}) {
|
|
287
|
+
return new Application(options);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createProviderClass({ id, dependsOn = [], register = null, boot = null, shutdown = null } = {}) {
|
|
291
|
+
const providerId = normalizeText(id);
|
|
292
|
+
if (!providerId) {
|
|
293
|
+
throw new ProviderNormalizationError("createProviderClass requires id.");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
class DynamicProvider extends ServiceProvider {
|
|
297
|
+
static id = providerId;
|
|
298
|
+
|
|
299
|
+
static dependsOn = normalizeStringArray(dependsOn);
|
|
300
|
+
|
|
301
|
+
async register(app) {
|
|
302
|
+
if (typeof register === "function") {
|
|
303
|
+
await register(app);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async boot(app) {
|
|
308
|
+
if (typeof boot === "function") {
|
|
309
|
+
await boot(app);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async shutdown(app) {
|
|
314
|
+
if (typeof shutdown === "function") {
|
|
315
|
+
await shutdown(app);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return DynamicProvider;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export { Application, createApplication, createProviderClass };
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircularDependencyError,
|
|
3
|
+
DuplicateBindingError,
|
|
4
|
+
InvalidFactoryError,
|
|
5
|
+
InvalidTokenError,
|
|
6
|
+
UnresolvedTokenError
|
|
7
|
+
} from "./containerErrors.js";
|
|
8
|
+
|
|
9
|
+
const LIFETIME_TRANSIENT = "transient";
|
|
10
|
+
const LIFETIME_SINGLETON = "singleton";
|
|
11
|
+
const LIFETIME_SCOPED = "scoped";
|
|
12
|
+
|
|
13
|
+
function normalizeToken(token) {
|
|
14
|
+
const kind = typeof token;
|
|
15
|
+
if (kind === "string") {
|
|
16
|
+
const normalized = token.trim();
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
throw new InvalidTokenError("Container token string cannot be empty.");
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (kind === "symbol" || kind === "function") {
|
|
24
|
+
return token;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new InvalidTokenError("Container token must be a non-empty string, symbol, or function.", {
|
|
28
|
+
receivedType: kind
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function tokenLabel(token) {
|
|
33
|
+
if (typeof token === "string") {
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof token === "symbol") {
|
|
38
|
+
return token.description ? `Symbol(${token.description})` : String(token);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof token === "function") {
|
|
42
|
+
return token.name ? `Function(${token.name})` : "Function(<anonymous>)";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return String(token);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureFactory(factory, token) {
|
|
49
|
+
if (typeof factory !== "function") {
|
|
50
|
+
throw new InvalidFactoryError(`Factory for token \"${tokenLabel(token)}\" must be a function.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeTagName(tagName) {
|
|
55
|
+
const normalized = String(tagName || "").trim();
|
|
56
|
+
if (!normalized) {
|
|
57
|
+
throw new TypeError("Tag name is required.");
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeScopeId(scopeId) {
|
|
63
|
+
const normalized = String(scopeId || "").trim();
|
|
64
|
+
return normalized || "scope";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class Container {
|
|
68
|
+
constructor({ parent = null, scopeId = "root" } = {}) {
|
|
69
|
+
this.parent = parent instanceof Container ? parent : null;
|
|
70
|
+
this.scopeId = normalizeScopeId(scopeId);
|
|
71
|
+
this.bindings = new Map();
|
|
72
|
+
this.instances = new Map();
|
|
73
|
+
this.scopedInstances = new Map();
|
|
74
|
+
|
|
75
|
+
if (!this.parent) {
|
|
76
|
+
this.tags = new Map();
|
|
77
|
+
this.resolutionStack = [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
root() {
|
|
82
|
+
let node = this;
|
|
83
|
+
while (node.parent) {
|
|
84
|
+
node = node.parent;
|
|
85
|
+
}
|
|
86
|
+
return node;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
has(token) {
|
|
90
|
+
const normalizedToken = normalizeToken(token);
|
|
91
|
+
return this.findBindingRecord(normalizedToken) !== null || this.findInstanceRecord(normalizedToken) !== null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
bind(token, factory) {
|
|
95
|
+
return this.setBinding(token, factory, LIFETIME_TRANSIENT);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
singleton(token, factory) {
|
|
99
|
+
return this.setBinding(token, factory, LIFETIME_SINGLETON);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
scoped(token, factory) {
|
|
103
|
+
return this.setBinding(token, factory, LIFETIME_SCOPED);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
instance(token, value) {
|
|
107
|
+
const normalizedToken = normalizeToken(token);
|
|
108
|
+
if (this.bindings.has(normalizedToken) || this.instances.has(normalizedToken)) {
|
|
109
|
+
throw new DuplicateBindingError(`Token \"${tokenLabel(normalizedToken)}\" is already bound.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.instances.set(normalizedToken, value);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
tag(token, tagName) {
|
|
117
|
+
const normalizedToken = normalizeToken(token);
|
|
118
|
+
const normalizedTagName = normalizeTagName(tagName);
|
|
119
|
+
|
|
120
|
+
if (!this.has(normalizedToken)) {
|
|
121
|
+
throw new UnresolvedTokenError(`Cannot tag unresolved token \"${tokenLabel(normalizedToken)}\".`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rootContainer = this.root();
|
|
125
|
+
if (!rootContainer.tags.has(normalizedTagName)) {
|
|
126
|
+
rootContainer.tags.set(normalizedTagName, new Set());
|
|
127
|
+
}
|
|
128
|
+
rootContainer.tags.get(normalizedTagName).add(normalizedToken);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
resolveTag(tagName) {
|
|
133
|
+
const normalizedTagName = normalizeTagName(tagName);
|
|
134
|
+
const rootContainer = this.root();
|
|
135
|
+
const tokens = rootContainer.tags.get(normalizedTagName);
|
|
136
|
+
if (!tokens || tokens.size < 1) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return [...tokens]
|
|
141
|
+
.sort((left, right) => tokenLabel(left).localeCompare(tokenLabel(right)))
|
|
142
|
+
.map((token) => this.make(token));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
createScope(scopeId = "scope") {
|
|
146
|
+
return new Container({
|
|
147
|
+
parent: this,
|
|
148
|
+
scopeId: normalizeScopeId(scopeId)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
make(token) {
|
|
153
|
+
const normalizedToken = normalizeToken(token);
|
|
154
|
+
const instanceRecord = this.findInstanceRecord(normalizedToken);
|
|
155
|
+
if (instanceRecord) {
|
|
156
|
+
return instanceRecord.value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const bindingRecord = this.findBindingRecord(normalizedToken);
|
|
160
|
+
if (!bindingRecord) {
|
|
161
|
+
throw new UnresolvedTokenError(`Token \"${tokenLabel(normalizedToken)}\" is not registered.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const rootContainer = this.root();
|
|
165
|
+
const stack = rootContainer.resolutionStack;
|
|
166
|
+
const stackIndex = stack.indexOf(normalizedToken);
|
|
167
|
+
if (stackIndex >= 0) {
|
|
168
|
+
const cycle = stack
|
|
169
|
+
.slice(stackIndex)
|
|
170
|
+
.concat([normalizedToken])
|
|
171
|
+
.map((entry) => tokenLabel(entry));
|
|
172
|
+
throw new CircularDependencyError(`Circular dependency detected: ${cycle.join(" -> ")}.`, {
|
|
173
|
+
cycle
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stack.push(normalizedToken);
|
|
178
|
+
try {
|
|
179
|
+
return this.resolveFromBindingRecord(bindingRecord);
|
|
180
|
+
} finally {
|
|
181
|
+
stack.pop();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setBinding(token, factory, lifetime) {
|
|
186
|
+
const normalizedToken = normalizeToken(token);
|
|
187
|
+
ensureFactory(factory, normalizedToken);
|
|
188
|
+
|
|
189
|
+
if (this.bindings.has(normalizedToken) || this.instances.has(normalizedToken)) {
|
|
190
|
+
throw new DuplicateBindingError(`Token \"${tokenLabel(normalizedToken)}\" is already bound.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.bindings.set(normalizedToken, {
|
|
194
|
+
token: normalizedToken,
|
|
195
|
+
factory,
|
|
196
|
+
lifetime
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
findBindingRecord(token) {
|
|
203
|
+
let node = this;
|
|
204
|
+
while (node) {
|
|
205
|
+
if (node.bindings.has(token)) {
|
|
206
|
+
return {
|
|
207
|
+
container: node,
|
|
208
|
+
binding: node.bindings.get(token)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
node = node.parent;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
findInstanceRecord(token) {
|
|
218
|
+
let node = this;
|
|
219
|
+
while (node) {
|
|
220
|
+
if (node.instances.has(token)) {
|
|
221
|
+
return {
|
|
222
|
+
container: node,
|
|
223
|
+
value: node.instances.get(token)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
node = node.parent;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
resolveFromBindingRecord(record) {
|
|
233
|
+
const { container, binding } = record;
|
|
234
|
+
|
|
235
|
+
if (binding.lifetime === LIFETIME_SINGLETON) {
|
|
236
|
+
if (container.instances.has(binding.token)) {
|
|
237
|
+
return container.instances.get(binding.token);
|
|
238
|
+
}
|
|
239
|
+
const created = binding.factory(this);
|
|
240
|
+
container.instances.set(binding.token, created);
|
|
241
|
+
return created;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (binding.lifetime === LIFETIME_SCOPED) {
|
|
245
|
+
if (this.scopedInstances.has(binding.token)) {
|
|
246
|
+
return this.scopedInstances.get(binding.token);
|
|
247
|
+
}
|
|
248
|
+
const created = binding.factory(this);
|
|
249
|
+
this.scopedInstances.set(binding.token, created);
|
|
250
|
+
return created;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return binding.factory(this);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function createContainer(options = {}) {
|
|
258
|
+
return new Container(options);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export { Container, createContainer, tokenLabel };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class ContainerError extends Error {
|
|
2
|
+
constructor(message, details = {}) {
|
|
3
|
+
super(String(message || "Container error."));
|
|
4
|
+
this.name = this.constructor.name;
|
|
5
|
+
this.details = details && typeof details === "object" ? { ...details } : {};
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class InvalidTokenError extends ContainerError {}
|
|
10
|
+
class InvalidFactoryError extends ContainerError {}
|
|
11
|
+
class DuplicateBindingError extends ContainerError {}
|
|
12
|
+
class UnresolvedTokenError extends ContainerError {}
|
|
13
|
+
class CircularDependencyError extends ContainerError {}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
ContainerError,
|
|
17
|
+
InvalidTokenError,
|
|
18
|
+
InvalidFactoryError,
|
|
19
|
+
DuplicateBindingError,
|
|
20
|
+
UnresolvedTokenError,
|
|
21
|
+
CircularDependencyError
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { Container, createContainer, tokenLabel } from "./container.js";
|
|
2
|
+
export {
|
|
3
|
+
ContainerError,
|
|
4
|
+
InvalidTokenError,
|
|
5
|
+
InvalidFactoryError,
|
|
6
|
+
DuplicateBindingError,
|
|
7
|
+
UnresolvedTokenError,
|
|
8
|
+
CircularDependencyError
|
|
9
|
+
} from "./containerErrors.js";
|
|
10
|
+
export { Application, createApplication, createProviderClass } from "./application.js";
|
|
11
|
+
export { ServiceProvider } from "./serviceProvider.js";
|
|
12
|
+
export {
|
|
13
|
+
KernelError,
|
|
14
|
+
ProviderNormalizationError,
|
|
15
|
+
DuplicateProviderError,
|
|
16
|
+
ProviderDependencyError,
|
|
17
|
+
ProviderLifecycleError
|
|
18
|
+
} from "./kernelErrors.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class KernelError extends Error {
|
|
2
|
+
constructor(message, details = {}) {
|
|
3
|
+
super(String(message || "Kernel error."));
|
|
4
|
+
this.name = this.constructor.name;
|
|
5
|
+
this.details = details && typeof details === "object" ? { ...details } : {};
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ProviderNormalizationError extends KernelError {}
|
|
10
|
+
class DuplicateProviderError extends KernelError {}
|
|
11
|
+
class ProviderDependencyError extends KernelError {}
|
|
12
|
+
class ProviderLifecycleError extends KernelError {}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
KernelError,
|
|
16
|
+
ProviderNormalizationError,
|
|
17
|
+
DuplicateProviderError,
|
|
18
|
+
ProviderDependencyError,
|
|
19
|
+
ProviderLifecycleError
|
|
20
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { formatDateTime } from "./formatDateTime.js";
|
|
4
|
+
|
|
5
|
+
test("formatDateTime returns fallback for invalid values", () => {
|
|
6
|
+
assert.equal(formatDateTime("not-a-date"), "unknown");
|
|
7
|
+
assert.equal(formatDateTime("not-a-date", { fallback: "n/a" }), "n/a");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("formatDateTime formats valid date-like values", () => {
|
|
11
|
+
const formatted = formatDateTime("2026-03-14T12:34:56.000Z");
|
|
12
|
+
assert.equal(typeof formatted, "string");
|
|
13
|
+
assert.ok(formatted.length > 0);
|
|
14
|
+
assert.notEqual(formatted, "unknown");
|
|
15
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { isRecord } from "./normalize.js";
|
|
2
|
+
export { pickOwnProperties } from "./pickOwnProperties.js";
|
|
3
|
+
export { formatDateTime } from "./formatDateTime.js";
|
|
4
|
+
export { appendQueryString } from "./queryPath.js";
|
|
5
|
+
export { normalizePermissionList, hasPermission } from "./permissions.js";
|
|
6
|
+
export {
|
|
7
|
+
normalizeReturnToPath,
|
|
8
|
+
resolveAllowedOriginsFromPlacementContext
|
|
9
|
+
} from "./returnToPath.js";
|
|
10
|
+
export {
|
|
11
|
+
isTransientQueryError,
|
|
12
|
+
shouldRetryTransientQueryFailure,
|
|
13
|
+
transientQueryRetryDelay
|
|
14
|
+
} from "./queryResilience.js";
|