@jskit-ai/kernel 0.1.158 → 0.1.160
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/_testable/index.js +1 -3
- package/client/componentRegistry.js +44 -0
- package/client/componentRegistry.test.js +16 -0
- package/client/moduleBootstrap.js +54 -97
- package/client/moduleBootstrap.test.js +47 -32
- package/client/vite/clientBootstrapPlugin.js +12 -3
- package/client/vite/clientBootstrapPlugin.test.js +21 -6
- package/internal/node/installedPackages.js +2 -7
- package/internal/node/installedPackages.test.js +26 -5
- package/package.json +3 -5
- package/server/actions/actionCatalogue.js +162 -0
- package/server/actions/actionEvents.js +66 -0
- package/server/actions/actionProvider.js +23 -0
- package/server/actions/index.js +3 -6
- package/server/features/defineFeature.js +131 -0
- package/server/features/defineFeature.test.js +238 -0
- package/server/features/index.js +1 -0
- package/server/http/HttpProvider.js +23 -0
- package/server/http/_testable/index.js +0 -1
- package/server/http/capabilityHttpRuntime.js +123 -0
- package/server/http/capabilityHttpRuntime.test.js +161 -0
- package/server/http/directRequestActionExecutor.js +78 -0
- package/server/http/index.js +2 -1
- package/server/http/lib/index.js +1 -13
- package/server/http/lib/routeRegistration.js +9 -25
- package/server/platform/index.js +2 -2
- package/server/platform/installedRuntime.js +122 -0
- package/server/platform/installedRuntime.test.js +103 -0
- package/server/platform/providerRuntime/capabilityProviderLoader.js +100 -0
- package/server/platform/providerRuntime/capabilityProviderLoader.test.js +80 -0
- package/server/platform/providerRuntime/packageCatalog.js +4 -1
- package/server/platform/surfaceRuntime.js +1 -15
- package/server/runtime/BootstrapProvider.js +69 -0
- package/server/runtime/EventProvider.js +74 -0
- package/server/runtime/EventProvider.test.js +32 -0
- package/server/runtime/entityChangeEvents.js +7 -7
- package/server/runtime/entityChangeEvents.test.js +2 -2
- package/server/runtime/fastifyBootstrap.js +0 -40
- package/server/runtime/index.js +2 -3
- package/server/surface/index.js +0 -1
- package/shared/actions/actionDefinitions.js +20 -0
- package/shared/actions/pipeline.js +22 -0
- package/shared/actions/policies.js +1 -1
- package/shared/actions/registry.js +3 -0
- package/shared/capabilities/defineProvider.js +99 -0
- package/shared/capabilities/index.js +2 -0
- package/shared/capabilities/runtime.js +243 -0
- package/shared/capabilities/runtime.test.js +170 -0
- package/test/barrelExposure.test.js +36 -13
- package/test/exportsContract.test.js +8 -3
- package/server/actions/ActionRuntimeServiceProvider.js +0 -304
- package/server/actions/ActionRuntimeServiceProvider.test.js +0 -558
- package/server/container/ContainerCoreServiceProvider.js +0 -27
- package/server/container/index.js +0 -10
- package/server/http/HttpFastifyServiceProvider.js +0 -25
- package/server/http/lib/httpRuntime.js +0 -83
- package/server/http/lib/kernel.js +0 -15
- package/server/http/lib/kernel.test.js +0 -1565
- package/server/http/lib/requestActionExecutor.js +0 -265
- package/server/http/lib/requestScope.js +0 -57
- package/server/kernel/KernelCoreServiceProvider.js +0 -27
- package/server/kernel/index.js +0 -10
- package/server/platform/PlatformServerRuntimeServiceProvider.js +0 -30
- package/server/platform/providerRuntime/providerLoader.js +0 -283
- package/server/platform/providerRuntime.js +0 -146
- package/server/platform/providerRuntime.test.js +0 -214
- package/server/platform/runtime.js +0 -40
- package/server/registries/actionSurfaceSourceRegistry.js +0 -148
- package/server/registries/bootstrapPayloadContributorRegistry.js +0 -51
- package/server/registries/domainEventListenerRegistry.js +0 -58
- package/server/registries/index.js +0 -32
- package/server/registries/primitives.js +0 -63
- package/server/registries/routeVisibilityResolverRegistry.js +0 -84
- package/server/registries/serviceRegistrationRegistry.js +0 -488
- package/server/runtime/ServerRuntimeCoreServiceProvider.js +0 -64
- package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +0 -53
- package/server/runtime/apiRoutePolicyParity.test.js +0 -109
- package/server/runtime/apiRouteRegistration.js +0 -65
- package/server/runtime/bootBootstrapRoutes.js +0 -29
- package/server/runtime/bootBootstrapRoutes.test.js +0 -69
- package/server/runtime/bootstrapContributors.test.js +0 -166
- package/server/runtime/composition.js +0 -142
- package/server/runtime/domainEvents.test.js +0 -114
- package/server/runtime/domainRules.js +0 -50
- package/server/runtime/domainRules.test.js +0 -87
- package/server/runtime/runtimeAssembly.js +0 -113
- package/server/runtime/runtimeKernel.js +0 -55
- package/server/runtime/serviceRegistration.test.js +0 -251
- package/server/support/SupportCoreServiceProvider.js +0 -27
- package/server/surface/SurfaceRoutingServiceProvider.js +0 -27
- package/shared/actions/index.js +0 -2
- package/shared/runtime/application.js +0 -324
- package/shared/runtime/container.js +0 -261
- package/shared/runtime/containerErrors.js +0 -22
- package/shared/runtime/index.js +0 -18
- package/shared/runtime/kernelErrors.js +0 -25
- package/shared/runtime/kernelErrors.test.js +0 -120
- package/shared/runtime/serviceProvider.js +0 -13
- package/shared/support/containerToken.js +0 -8
- package/shared/support/generatedUiContract.js +0 -542
- package/shared/support/generatedUiContract.test.js +0 -218
- package/shared/support/tokens.js +0 -1
- package/shared/support/tokens.test.js +0 -17
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
7
|
+
import { createInstalledRuntime } from "@jskit-ai/kernel/server/platform";
|
|
8
|
+
|
|
9
|
+
test("installed runtime composes explicit package providers and application inputs", async (context) => {
|
|
10
|
+
const appRoot = await mkdtemp(path.join(tmpdir(), "jskit-installed-runtime-"));
|
|
11
|
+
context.after(() => rm(appRoot, { recursive: true, force: true }));
|
|
12
|
+
const packageRoot = path.join(appRoot, "packages", "example");
|
|
13
|
+
await mkdir(path.join(packageRoot, "src"), { recursive: true });
|
|
14
|
+
await writeFile(
|
|
15
|
+
path.join(appRoot, "package.json"),
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
name: "fixture-app",
|
|
18
|
+
private: true,
|
|
19
|
+
dependencies: { "@fixture/example": "file:packages/example" }
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
await writeFile(
|
|
23
|
+
path.join(packageRoot, "package.json"),
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
name: "@fixture/example",
|
|
26
|
+
version: "0.1.0",
|
|
27
|
+
type: "module",
|
|
28
|
+
jskit: {
|
|
29
|
+
kind: "runtime",
|
|
30
|
+
capabilities: {
|
|
31
|
+
requires: ["runtime.env"],
|
|
32
|
+
provides: ["feature.example"]
|
|
33
|
+
},
|
|
34
|
+
runtime: {
|
|
35
|
+
server: {
|
|
36
|
+
providers: [{ entrypoint: "src/provider.js", export: "ExampleProvider" }]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
await writeFile(
|
|
43
|
+
path.join(packageRoot, "src", "provider.js"),
|
|
44
|
+
[
|
|
45
|
+
"export const ExampleProvider = {",
|
|
46
|
+
" id: 'feature.example',",
|
|
47
|
+
" requires: { env: 'runtime.env' },",
|
|
48
|
+
" provides: { example: 'feature.example' },",
|
|
49
|
+
" setup({ env }) { return { example: { name: env.APP_NAME } }; }",
|
|
50
|
+
"};"
|
|
51
|
+
].join("\n")
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let observed = null;
|
|
55
|
+
const observer = defineProvider({
|
|
56
|
+
id: "fixture.observer",
|
|
57
|
+
requires: {
|
|
58
|
+
actions: "runtime.actions",
|
|
59
|
+
example: "feature.example",
|
|
60
|
+
logger: "runtime.logger"
|
|
61
|
+
},
|
|
62
|
+
setup(dependencies) {
|
|
63
|
+
observed = dependencies;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const runtime = await createInstalledRuntime({
|
|
67
|
+
appRoot,
|
|
68
|
+
env: { APP_NAME: "Books" },
|
|
69
|
+
logger: { info() {} },
|
|
70
|
+
providers: [observer]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.equal(observed.example.name, "Books");
|
|
74
|
+
assert.equal(typeof observed.actions.execute, "function");
|
|
75
|
+
assert.equal(typeof observed.logger.info, "function");
|
|
76
|
+
assert.deepEqual(runtime.packageOrder, ["@fixture/example"]);
|
|
77
|
+
assert.deepEqual(runtime.providerPackageOrder, ["@fixture/example"]);
|
|
78
|
+
assert.deepEqual(runtime.diagnostics.providerOrder, [
|
|
79
|
+
"feature.example",
|
|
80
|
+
"runtime.events",
|
|
81
|
+
"runtime.actions",
|
|
82
|
+
"fixture.observer"
|
|
83
|
+
]);
|
|
84
|
+
assert.equal(Object.hasOwn(runtime, "app"), false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("installed runtime attaches one idempotent Fastify shutdown hook", async (context) => {
|
|
88
|
+
const appRoot = await mkdtemp(path.join(tmpdir(), "jskit-installed-runtime-close-"));
|
|
89
|
+
context.after(() => rm(appRoot, { recursive: true, force: true }));
|
|
90
|
+
await writeFile(path.join(appRoot, "package.json"), JSON.stringify({ name: "fixture-app", private: true }));
|
|
91
|
+
const hooks = [];
|
|
92
|
+
const fastify = {
|
|
93
|
+
addHook(name, handler) {
|
|
94
|
+
hooks.push({ name, handler });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const installed = await createInstalledRuntime({ appRoot, fastify });
|
|
98
|
+
const closeHook = hooks.find((entry) => entry.name === "onClose");
|
|
99
|
+
assert.ok(closeHook);
|
|
100
|
+
await closeHook.handler();
|
|
101
|
+
await closeHook.handler();
|
|
102
|
+
assert.equal(installed.runtime.diagnostics().lifecycleState, "stopped");
|
|
103
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { defineProvider } from "../../../shared/capabilities/defineProvider.js";
|
|
4
|
+
import { fileExists, isIdentifier, isInsidePackageRoot } from "./helpers.js";
|
|
5
|
+
|
|
6
|
+
function declaredProviderEntrypoints(packageEntry) {
|
|
7
|
+
const runtime = packageEntry?.packageMetadata?.runtime;
|
|
8
|
+
const server = runtime && typeof runtime === "object" ? runtime.server : null;
|
|
9
|
+
const entries = Array.isArray(server?.providers) ? server.providers : [];
|
|
10
|
+
|
|
11
|
+
return entries.map((value) => {
|
|
12
|
+
const entry = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
13
|
+
if (entry.discover != null) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Package ${packageEntry.packageId} runtime.server.providers[] must declare an explicit entrypoint and export.`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const entrypoint = String(entry.entrypoint || "").trim();
|
|
19
|
+
const exportName = String(entry.export || "").trim();
|
|
20
|
+
if (!entrypoint) {
|
|
21
|
+
throw new Error(`Package ${packageEntry.packageId} runtime.server.providers[] entrypoint is required.`);
|
|
22
|
+
}
|
|
23
|
+
if (!isIdentifier(exportName)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Package ${packageEntry.packageId} runtime.server.providers[] export is invalid: ${exportName || "<missing>"}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze({ entrypoint, exportName });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeProviderExports(value, packageId, exportName) {
|
|
33
|
+
const queue = Array.isArray(value) ? [...value] : [value];
|
|
34
|
+
const providers = [];
|
|
35
|
+
while (queue.length > 0) {
|
|
36
|
+
const candidate = queue.shift();
|
|
37
|
+
if (Array.isArray(candidate)) {
|
|
38
|
+
queue.unshift(...candidate);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
providers.push(defineProvider(candidate));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Package ${packageId} provider export "${exportName}" must contain provider definitions: ${error.message}`,
|
|
46
|
+
{ cause: error }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (providers.length < 1) {
|
|
51
|
+
throw new Error(`Package ${packageId} provider export "${exportName}" is empty.`);
|
|
52
|
+
}
|
|
53
|
+
return providers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadCapabilityProviders({ packageEntry } = {}) {
|
|
57
|
+
const providers = [];
|
|
58
|
+
for (const declaration of declaredProviderEntrypoints(packageEntry)) {
|
|
59
|
+
const modulePath = path.resolve(packageEntry.packageRoot, declaration.entrypoint);
|
|
60
|
+
if (!isInsidePackageRoot(packageEntry.packageRoot, modulePath)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Package ${packageEntry.packageId} provider entrypoint escapes package root: ${declaration.entrypoint}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!(await fileExists(modulePath))) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Package ${packageEntry.packageId} provider entrypoint not found: ${declaration.entrypoint}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const namespace = await import(pathToFileURL(modulePath).href);
|
|
72
|
+
if (!Object.hasOwn(namespace, declaration.exportName)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Package ${packageEntry.packageId} provider export "${declaration.exportName}" was not found.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
providers.push(
|
|
78
|
+
...normalizeProviderExports(
|
|
79
|
+
namespace[declaration.exportName],
|
|
80
|
+
packageEntry.packageId,
|
|
81
|
+
declaration.exportName
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return Object.freeze(providers);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendCapabilityProviders({ providers, sourceId, seenProviderIds, orderedProviders }) {
|
|
89
|
+
for (const provider of providers) {
|
|
90
|
+
if (seenProviderIds.has(provider.id)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Provider id "${provider.id}" is duplicated between ${seenProviderIds.get(provider.id)} and ${sourceId}.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
seenProviderIds.set(provider.id, sourceId);
|
|
96
|
+
orderedProviders.push(provider);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { appendCapabilityProviders, loadCapabilityProviders };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
appendCapabilityProviders,
|
|
8
|
+
loadCapabilityProviders
|
|
9
|
+
} from "./capabilityProviderLoader.js";
|
|
10
|
+
|
|
11
|
+
async function fixturePackage() {
|
|
12
|
+
const packageRoot = await mkdtemp(path.join(tmpdir(), "jskit-capability-provider-"));
|
|
13
|
+
await mkdir(path.join(packageRoot, "src"), { recursive: true });
|
|
14
|
+
return {
|
|
15
|
+
packageRoot,
|
|
16
|
+
packageId: "@fixture/example",
|
|
17
|
+
packageMetadata: {
|
|
18
|
+
runtime: {
|
|
19
|
+
server: {
|
|
20
|
+
providers: [{ entrypoint: "src/provider.js", export: "providers" }]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("capability provider loader imports explicit provider definitions", async () => {
|
|
28
|
+
const packageEntry = await fixturePackage();
|
|
29
|
+
try {
|
|
30
|
+
await writeFile(
|
|
31
|
+
path.join(packageEntry.packageRoot, "src", "provider.js"),
|
|
32
|
+
[
|
|
33
|
+
"export const providers = [{",
|
|
34
|
+
" id: 'feature.example',",
|
|
35
|
+
" provides: { example: 'feature.example' },",
|
|
36
|
+
" setup() { return { example: { ready: true } }; }",
|
|
37
|
+
"}];"
|
|
38
|
+
].join("\n"),
|
|
39
|
+
"utf8"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const providers = await loadCapabilityProviders({ packageEntry });
|
|
43
|
+
assert.deepEqual(providers.map((provider) => provider.id), ["feature.example"]);
|
|
44
|
+
assert.deepEqual(providers[0].provides, { example: "feature.example" });
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(packageEntry.packageRoot, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("capability provider loader rejects discovery, classes, path escapes, and duplicates", async () => {
|
|
51
|
+
const packageEntry = await fixturePackage();
|
|
52
|
+
try {
|
|
53
|
+
packageEntry.packageMetadata.runtime.server.providers = [{ discover: { dir: "src" } }];
|
|
54
|
+
await assert.rejects(loadCapabilityProviders({ packageEntry }), /explicit entrypoint and export/u);
|
|
55
|
+
|
|
56
|
+
packageEntry.packageMetadata.runtime.server.providers = [{ entrypoint: "src/provider.js", export: "Provider" }];
|
|
57
|
+
await writeFile(
|
|
58
|
+
path.join(packageEntry.packageRoot, "src", "provider.js"),
|
|
59
|
+
"export class Provider { static id = 'old.provider'; register() {} }\n",
|
|
60
|
+
"utf8"
|
|
61
|
+
);
|
|
62
|
+
await assert.rejects(loadCapabilityProviders({ packageEntry }), /must contain provider definitions/u);
|
|
63
|
+
|
|
64
|
+
packageEntry.packageMetadata.runtime.server.providers = [{ entrypoint: "../outside.js", export: "provider" }];
|
|
65
|
+
await assert.rejects(loadCapabilityProviders({ packageEntry }), /escapes package root/u);
|
|
66
|
+
|
|
67
|
+
const providers = [{ id: "feature.example" }];
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => appendCapabilityProviders({
|
|
70
|
+
providers,
|
|
71
|
+
sourceId: "second",
|
|
72
|
+
seenProviderIds: new Map([["feature.example", "first"]]),
|
|
73
|
+
orderedProviders: []
|
|
74
|
+
}),
|
|
75
|
+
/duplicated between first and second/u
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(packageEntry.packageRoot, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { sortStrings } from "../../../shared/support/sorting.js";
|
|
2
2
|
import { discoverInstalledPackages } from "../../../internal/node/installedPackages.js";
|
|
3
3
|
|
|
4
|
-
const EXCLUSIVE_CAPABILITIES = Object.freeze([
|
|
4
|
+
const EXCLUSIVE_CAPABILITIES = Object.freeze([
|
|
5
|
+
"auth.service",
|
|
6
|
+
"runtime.database.driver"
|
|
7
|
+
]);
|
|
5
8
|
|
|
6
9
|
function normalizeUiRoutePath(pathValue) {
|
|
7
10
|
const rawPath = String(pathValue || "").trim();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createProviderRuntimeFromApp } from "./providerRuntime.js";
|
|
2
1
|
import { matchesPathPrefix, normalizePathname } from "../../shared/surface/paths.js";
|
|
3
2
|
|
|
4
3
|
function toRequestPathname(urlValue) {
|
|
@@ -129,22 +128,9 @@ function resolveRuntimeProfileFromSurface({
|
|
|
129
128
|
return "";
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
async function tryCreateProviderRuntimeFromApp(options = {}) {
|
|
133
|
-
try {
|
|
134
|
-
return await createProviderRuntimeFromApp(options);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
const message = String(error?.message || "");
|
|
137
|
-
if (message.includes("Lock file not found:")) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
131
|
export {
|
|
145
132
|
toRequestPathname,
|
|
146
133
|
shouldServePathForSurface,
|
|
147
134
|
registerSurfaceRequestConstraint,
|
|
148
|
-
resolveRuntimeProfileFromSurface
|
|
149
|
-
tryCreateProviderRuntimeFromApp
|
|
135
|
+
resolveRuntimeProfileFromSurface
|
|
150
136
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineProvider } from "../../shared/capabilities/defineProvider.js";
|
|
2
|
+
import { isRecord, normalizeText } from "../../shared/support/normalize.js";
|
|
3
|
+
import { AUTH_POLICY_PUBLIC } from "../../shared/support/policies.js";
|
|
4
|
+
|
|
5
|
+
function createBootstrapRuntime() {
|
|
6
|
+
const contributors = [];
|
|
7
|
+
const contributorIds = new Set();
|
|
8
|
+
let sealed = false;
|
|
9
|
+
|
|
10
|
+
function register({ id, order = 0, contribute } = {}) {
|
|
11
|
+
if (sealed) throw new Error("Bootstrap registration is closed after the first payload is resolved.");
|
|
12
|
+
const contributorId = normalizeText(id);
|
|
13
|
+
if (!contributorId) throw new TypeError("Bootstrap contributor id is required.");
|
|
14
|
+
if (typeof contribute !== "function") throw new TypeError(`Bootstrap contributor "${contributorId}" requires contribute().`);
|
|
15
|
+
if (contributorIds.has(contributorId)) throw new Error(`Bootstrap contributor "${contributorId}" is duplicated.`);
|
|
16
|
+
contributorIds.add(contributorId);
|
|
17
|
+
contributors.push(Object.freeze({ id: contributorId, order: Number.isFinite(order) ? Number(order) : 0, contribute }));
|
|
18
|
+
return api;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function resolve(context = {}) {
|
|
22
|
+
sealed = true;
|
|
23
|
+
let payload = {};
|
|
24
|
+
const ordered = contributors
|
|
25
|
+
.map((entry, index) => ({ entry, index }))
|
|
26
|
+
.sort((left, right) => left.entry.order - right.entry.order || left.index - right.index)
|
|
27
|
+
.map(({ entry }) => entry);
|
|
28
|
+
for (const contributor of ordered) {
|
|
29
|
+
const contribution = await contributor.contribute(Object.freeze({
|
|
30
|
+
...(isRecord(context) ? context : {}),
|
|
31
|
+
payload: Object.freeze({ ...payload })
|
|
32
|
+
}));
|
|
33
|
+
payload = { ...payload, ...(isRecord(contribution) ? contribution : {}) };
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze(payload);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function diagnostics() {
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
sealed,
|
|
41
|
+
contributorIds: Object.freeze(contributors.map((entry) => entry.id).sort())
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const api = Object.freeze({ register, resolve, diagnostics });
|
|
46
|
+
return api;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const BootstrapProvider = defineProvider({
|
|
50
|
+
id: "runtime.bootstrap",
|
|
51
|
+
requires: { http: "runtime.http" },
|
|
52
|
+
provides: { bootstrap: "runtime.bootstrap" },
|
|
53
|
+
setup({ http }) {
|
|
54
|
+
const bootstrap = createBootstrapRuntime();
|
|
55
|
+
http.router.register("GET", "/api/bootstrap", {
|
|
56
|
+
auth: AUTH_POLICY_PUBLIC,
|
|
57
|
+
meta: { tags: ["bootstrap"], summary: "Resolve app bootstrap payload" }
|
|
58
|
+
}, async (request, reply) => {
|
|
59
|
+
reply.code(200).send(await bootstrap.resolve({
|
|
60
|
+
request,
|
|
61
|
+
reply,
|
|
62
|
+
query: request.query || {}
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
return { bootstrap };
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export { BootstrapProvider, createBootstrapRuntime };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineProvider } from "../../shared/capabilities/defineProvider.js";
|
|
2
|
+
|
|
3
|
+
function normalizeListener(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
|
+
throw new TypeError("Event listener must be an object.");
|
|
6
|
+
}
|
|
7
|
+
const id = String(value.id || "").trim();
|
|
8
|
+
if (!id) throw new TypeError("Event listener id is required.");
|
|
9
|
+
if (typeof value.handle !== "function") {
|
|
10
|
+
throw new TypeError(`Event listener "${id}" requires handle().`);
|
|
11
|
+
}
|
|
12
|
+
if (value.matches != null && typeof value.matches !== "function") {
|
|
13
|
+
throw new TypeError(`Event listener "${id}" matches must be a function when provided.`);
|
|
14
|
+
}
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
id,
|
|
17
|
+
handle: value.handle,
|
|
18
|
+
matches: typeof value.matches === "function" ? value.matches : null
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeEvent(value) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
throw new TypeError("Published event must be an object.");
|
|
25
|
+
}
|
|
26
|
+
const type = String(value.type || "").trim();
|
|
27
|
+
if (!type) throw new TypeError("Published event type is required.");
|
|
28
|
+
return Object.freeze({ ...value, type });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createEventRuntime() {
|
|
32
|
+
const listeners = [];
|
|
33
|
+
const listenerIds = new Set();
|
|
34
|
+
|
|
35
|
+
function register(value) {
|
|
36
|
+
const listener = normalizeListener(value);
|
|
37
|
+
if (listenerIds.has(listener.id)) {
|
|
38
|
+
throw new Error(`Event listener "${listener.id}" is duplicated.`);
|
|
39
|
+
}
|
|
40
|
+
listenerIds.add(listener.id);
|
|
41
|
+
listeners.push(listener);
|
|
42
|
+
return api;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function publish(value) {
|
|
46
|
+
const event = normalizeEvent(value);
|
|
47
|
+
for (const listener of listeners) {
|
|
48
|
+
if (listener.matches && listener.matches(event) !== true) continue;
|
|
49
|
+
await listener.handle(event);
|
|
50
|
+
}
|
|
51
|
+
return event;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function diagnostics() {
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
listenerIds: Object.freeze(listeners.map((listener) => listener.id))
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const api = Object.freeze({ register, publish, diagnostics });
|
|
61
|
+
return api;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const EventProvider = defineProvider({
|
|
65
|
+
id: "runtime.events",
|
|
66
|
+
provides: {
|
|
67
|
+
events: "runtime.events"
|
|
68
|
+
},
|
|
69
|
+
setup() {
|
|
70
|
+
return { events: createEventRuntime() };
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export { EventProvider, createEventRuntime };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createEventRuntime } from "./EventProvider.js";
|
|
4
|
+
|
|
5
|
+
test("event runtime publishes ephemeral events to explicit listeners in registration order", async () => {
|
|
6
|
+
const events = createEventRuntime();
|
|
7
|
+
const calls = [];
|
|
8
|
+
events.register({
|
|
9
|
+
id: "first",
|
|
10
|
+
handle(event) {
|
|
11
|
+
calls.push(`first:${event.type}`);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
events.register({
|
|
15
|
+
id: "matching",
|
|
16
|
+
matches(event) {
|
|
17
|
+
return event.type === "book.saved";
|
|
18
|
+
},
|
|
19
|
+
handle(event) {
|
|
20
|
+
calls.push(`matching:${event.bookId}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await events.publish({ type: "book.saved", bookId: "12" });
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(calls, ["first:book.saved", "matching:12"]);
|
|
27
|
+
assert.deepEqual(events.diagnostics().listenerIds, ["first", "matching"]);
|
|
28
|
+
assert.throws(
|
|
29
|
+
() => events.register({ id: "first", handle() {} }),
|
|
30
|
+
/duplicated/u
|
|
31
|
+
);
|
|
32
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isContainerToken } from "../../shared/support/containerToken.js";
|
|
2
1
|
import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
|
|
3
2
|
import { resolveServiceContext } from "./serviceAuthorization.js";
|
|
4
3
|
|
|
@@ -203,7 +202,7 @@ function normalizeRealtimePayload(value, { context = "realtime entity change.pay
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
function createRealtimeEntityChangeMeta({
|
|
206
|
-
|
|
205
|
+
serviceId,
|
|
207
206
|
methodName,
|
|
208
207
|
event,
|
|
209
208
|
change = {}
|
|
@@ -213,7 +212,7 @@ function createRealtimeEntityChangeMeta({
|
|
|
213
212
|
const meta = {
|
|
214
213
|
...baseMeta,
|
|
215
214
|
service: Object.freeze({
|
|
216
|
-
|
|
215
|
+
id: serviceId,
|
|
217
216
|
method: methodName
|
|
218
217
|
})
|
|
219
218
|
};
|
|
@@ -256,7 +255,7 @@ function createRealtimeEntityChangePublisher({
|
|
|
256
255
|
source,
|
|
257
256
|
entity,
|
|
258
257
|
event,
|
|
259
|
-
|
|
258
|
+
serviceId,
|
|
260
259
|
methodName,
|
|
261
260
|
scopeResolver = resolveDefaultScope
|
|
262
261
|
} = {}) {
|
|
@@ -265,8 +264,9 @@ function createRealtimeEntityChangePublisher({
|
|
|
265
264
|
throw new TypeError("createRealtimeEntityChangePublisher requires event.");
|
|
266
265
|
}
|
|
267
266
|
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
const normalizedServiceId = normalizeText(serviceId);
|
|
268
|
+
if (!normalizedServiceId) {
|
|
269
|
+
throw new TypeError("createRealtimeEntityChangePublisher requires serviceId.");
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
const normalizedMethodName = normalizeText(methodName);
|
|
@@ -284,7 +284,7 @@ function createRealtimeEntityChangePublisher({
|
|
|
284
284
|
return async function publishRealtimeEntityChange(operation, entityId, change = {}, options = {}) {
|
|
285
285
|
const runtimeOptions = resolveRealtimeEntityChangeOptions(change, options);
|
|
286
286
|
const meta = createRealtimeEntityChangeMeta({
|
|
287
|
-
|
|
287
|
+
serviceId: normalizedServiceId,
|
|
288
288
|
methodName: normalizedMethodName,
|
|
289
289
|
event: normalizedEvent,
|
|
290
290
|
change
|
|
@@ -224,7 +224,7 @@ test("realtime entity change publisher adds service, semantic, and realtime meta
|
|
|
224
224
|
source: "vibe64",
|
|
225
225
|
entity: "project",
|
|
226
226
|
event: "vibe64.project.changed",
|
|
227
|
-
|
|
227
|
+
serviceId: "vibe64.terminals",
|
|
228
228
|
methodName: "projectRuntime"
|
|
229
229
|
});
|
|
230
230
|
|
|
@@ -259,7 +259,7 @@ test("realtime entity change publisher adds service, semantic, and realtime meta
|
|
|
259
259
|
id: "workspace_23"
|
|
260
260
|
});
|
|
261
261
|
assert.equal(payload?.actorId, "user_17");
|
|
262
|
-
assert.equal(payload?.meta?.service?.
|
|
262
|
+
assert.equal(payload?.meta?.service?.id, "vibe64.terminals");
|
|
263
263
|
assert.equal(payload?.meta?.service?.method, "projectRuntime");
|
|
264
264
|
assert.equal(payload?.meta?.action, "runtime-closed");
|
|
265
265
|
assert.equal(payload?.meta?.reason, "user-request");
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ActionRuntimeError } from "../../shared/actions/actionDefinitions.js";
|
|
2
2
|
import { normalizeOpaqueId } from "../../shared/support/normalize.js";
|
|
3
|
-
import { isAppError } from "./errors.js";
|
|
4
3
|
import { resolveDefaultSurfaceId } from "../support/appConfig.js";
|
|
5
4
|
|
|
6
5
|
const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
|
|
@@ -403,44 +402,6 @@ function registerApiErrorHandler(
|
|
|
403
402
|
});
|
|
404
403
|
}
|
|
405
404
|
|
|
406
|
-
function ensureApiErrorHandling(
|
|
407
|
-
app,
|
|
408
|
-
{
|
|
409
|
-
fastifyToken = "jskit.fastify",
|
|
410
|
-
markerToken = "kernel.runtime.apiErrorHandlerRegistered",
|
|
411
|
-
isAppError: isAppErrorOverride,
|
|
412
|
-
autoRegister = true,
|
|
413
|
-
...handlerOptions
|
|
414
|
-
} = {}
|
|
415
|
-
) {
|
|
416
|
-
if (!app || typeof app.make !== "function" || typeof app.has !== "function" || typeof app.instance !== "function") {
|
|
417
|
-
throw new TypeError("ensureApiErrorHandling requires an application instance.");
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (autoRegister === false) {
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const normalizedMarkerToken = String(markerToken || "").trim() || "kernel.runtime.apiErrorHandlerRegistered";
|
|
425
|
-
if (app.has(normalizedMarkerToken)) {
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const fastify = app.make(fastifyToken);
|
|
430
|
-
if (!fastify || typeof fastify.setErrorHandler !== "function") {
|
|
431
|
-
throw new TypeError("ensureApiErrorHandling requires a Fastify instance.");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const appErrorPredicate = typeof isAppErrorOverride === "function" ? isAppErrorOverride : isAppError;
|
|
435
|
-
registerApiErrorHandler(fastify, {
|
|
436
|
-
...handlerOptions,
|
|
437
|
-
isAppError: appErrorPredicate
|
|
438
|
-
});
|
|
439
|
-
app.instance(normalizedMarkerToken, true);
|
|
440
|
-
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
405
|
function resolveDatabaseErrorCode(error) {
|
|
445
406
|
const errorCode = String(error?.code || "")
|
|
446
407
|
.trim()
|
|
@@ -546,7 +507,6 @@ export {
|
|
|
546
507
|
registerBodylessContentTypeNormalizer,
|
|
547
508
|
registerRequestLoggingHooks,
|
|
548
509
|
registerApiErrorHandler,
|
|
549
|
-
ensureApiErrorHandling,
|
|
550
510
|
resolveDatabaseErrorCode,
|
|
551
511
|
recordDbErrorBestEffort,
|
|
552
512
|
runGracefulShutdown
|
package/server/runtime/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { AppError, createValidationError } from "./errors.js";
|
|
2
2
|
export { parsePositiveInteger } from "./integers.js";
|
|
3
3
|
export { requireAuth } from "./serviceAuthorization.js";
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export { registerBootstrapPayloadContributor } from "../registries/bootstrapPayloadContributorRegistry.js";
|
|
4
|
+
export { BootstrapProvider, createBootstrapRuntime } from "./BootstrapProvider.js";
|
|
5
|
+
export { EventProvider, createEventRuntime } from "./EventProvider.js";
|
package/server/surface/index.js
CHANGED
|
@@ -242,6 +242,25 @@ function normalizeActionExtensions(value) {
|
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
function normalizeActionEvents(value, actionId) {
|
|
246
|
+
if (value == null) return Object.freeze([]);
|
|
247
|
+
if (!Array.isArray(value)) {
|
|
248
|
+
throw createActionRuntimeError(500, `Action definition "${actionId}" events must be an array.`, {
|
|
249
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return Object.freeze(value.map((builder, index) => {
|
|
253
|
+
if (typeof builder !== "function") {
|
|
254
|
+
throw createActionRuntimeError(
|
|
255
|
+
500,
|
|
256
|
+
`Action definition "${actionId}" events[${index}] must be a function.`,
|
|
257
|
+
{ code: "ACTION_DEFINITION_INVALID" }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return builder;
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
245
264
|
function normalizeActionDefinition(definition, { contributorId = "", contributorDomain = "" } = {}) {
|
|
246
265
|
const source = definition && typeof definition === "object" ? definition : {};
|
|
247
266
|
|
|
@@ -312,6 +331,7 @@ function normalizeActionDefinition(definition, { contributorId = "", contributor
|
|
|
312
331
|
}),
|
|
313
332
|
observability: normalizeObservabilityConfig(source.observability),
|
|
314
333
|
extensions: normalizeActionExtensions(source.extensions),
|
|
334
|
+
events: normalizeActionEvents(source.events, id),
|
|
315
335
|
execute: source.execute,
|
|
316
336
|
contributorId: normalizeText(contributorId)
|
|
317
337
|
});
|