@jskit-ai/kernel 0.1.159 → 0.1.161
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.test.js +2 -2
- 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,162 @@
|
|
|
1
|
+
import { createActionRegistry } from "../../shared/actions/registry.js";
|
|
2
|
+
import { isRecord } from "../../shared/support/normalize.js";
|
|
3
|
+
|
|
4
|
+
function normalizeRegistration(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6
|
+
throw new TypeError("Action catalogue registration must be an action contributor object.");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createActionCatalogue({
|
|
13
|
+
idempotencyAdapter,
|
|
14
|
+
auditAdapter,
|
|
15
|
+
observabilityAdapter,
|
|
16
|
+
events,
|
|
17
|
+
logger = null
|
|
18
|
+
} = {}) {
|
|
19
|
+
const contributors = [];
|
|
20
|
+
const contextContributors = [];
|
|
21
|
+
const contextContributorIds = new Set();
|
|
22
|
+
let registry = createActionRegistry({
|
|
23
|
+
contributors,
|
|
24
|
+
idempotencyAdapter,
|
|
25
|
+
auditAdapter,
|
|
26
|
+
observabilityAdapter,
|
|
27
|
+
events,
|
|
28
|
+
logger
|
|
29
|
+
});
|
|
30
|
+
let sealed = false;
|
|
31
|
+
|
|
32
|
+
function register(contributor) {
|
|
33
|
+
if (sealed) {
|
|
34
|
+
throw new Error("Action catalogue registration is closed after discovery or execution begins.");
|
|
35
|
+
}
|
|
36
|
+
const nextContributors = [...contributors, normalizeRegistration(contributor)];
|
|
37
|
+
const nextRegistry = createActionRegistry({
|
|
38
|
+
contributors: nextContributors,
|
|
39
|
+
idempotencyAdapter,
|
|
40
|
+
auditAdapter,
|
|
41
|
+
observabilityAdapter,
|
|
42
|
+
events,
|
|
43
|
+
logger
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
contributors.push(contributor);
|
|
47
|
+
registry = nextRegistry;
|
|
48
|
+
return api;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function registerContextContributor({ id, contribute } = {}) {
|
|
52
|
+
if (sealed) {
|
|
53
|
+
throw new Error("Action catalogue registration is closed after discovery or execution begins.");
|
|
54
|
+
}
|
|
55
|
+
const contributorId = String(id || "").trim();
|
|
56
|
+
if (!contributorId) {
|
|
57
|
+
throw new TypeError("Action context contributor id is required.");
|
|
58
|
+
}
|
|
59
|
+
if (typeof contribute !== "function") {
|
|
60
|
+
throw new TypeError(`Action context contributor "${contributorId}" requires contribute().`);
|
|
61
|
+
}
|
|
62
|
+
if (contextContributorIds.has(contributorId)) {
|
|
63
|
+
throw new Error(`Action context contributor "${contributorId}" is duplicated.`);
|
|
64
|
+
}
|
|
65
|
+
contextContributorIds.add(contributorId);
|
|
66
|
+
contextContributors.push(Object.freeze({ id: contributorId, contribute }));
|
|
67
|
+
return api;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function currentRegistry() {
|
|
71
|
+
sealed = true;
|
|
72
|
+
return registry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function applyContextDefaults(context, patch) {
|
|
76
|
+
const next = { ...context };
|
|
77
|
+
for (const [key, value] of Object.entries(isRecord(patch) ? patch : {})) {
|
|
78
|
+
if (key === "requestMeta") {
|
|
79
|
+
next.requestMeta = {
|
|
80
|
+
...(isRecord(value) ? value : {}),
|
|
81
|
+
...(isRecord(next.requestMeta) ? next.requestMeta : {})
|
|
82
|
+
};
|
|
83
|
+
} else if (!Object.hasOwn(next, key)) {
|
|
84
|
+
next[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return next;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function buildExecutionContext({ actionId, version, input, context }) {
|
|
91
|
+
const registryInstance = currentRegistry();
|
|
92
|
+
const definition = registryInstance.getDefinition(actionId, version);
|
|
93
|
+
let enriched = { ...(isRecord(context) ? context : {}) };
|
|
94
|
+
for (const contributor of contextContributors) {
|
|
95
|
+
const patch = await contributor.contribute(Object.freeze({
|
|
96
|
+
actionId: definition.id,
|
|
97
|
+
version: definition.version,
|
|
98
|
+
definition,
|
|
99
|
+
input: Object.freeze({ ...(isRecord(input) ? input : {}) }),
|
|
100
|
+
context: Object.freeze({ ...enriched })
|
|
101
|
+
}));
|
|
102
|
+
enriched = applyContextDefaults(enriched, patch);
|
|
103
|
+
}
|
|
104
|
+
return Object.freeze(enriched);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function execute(payload = {}) {
|
|
108
|
+
const source = payload && typeof payload === "object" ? payload : {};
|
|
109
|
+
const input = source.input && typeof source.input === "object" ? source.input : {};
|
|
110
|
+
const context = await buildExecutionContext({
|
|
111
|
+
actionId: source.actionId,
|
|
112
|
+
version: source.version == null ? null : source.version,
|
|
113
|
+
input,
|
|
114
|
+
context: source.context
|
|
115
|
+
});
|
|
116
|
+
return currentRegistry().execute({
|
|
117
|
+
actionId: source.actionId,
|
|
118
|
+
version: source.version == null ? null : source.version,
|
|
119
|
+
input,
|
|
120
|
+
context,
|
|
121
|
+
deps: isRecord(source.deps) ? source.deps : {}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function executeStream(payload = {}) {
|
|
126
|
+
const source = payload && typeof payload === "object" ? payload : {};
|
|
127
|
+
const input = source.input && typeof source.input === "object" ? source.input : {};
|
|
128
|
+
const context = await buildExecutionContext({
|
|
129
|
+
actionId: source.actionId,
|
|
130
|
+
version: source.version == null ? null : source.version,
|
|
131
|
+
input,
|
|
132
|
+
context: source.context
|
|
133
|
+
});
|
|
134
|
+
return currentRegistry().executeStream({
|
|
135
|
+
actionId: source.actionId,
|
|
136
|
+
version: source.version == null ? null : source.version,
|
|
137
|
+
input,
|
|
138
|
+
context,
|
|
139
|
+
deps: isRecord(source.deps) ? source.deps : {}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function listDefinitions() {
|
|
144
|
+
return currentRegistry().listDefinitions();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getDefinition(actionId, version = null) {
|
|
148
|
+
return currentRegistry().getDefinition(actionId, version);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const api = Object.freeze({
|
|
152
|
+
register,
|
|
153
|
+
registerContextContributor,
|
|
154
|
+
execute,
|
|
155
|
+
executeStream,
|
|
156
|
+
listDefinitions,
|
|
157
|
+
getDefinition
|
|
158
|
+
});
|
|
159
|
+
return api;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { createActionCatalogue };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolveDefaultScope } from "../runtime/entityChangeEvents.js";
|
|
2
|
+
import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
|
|
3
|
+
|
|
4
|
+
const ENTITY_CHANGE_OPERATIONS = new Set(["created", "updated", "deleted"]);
|
|
5
|
+
|
|
6
|
+
function resolveValue(value, execution) {
|
|
7
|
+
return typeof value === "function" ? value(execution) : value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeRealtime(value, execution) {
|
|
11
|
+
const source = normalizeObject(value);
|
|
12
|
+
if (Object.keys(source).length === 0) return null;
|
|
13
|
+
const event = normalizeText(source.event);
|
|
14
|
+
if (!event) throw new TypeError("Action entity-change realtime.event is required.");
|
|
15
|
+
const audience = resolveValue(source.audience ?? "event_scope", execution);
|
|
16
|
+
const payload = resolveValue(source.payload, execution);
|
|
17
|
+
if (payload != null && (!payload || typeof payload !== "object" || Array.isArray(payload))) {
|
|
18
|
+
throw new TypeError("Action entity-change realtime.payload must resolve to an object.");
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
event,
|
|
22
|
+
audience,
|
|
23
|
+
...(payload == null ? {} : { payload: Object.freeze({ ...payload }) })
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createEntityChangedActionEvent({
|
|
28
|
+
source,
|
|
29
|
+
entity,
|
|
30
|
+
operation,
|
|
31
|
+
entityId,
|
|
32
|
+
realtime = null
|
|
33
|
+
} = {}) {
|
|
34
|
+
const normalizedSource = normalizeText(source);
|
|
35
|
+
const normalizedEntity = normalizeText(entity);
|
|
36
|
+
if (!normalizedSource) throw new TypeError("Action entity-change source is required.");
|
|
37
|
+
if (!normalizedEntity) throw new TypeError("Action entity-change entity is required.");
|
|
38
|
+
|
|
39
|
+
return function buildEntityChangedEvent(execution = {}) {
|
|
40
|
+
const normalizedOperation = normalizeText(resolveValue(operation, execution)).toLowerCase();
|
|
41
|
+
if (!ENTITY_CHANGE_OPERATIONS.has(normalizedOperation)) {
|
|
42
|
+
throw new TypeError("Action entity-change operation must resolve to created, updated, or deleted.");
|
|
43
|
+
}
|
|
44
|
+
const normalizedEntityId = normalizeOpaqueId(resolveValue(entityId, execution));
|
|
45
|
+
if (normalizedEntityId == null) return null;
|
|
46
|
+
|
|
47
|
+
const context = normalizeObject(execution.context);
|
|
48
|
+
const requestMeta = normalizeObject(context.requestMeta);
|
|
49
|
+
const realtimeMeta = normalizeRealtime(realtime, execution);
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
type: "entity.changed",
|
|
52
|
+
source: normalizedSource,
|
|
53
|
+
entity: normalizedEntity,
|
|
54
|
+
operation: normalizedOperation,
|
|
55
|
+
entityId: normalizedEntityId,
|
|
56
|
+
scope: resolveDefaultScope(normalizeObject(context.visibilityContext), { context }),
|
|
57
|
+
actorId: normalizeOpaqueId(context?.actor?.id),
|
|
58
|
+
commandId: normalizeText(requestMeta.commandId || requestMeta.idempotencyKey) || null,
|
|
59
|
+
sourceClientId: normalizeText(requestMeta.sourceClientId) || null,
|
|
60
|
+
occurredAt: new Date().toISOString(),
|
|
61
|
+
...(realtimeMeta ? { realtime: realtimeMeta } : {})
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { createEntityChangedActionEvent };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineProvider } from "../../shared/capabilities/defineProvider.js";
|
|
2
|
+
import { createEventRuntime } from "../runtime/EventProvider.js";
|
|
3
|
+
import { createActionCatalogue } from "./actionCatalogue.js";
|
|
4
|
+
|
|
5
|
+
function createActionProvider(options = {}) {
|
|
6
|
+
return defineProvider({
|
|
7
|
+
id: "runtime.actions",
|
|
8
|
+
optional: {
|
|
9
|
+
events: "runtime.events"
|
|
10
|
+
},
|
|
11
|
+
provides: {
|
|
12
|
+
actions: "runtime.actions"
|
|
13
|
+
},
|
|
14
|
+
setup({ events }) {
|
|
15
|
+
return { actions: createActionCatalogue({
|
|
16
|
+
...options,
|
|
17
|
+
events: events || createEventRuntime()
|
|
18
|
+
}) };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { createActionProvider };
|
package/server/actions/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
registerActionContextContributor,
|
|
5
|
-
ActionRuntimeServiceProvider
|
|
6
|
-
} from "./ActionRuntimeServiceProvider.js";
|
|
1
|
+
export { createActionCatalogue } from "./actionCatalogue.js";
|
|
2
|
+
export { createActionProvider } from "./actionProvider.js";
|
|
3
|
+
export { createEntityChangedActionEvent } from "./actionEvents.js";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { isRecord } from "../../shared/support/normalize.js";
|
|
2
|
+
import { defineProvider } from "../../shared/capabilities/defineProvider.js";
|
|
3
|
+
|
|
4
|
+
function exactOutputNames(value, expectedNames, featureId) {
|
|
5
|
+
const outputs = value == null ? {} : value;
|
|
6
|
+
if (!isRecord(outputs)) {
|
|
7
|
+
throw new TypeError(`Feature "${featureId}" setup() must return its declared capability outputs.`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const actualNames = Object.keys(outputs).sort();
|
|
11
|
+
const normalizedExpectedNames = [...expectedNames].sort();
|
|
12
|
+
if (
|
|
13
|
+
actualNames.length !== normalizedExpectedNames.length ||
|
|
14
|
+
actualNames.some((name, index) => name !== normalizedExpectedNames[index])
|
|
15
|
+
) {
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
`Feature "${featureId}" setup() outputs must be exactly: ${normalizedExpectedNames.join(", ") || "<none>"}.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return Object.freeze({ ...outputs });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeActionFactory(value, featureId) {
|
|
25
|
+
if (value == null) {
|
|
26
|
+
return () => [];
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "function") {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return () => value;
|
|
33
|
+
}
|
|
34
|
+
throw new TypeError(`Feature "${featureId}" actions must be an array or function.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyActionDefaults(actions, defaults, featureId) {
|
|
38
|
+
if (!isRecord(defaults)) {
|
|
39
|
+
throw new TypeError(`Feature "${featureId}" actionDefaults must be an object.`);
|
|
40
|
+
}
|
|
41
|
+
if (Object.hasOwn(defaults, "dependencies")) {
|
|
42
|
+
throw new TypeError(`Feature "${featureId}" actionDefaults cannot declare container dependencies.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return actions.map((value) => {
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
|
+
throw new TypeError(`Feature "${featureId}" actions must contain action definition objects.`);
|
|
48
|
+
}
|
|
49
|
+
if (Object.hasOwn(value, "dependencies")) {
|
|
50
|
+
throw new TypeError(`Feature "${featureId}" actions cannot declare container dependencies.`);
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze({ ...defaults, ...value });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function withoutActionCatalogue(dependencies) {
|
|
57
|
+
const { actionCatalogue, ...featureDependencies } = dependencies;
|
|
58
|
+
return Object.freeze(featureDependencies);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defineFeature({
|
|
62
|
+
id,
|
|
63
|
+
domain = id,
|
|
64
|
+
requires = {},
|
|
65
|
+
optional = {},
|
|
66
|
+
provides = {},
|
|
67
|
+
setup = () => ({}),
|
|
68
|
+
actions = [],
|
|
69
|
+
actionDefaults = {},
|
|
70
|
+
boot = null,
|
|
71
|
+
shutdown = null
|
|
72
|
+
} = {}) {
|
|
73
|
+
if (Object.hasOwn(requires || {}, "actionCatalogue") || Object.hasOwn(optional || {}, "actionCatalogue")) {
|
|
74
|
+
throw new TypeError("Feature dependencies reserve the local name actionCatalogue.");
|
|
75
|
+
}
|
|
76
|
+
if (typeof setup !== "function") {
|
|
77
|
+
throw new TypeError(`Feature "${String(id || "")}" setup must be a function.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const actionFactory = normalizeActionFactory(actions, String(id || ""));
|
|
81
|
+
const expectedOutputNames = Object.keys(provides || {});
|
|
82
|
+
|
|
83
|
+
return defineProvider({
|
|
84
|
+
id,
|
|
85
|
+
requires: {
|
|
86
|
+
...requires,
|
|
87
|
+
actionCatalogue: "runtime.actions"
|
|
88
|
+
},
|
|
89
|
+
optional,
|
|
90
|
+
provides,
|
|
91
|
+
async setup(dependencies, context) {
|
|
92
|
+
const { actionCatalogue } = dependencies;
|
|
93
|
+
const featureDependencies = withoutActionCatalogue(dependencies);
|
|
94
|
+
const featureContext = Object.freeze({ ...context, actionCatalogue });
|
|
95
|
+
const outputs = exactOutputNames(
|
|
96
|
+
await setup(Object.freeze(featureDependencies), featureContext),
|
|
97
|
+
expectedOutputNames,
|
|
98
|
+
String(id || "")
|
|
99
|
+
);
|
|
100
|
+
const featureActions = await actionFactory(
|
|
101
|
+
Object.freeze({
|
|
102
|
+
...featureDependencies,
|
|
103
|
+
...outputs
|
|
104
|
+
}),
|
|
105
|
+
featureContext
|
|
106
|
+
);
|
|
107
|
+
if (!Array.isArray(featureActions)) {
|
|
108
|
+
throw new TypeError(`Feature "${String(id || "")}" actions() must return an array.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const materializedActions = applyActionDefaults(featureActions, actionDefaults, String(id || ""));
|
|
112
|
+
|
|
113
|
+
if (materializedActions.length > 0) {
|
|
114
|
+
actionCatalogue.register({
|
|
115
|
+
contributorId: String(id || ""),
|
|
116
|
+
domain,
|
|
117
|
+
actions: materializedActions
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return outputs;
|
|
121
|
+
},
|
|
122
|
+
boot: typeof boot === "function"
|
|
123
|
+
? (dependencies, context) => boot(withoutActionCatalogue(dependencies), context)
|
|
124
|
+
: boot,
|
|
125
|
+
shutdown: typeof shutdown === "function"
|
|
126
|
+
? (dependencies, context) => shutdown(withoutActionCatalogue(dependencies), context)
|
|
127
|
+
: shutdown
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { defineFeature };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
4
|
+
import {
|
|
5
|
+
createActionProvider,
|
|
6
|
+
createEntityChangedActionEvent
|
|
7
|
+
} from "@jskit-ai/kernel/server/actions";
|
|
8
|
+
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
9
|
+
import { defineFeature } from "@jskit-ai/kernel/server/features";
|
|
10
|
+
import { EventProvider } from "@jskit-ai/kernel/server/runtime";
|
|
11
|
+
|
|
12
|
+
function emptyInput() {
|
|
13
|
+
return { schema: createSchema({}) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("features capture only named capabilities and publish actions without container lookup", async () => {
|
|
17
|
+
let setupActionCatalogue = null;
|
|
18
|
+
const database = Object.freeze({
|
|
19
|
+
async listBooks() {
|
|
20
|
+
return [{ id: 1, title: "Kindred" }];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const databaseProvider = defineProvider({
|
|
24
|
+
id: "database.test",
|
|
25
|
+
provides: { database: "runtime.database" },
|
|
26
|
+
setup() {
|
|
27
|
+
return { database };
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const booksFeature = defineFeature({
|
|
31
|
+
id: "feature.books",
|
|
32
|
+
domain: "books",
|
|
33
|
+
requires: { database: "runtime.database" },
|
|
34
|
+
provides: { books: "feature.books" },
|
|
35
|
+
actionDefaults: {
|
|
36
|
+
channels: ["api", "assistant"],
|
|
37
|
+
surfaces: ["app"]
|
|
38
|
+
},
|
|
39
|
+
setup({ database: selectedDatabase }, { actionCatalogue }) {
|
|
40
|
+
setupActionCatalogue = actionCatalogue;
|
|
41
|
+
return {
|
|
42
|
+
books: Object.freeze({
|
|
43
|
+
list: () => selectedDatabase.listBooks()
|
|
44
|
+
})
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
actions({ books }) {
|
|
48
|
+
return [{
|
|
49
|
+
id: "books.list",
|
|
50
|
+
kind: "query",
|
|
51
|
+
input: emptyInput(),
|
|
52
|
+
idempotency: "none",
|
|
53
|
+
async execute() {
|
|
54
|
+
return books.list();
|
|
55
|
+
}
|
|
56
|
+
}];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let actions = null;
|
|
61
|
+
const observer = defineProvider({
|
|
62
|
+
id: "test.observer",
|
|
63
|
+
requires: { actionCatalogue: "runtime.actions" },
|
|
64
|
+
setup({ actionCatalogue }) {
|
|
65
|
+
actions = actionCatalogue;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const runtime = createCapabilityRuntime({
|
|
69
|
+
providers: [observer, booksFeature, createActionProvider(), databaseProvider]
|
|
70
|
+
});
|
|
71
|
+
await runtime.start();
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(actions.listDefinitions().map((entry) => entry.id), ["books.list"]);
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
await actions.execute({
|
|
76
|
+
actionId: "books.list",
|
|
77
|
+
context: { channel: "assistant", surface: "app" }
|
|
78
|
+
}),
|
|
79
|
+
[{ id: 1, title: "Kindred" }]
|
|
80
|
+
);
|
|
81
|
+
assert.equal(Object.hasOwn(actions, "make"), false);
|
|
82
|
+
assert.equal(Object.hasOwn(actions, "resolve"), false);
|
|
83
|
+
assert.equal(setupActionCatalogue, actions);
|
|
84
|
+
assert.throws(
|
|
85
|
+
() => actions.register({ contributorId: "late", domain: "late", actions: [] }),
|
|
86
|
+
/registration is closed/u
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("feature registration rejects duplicate actions and undeclared setup outputs", async () => {
|
|
91
|
+
const action = {
|
|
92
|
+
id: "books.read",
|
|
93
|
+
kind: "query",
|
|
94
|
+
channels: ["api"],
|
|
95
|
+
surfaces: ["app"],
|
|
96
|
+
input: emptyInput(),
|
|
97
|
+
idempotency: "none",
|
|
98
|
+
async execute() {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const first = defineFeature({ id: "feature.first", domain: "books", actions: [action] });
|
|
103
|
+
const second = defineFeature({ id: "feature.second", domain: "books", actions: [action] });
|
|
104
|
+
const duplicateRuntime = createCapabilityRuntime({
|
|
105
|
+
providers: [createActionProvider(), first, second]
|
|
106
|
+
});
|
|
107
|
+
await assert.rejects(duplicateRuntime.start(), /duplicated between contributors/u);
|
|
108
|
+
|
|
109
|
+
const undeclared = defineFeature({
|
|
110
|
+
id: "feature.undeclared",
|
|
111
|
+
setup() {
|
|
112
|
+
return { surprise: true };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const invalidRuntime = createCapabilityRuntime({
|
|
116
|
+
providers: [createActionProvider(), undeclared]
|
|
117
|
+
});
|
|
118
|
+
await assert.rejects(invalidRuntime.start(), /outputs must be exactly: <none>/u);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("action context contributors enrich execution without a request scope container", async () => {
|
|
122
|
+
let actions;
|
|
123
|
+
const actionProvider = createActionProvider();
|
|
124
|
+
const feature = defineFeature({
|
|
125
|
+
id: "feature.context-proof",
|
|
126
|
+
domain: "context-proof",
|
|
127
|
+
actions: [{
|
|
128
|
+
id: "context-proof.read",
|
|
129
|
+
kind: "query",
|
|
130
|
+
channels: ["api"],
|
|
131
|
+
surfaces: ["app"],
|
|
132
|
+
input: emptyInput(),
|
|
133
|
+
idempotency: "none",
|
|
134
|
+
async execute(_input, context) {
|
|
135
|
+
return context;
|
|
136
|
+
}
|
|
137
|
+
}]
|
|
138
|
+
});
|
|
139
|
+
const contributor = defineProvider({
|
|
140
|
+
id: "test.context-contributor",
|
|
141
|
+
requires: { actionCatalogue: "runtime.actions" },
|
|
142
|
+
setup({ actionCatalogue }) {
|
|
143
|
+
actions = actionCatalogue;
|
|
144
|
+
actionCatalogue.registerContextContributor({
|
|
145
|
+
id: "test.actor",
|
|
146
|
+
contribute({ context }) {
|
|
147
|
+
return {
|
|
148
|
+
actorId: context.requestMeta?.request?.actorId || "anonymous",
|
|
149
|
+
requestMeta: { contributed: true }
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const runtime = createCapabilityRuntime({ providers: [actionProvider, contributor, feature] });
|
|
157
|
+
|
|
158
|
+
await runtime.start();
|
|
159
|
+
const context = await actions.execute({
|
|
160
|
+
actionId: "context-proof.read",
|
|
161
|
+
context: {
|
|
162
|
+
channel: "api",
|
|
163
|
+
surface: "app",
|
|
164
|
+
requestMeta: {
|
|
165
|
+
request: { actorId: "user-1" },
|
|
166
|
+
callerOwned: true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
assert.equal(context.actorId, "user-1");
|
|
172
|
+
assert.equal(context.requestMeta.contributed, true);
|
|
173
|
+
assert.deepEqual(context.requestMeta.request, { actorId: "user-1" });
|
|
174
|
+
assert.equal(context.requestMeta.callerOwned, true);
|
|
175
|
+
assert.throws(
|
|
176
|
+
() => actions.registerContextContributor({ id: "late", contribute() {} }),
|
|
177
|
+
/registration is closed/u
|
|
178
|
+
);
|
|
179
|
+
await runtime.shutdown();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("feature actions publish explicit success events without service registrations", async () => {
|
|
183
|
+
let actions;
|
|
184
|
+
const received = [];
|
|
185
|
+
const feature = defineFeature({
|
|
186
|
+
id: "feature.event-proof",
|
|
187
|
+
domain: "books",
|
|
188
|
+
actions: [{
|
|
189
|
+
id: "books.save",
|
|
190
|
+
kind: "command",
|
|
191
|
+
channels: ["api"],
|
|
192
|
+
surfaces: ["app"],
|
|
193
|
+
input: emptyInput(),
|
|
194
|
+
idempotency: "none",
|
|
195
|
+
events: [createEntityChangedActionEvent({
|
|
196
|
+
source: "books",
|
|
197
|
+
entity: "book",
|
|
198
|
+
operation: "updated",
|
|
199
|
+
entityId: ({ context }) => context.actor.id,
|
|
200
|
+
realtime: {
|
|
201
|
+
event: "books.changed",
|
|
202
|
+
audience: "actor_user"
|
|
203
|
+
}
|
|
204
|
+
})],
|
|
205
|
+
async execute() {
|
|
206
|
+
return { saved: true };
|
|
207
|
+
}
|
|
208
|
+
}]
|
|
209
|
+
});
|
|
210
|
+
const listener = defineProvider({
|
|
211
|
+
id: "test.event-listener",
|
|
212
|
+
requires: {
|
|
213
|
+
actionCatalogue: "runtime.actions",
|
|
214
|
+
events: "runtime.events"
|
|
215
|
+
},
|
|
216
|
+
setup({ actionCatalogue, events }) {
|
|
217
|
+
actions = actionCatalogue;
|
|
218
|
+
events.register({ id: "test.capture", handle: (event) => received.push(event) });
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
const runtime = createCapabilityRuntime({
|
|
223
|
+
providers: [EventProvider, createActionProvider(), feature, listener]
|
|
224
|
+
});
|
|
225
|
+
await runtime.start();
|
|
226
|
+
|
|
227
|
+
await actions.execute({
|
|
228
|
+
actionId: "books.save",
|
|
229
|
+
input: {},
|
|
230
|
+
context: { channel: "api", surface: "app", actor: { id: "7" } }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
assert.equal(received.length, 1);
|
|
234
|
+
assert.equal(received[0].type, "entity.changed");
|
|
235
|
+
assert.equal(received[0].entityId, "7");
|
|
236
|
+
assert.equal(received[0].realtime.event, "books.changed");
|
|
237
|
+
assert.equal(received[0].realtime.audience, "actor_user");
|
|
238
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineFeature } from "./defineFeature.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineProvider } from "../../shared/capabilities/defineProvider.js";
|
|
2
|
+
import { createCapabilityHttpRuntime } from "./capabilityHttpRuntime.js";
|
|
3
|
+
|
|
4
|
+
const HttpProvider = defineProvider({
|
|
5
|
+
id: "runtime.http",
|
|
6
|
+
requires: {
|
|
7
|
+
actions: "runtime.actions",
|
|
8
|
+
fastify: "runtime.fastify"
|
|
9
|
+
},
|
|
10
|
+
provides: {
|
|
11
|
+
http: "runtime.http"
|
|
12
|
+
},
|
|
13
|
+
setup({ actions, fastify }) {
|
|
14
|
+
return {
|
|
15
|
+
http: createCapabilityHttpRuntime({ actions, fastify })
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
boot(_dependencies, { outputs }) {
|
|
19
|
+
outputs.http.start();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export { HttpProvider };
|