@pulse-compute/provider-node 0.0.0 → 1.0.0-beta.1
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 +60 -1
- package/package.json +61 -6
- package/src/capabilities.js +56 -0
- package/src/compiler/assets-provider-proof.js +434 -0
- package/src/compiler/backend-json-request-body-proof.js +90 -0
- package/src/compiler/index.js +38 -0
- package/src/compiler/node-adapter.js +452 -0
- package/src/compiler/node-compiled-wasm-assets-lifecycle-proof.js +435 -0
- package/src/compiler/request-json-body-proof.js +181 -0
- package/src/compiler/route-handler-compiled-wasm-bridge.js +323 -0
- package/src/compiler/route-handler-effect-proof.js +215 -0
- package/src/compiler/route-handler-live-origin-proof.js +372 -0
- package/src/compiler/schema-decode-result-proof.js +180 -0
- package/src/compiler/schema-response-codec-proof.js +149 -0
- package/src/events/reference-adapter.js +145 -0
- package/src/index.js +57 -0
- package/src/javascript/assets-adapter.js +68 -0
- package/src/javascript/bindings-adapter.js +120 -0
- package/src/javascript/fetch-adapter.js +187 -0
- package/src/javascript/grip-broadcast.js +243 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +69 -0
- package/src/javascript/node-adapter.js +281 -0
- package/src/javascript/package-effects.js +78 -0
- package/src/javascript/runtime-host.js +165 -0
- package/src/javascript/source-package.js +439 -0
- package/src/javascript/support.js +205 -0
- package/src/javascript/target-support-policy.js +134 -0
- package/src/javascript/target.js +37 -0
- package/src/javascript/test-runtime.js +111 -0
- package/src/native/target.js +78 -0
- package/src/runtime/assets-provider.js +776 -0
- package/src/runtime/canonical-api-runtime.js +632 -0
- package/src/runtime/compiled-wasm-node-adapter-kv.js +15 -0
- package/src/runtime/index.js +17 -0
- package/src/runtime/jwt-verifier.js +264 -0
- package/src/runtime/route-handler-effects.js +757 -0
- package/src/toolchain.js +258 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
function loadSchemaResponseContracts() {
|
|
7
|
+
const local = path.resolve(__dirname, '../../../../wasm/packages/contracts/src/handler/schema-response-codec.js');
|
|
8
|
+
if (fs.existsSync(local)) return require(local);
|
|
9
|
+
return require('@pulse-compute/wasm-contracts/handler/schema-response-codec');
|
|
10
|
+
}
|
|
11
|
+
function loadDiagnostics() {
|
|
12
|
+
const local = path.resolve(__dirname, '../../../../wasm/packages/contracts/src/diagnostics.js');
|
|
13
|
+
if (fs.existsSync(local)) return require(local);
|
|
14
|
+
return require('@pulse-compute/wasm-contracts/diagnostics');
|
|
15
|
+
}
|
|
16
|
+
function loadArtifactsDir() {
|
|
17
|
+
const local = path.resolve(__dirname, '../../../../wasm/packages/build-support/src/artifacts-dir.js');
|
|
18
|
+
if (fs.existsSync(local)) return require(local);
|
|
19
|
+
return require('@pulse-compute/wasm-build-support/artifacts-dir');
|
|
20
|
+
}
|
|
21
|
+
const schemaResponse = loadSchemaResponseContracts();
|
|
22
|
+
const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = loadDiagnostics();
|
|
23
|
+
const { getDefaultArtifactsDir } = loadArtifactsDir();
|
|
24
|
+
const { createCompiledHandlerBridgeRuntime, findCompiledHandlerWasm } = require('./route-handler-compiled-wasm-bridge.js');
|
|
25
|
+
|
|
26
|
+
const phaseName = 'node-schema-response-codec-proof';
|
|
27
|
+
function clone(value) { if (Array.isArray(value)) return value.map(clone); if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, clone(v)])); return value; }
|
|
28
|
+
function payload(value) { return value && value.artifact && typeof value.artifact === 'object' ? value.artifact : value; }
|
|
29
|
+
function normalizePlan(input) { const plan = payload(input); if (!plan || typeof plan !== 'object') return undefined; return { ...plan, routes: Array.isArray(plan.routes) ? plan.routes.map(clone) : [] }; }
|
|
30
|
+
function responseRoutes(plan) { return (Array.isArray(plan && plan.routes) ? plan.routes : []).filter((route) => route && Array.isArray(route.finalResponses) && route.finalResponses.length > 0); }
|
|
31
|
+
function makeDiagnostic(code, message, severity, details, hint) { return normalizeDiagnostic({ phase: phaseName, severity: severity || 'error', code, message, hint, details, loc: { file: '<node-schema-response-codec-proof>' } }); }
|
|
32
|
+
function sanitize(value) { if (Array.isArray(value)) return value.map(sanitize); if (!value || typeof value !== 'object') return value; const out = {}; for (const [key, entry] of Object.entries(value)) { if (key === 'body') { out.body = String(entry || ''); out.bodyPreview = String(entry || '').slice(0, 240); } else out[key] = sanitize(entry); } return out; }
|
|
33
|
+
function check(name, ok, details, failures, diagnostics) { const entry = { name, status: ok ? 'ok' : 'error', details: sanitize(details || {}) }; if (!ok) { failures.push(entry); diagnostics.push(makeDiagnostic(schemaResponse.SCHEMA_RESPONSE_CODEC_DIAGNOSTICS.compiledSmokeFailed, `Node schema response codec proof check failed: ${name}.`, 'error', entry.details)); } return entry; }
|
|
34
|
+
function requestJsonRoutes(requestJsonBodyPlan) { const plan = normalizePlan(requestJsonBodyPlan); return (Array.isArray(plan && plan.routes) ? plan.routes : []).filter((route) => route.required); }
|
|
35
|
+
function routeIndexFor(requestJsonRoutesList, route) { return requestJsonRoutesList.findIndex((entry) => entry.path === route.path && String(entry.method).toUpperCase() === String(route.method).toUpperCase()); }
|
|
36
|
+
function runRequestJsonRoute(runtime, index, variant) { const suffix = variant === 'invalid' ? '_invalid' : variant === 'schema-missing' ? '_schema_missing' : ''; const fn = runtime.exports[`bridge_run_request_json_route_${index}${suffix}`]; if (typeof fn !== 'function') throw new Error(`Missing request JSON bridge export bridge_run_request_json_route_${index}${suffix}.`); const status = fn(); return { status, result: runtime.readResult(), bodyReadCount: runtime.requestBodyReadCount(), parseCount: runtime.requestJsonParseCount() }; }
|
|
37
|
+
|
|
38
|
+
function buildNodeSchemaResponseCodecProof(options = {}) {
|
|
39
|
+
const cwd = options.cwd || process.cwd();
|
|
40
|
+
const outDir = options.outDir || getDefaultArtifactsDir(cwd);
|
|
41
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
42
|
+
const diagnostics = [];
|
|
43
|
+
const failures = [];
|
|
44
|
+
const checks = [];
|
|
45
|
+
const plan = normalizePlan(options.schemaResponseCodecPlan || options.plan);
|
|
46
|
+
if (!plan) diagnostics.push(makeDiagnostic(schemaResponse.SCHEMA_RESPONSE_CODEC_DIAGNOSTICS.planMissing, 'Node schema response codec proof requires schema-response-codec-plan.json.', 'error', { hint: 'Enable schema-response-codec-plan before requesting the Node proof.' }));
|
|
47
|
+
const routes = responseRoutes(plan || {}).filter((route) => route.role === 'route');
|
|
48
|
+
if (plan && routes.length === 0) diagnostics.push(makeDiagnostic(schemaResponse.SCHEMA_RESPONSE_CODEC_DIAGNOSTICS.noSchemaResponseRoutes, 'Node schema response codec proof requires at least one route using ctx.result.json("Schema", ref).', 'error', { routes: (plan.routes || []).length }));
|
|
49
|
+
const requestRoutes = requestJsonRoutes(options.requestJsonBodyPlan);
|
|
50
|
+
const wasmFile = findCompiledHandlerWasm(options.compiledHandlers, cwd, outDir, options.compiledHandlersCwd || options.sourceCwd);
|
|
51
|
+
if (!wasmFile) diagnostics.push(makeDiagnostic(schemaResponse.SCHEMA_RESPONSE_CODEC_DIAGNOSTICS.wasmMissing, 'Node schema response codec proof requires the compiled-handler Wasm artifact.', 'error', { hint: 'Enable compiled handlers together with the schema response codec proof.' }));
|
|
52
|
+
const smoke = { results: {}, checks, failedChecks: failures };
|
|
53
|
+
let runtime;
|
|
54
|
+
if (wasmFile && diagnostics.filter((entry) => (entry.severity || 'error') === 'error').length === 0) {
|
|
55
|
+
try {
|
|
56
|
+
runtime = createCompiledHandlerBridgeRuntime(wasmFile);
|
|
57
|
+
for (let i = 0; i < routes.length; i += 1) {
|
|
58
|
+
const route = routes[i];
|
|
59
|
+
const requestJsonRouteIndex = routeIndexFor(requestRoutes, route);
|
|
60
|
+
if (requestJsonRouteIndex < 0) {
|
|
61
|
+
diagnostics.push(makeDiagnostic(schemaResponse.SCHEMA_RESPONSE_CODEC_DIAGNOSTICS.compiledSmokeFailed, `Could not map schema response route ${route.method} ${route.path} to a request-json bridge route.`, 'error', { route }));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const valid = runRequestJsonRoute(runtime, requestJsonRouteIndex, 'valid');
|
|
65
|
+
smoke.results[`route${i}`] = sanitize({ route, requestJsonRouteIndex, valid });
|
|
66
|
+
checks.push(check(`schema_response_route_${i}_status`, valid.status >= 200 && valid.status < 300, valid, failures, diagnostics));
|
|
67
|
+
checks.push(check(`schema_response_route_${i}_json_kind`, valid.result.kind === 'json-text', valid.result, failures, diagnostics));
|
|
68
|
+
checks.push(check(`schema_response_route_${i}_content_type`, (valid.result.headers || []).some(([name, value]) => String(name).toLowerCase() === 'content-type' && String(value).includes('application/json')), valid.result, failures, diagnostics));
|
|
69
|
+
checks.push(check(`schema_response_route_${i}_body_encoded_from_ref`, /body-123/.test(valid.result.body || '') && /Body User/.test(valid.result.body || ''), valid.result, failures, diagnostics));
|
|
70
|
+
checks.push(check(`schema_response_route_${i}_lazy_read_once`, valid.bodyReadCount === 1, valid, failures, diagnostics));
|
|
71
|
+
checks.push(check(`schema_response_route_${i}_parse_once`, valid.parseCount === 1, valid, failures, diagnostics));
|
|
72
|
+
const missing = runRequestJsonRoute(runtime, requestJsonRouteIndex, 'schema-missing');
|
|
73
|
+
smoke.results[`route${i}Missing`] = sanitize({ route, requestJsonRouteIndex, missing });
|
|
74
|
+
checks.push(check(`schema_response_route_${i}_missing_field_status`, missing.status === 400, missing, failures, diagnostics));
|
|
75
|
+
checks.push(check(`schema_response_route_${i}_missing_field_error`, /schema_decode_failed/.test(missing.result.body || ''), missing.result, failures, diagnostics));
|
|
76
|
+
}
|
|
77
|
+
checks.push(check('schema_response_module_has_no_js_handler_imports', runtime.imports.filter((entry) => entry.module === 'pulsewasm_handlers').length === 0, { imports: runtime.imports }, failures, diagnostics));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
diagnostics.push(makeDiagnostic(schemaResponse.SCHEMA_RESPONSE_CODEC_DIAGNOSTICS.compiledSmokeFailed, 'Node schema response codec proof smoke execution failed.', 'error', { message: error && error.message ? error.message : String(error), stack: error && error.stack ? error.stack : undefined }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const errorDiagnostics = diagnostics.filter((entry) => (entry.severity || 'error') === 'error');
|
|
83
|
+
const compiledHandlerFiles = Array.isArray(options.compiledHandlers && options.compiledHandlers.files) ? options.compiledHandlers.files : [];
|
|
84
|
+
const compiledHandlerOutputs = Array.isArray(options.compiledHandlers && options.compiledHandlers.outputFiles) ? options.compiledHandlers.outputFiles : [];
|
|
85
|
+
const artifact = normalizeArtifact({
|
|
86
|
+
version: schemaResponse.NODE_SCHEMA_RESPONSE_CODEC_PROOF_VERSION,
|
|
87
|
+
generatedBy,
|
|
88
|
+
phase: schemaResponse.NODE_SCHEMA_RESPONSE_CODEC_PROOF_PHASE,
|
|
89
|
+
artifact: schemaResponse.NODE_SCHEMA_RESPONSE_CODEC_PROOF_ARTIFACT,
|
|
90
|
+
contractId: schemaResponse.SCHEMA_RESPONSE_CODEC_CONTRACT_ID,
|
|
91
|
+
provider: 'node',
|
|
92
|
+
status: errorDiagnostics.length === 0 ? 'ok' : 'error',
|
|
93
|
+
sourcePlanVersion: plan && plan.version,
|
|
94
|
+
sourcePlanArtifact: plan && plan.artifact,
|
|
95
|
+
schemaResponseCodecPlanConnected: Boolean(plan),
|
|
96
|
+
requestJsonBodyPlanConnected: Boolean(options.requestJsonBodyPlan),
|
|
97
|
+
compiledHandlersConnected: Boolean(options.compiledHandlers),
|
|
98
|
+
runtime: { version: schemaResponse.NODE_SCHEMA_RESPONSE_CODEC_RUNTIME_VERSION, package: '@pulse-compute/wasm-runtime-core-as/compiler/compiled-handlers', wasm: wasmFile ? path.relative(cwd, wasmFile).replace(/\\/g, '/') : undefined },
|
|
99
|
+
scope: schemaResponse.defaultNodeSchemaResponseCodecProofPolicy(),
|
|
100
|
+
surface: JSON.parse(JSON.stringify(schemaResponse.SCHEMA_RESPONSE_CODEC_SURFACE)),
|
|
101
|
+
valueKinds: JSON.parse(JSON.stringify(schemaResponse.SCHEMA_RESPONSE_CODEC_VALUE_KINDS)),
|
|
102
|
+
reserved: [...schemaResponse.SCHEMA_RESPONSE_CODEC_RESERVED_SURFACE],
|
|
103
|
+
compiledWasmRuntimeImplemented: errorDiagnostics.length === 0,
|
|
104
|
+
nodeCompiledProofImplemented: errorDiagnostics.length === 0,
|
|
105
|
+
providerNetworkFetchRequired: false,
|
|
106
|
+
backendJsonRequestBodies: false,
|
|
107
|
+
requestBodyEffects: false,
|
|
108
|
+
automaticSchemaGeneration: false,
|
|
109
|
+
typedSchemaSpecificAccessors: false,
|
|
110
|
+
arbitraryJsObjectInference: false,
|
|
111
|
+
binaryRequestBodyParsing: false,
|
|
112
|
+
streamRequestBodyParsing: false,
|
|
113
|
+
asyncAwait: false,
|
|
114
|
+
promises: false,
|
|
115
|
+
asyncify: false,
|
|
116
|
+
routes: routes.map((route) => ({ routeId: route.routeId, method: route.method, path: route.path, handlerName: route.handlerName, finalResponses: route.finalResponses })),
|
|
117
|
+
smoke,
|
|
118
|
+
summary: {
|
|
119
|
+
schemaResponseRoutes: routes.length,
|
|
120
|
+
finalJsonResponses: routes.reduce((sum, route) => sum + route.finalResponses.length, 0),
|
|
121
|
+
compiled: Boolean(wasmFile) && errorDiagnostics.length === 0,
|
|
122
|
+
executed: checks.length > 0 && errorDiagnostics.length === 0,
|
|
123
|
+
checks: checks.length,
|
|
124
|
+
failedChecks: failures.length,
|
|
125
|
+
jsonResultValidated: checks.some((entry) => entry.name.includes('json_kind') && entry.status === 'ok'),
|
|
126
|
+
contentTypeValidated: checks.some((entry) => entry.name.includes('content_type') && entry.status === 'ok'),
|
|
127
|
+
encodeFromDecodeRefValidated: checks.some((entry) => entry.name.includes('body_encoded_from_ref') && entry.status === 'ok'),
|
|
128
|
+
lazyReadValidated: checks.some((entry) => entry.name.includes('lazy_read_once') && entry.status === 'ok'),
|
|
129
|
+
parseCacheValidated: checks.some((entry) => entry.name.includes('parse_once') && entry.status === 'ok'),
|
|
130
|
+
missingFieldValidated: checks.some((entry) => entry.name.includes('missing_field_error') && entry.status === 'ok'),
|
|
131
|
+
noJsHandlerImports: checks.some((entry) => entry.name === 'schema_response_module_has_no_js_handler_imports' && entry.status === 'ok'),
|
|
132
|
+
compiledWasmRuntimeImplemented: errorDiagnostics.length === 0,
|
|
133
|
+
nodeCompiledProofImplemented: errorDiagnostics.length === 0,
|
|
134
|
+
backendJsonRequestBodies: false,
|
|
135
|
+
requestBodyEffects: false,
|
|
136
|
+
automaticSchemaGeneration: false,
|
|
137
|
+
arbitraryJsObjectInference: false,
|
|
138
|
+
diagnostics: diagnostics.length,
|
|
139
|
+
errorDiagnostics: errorDiagnostics.length,
|
|
140
|
+
errors: errorDiagnostics.length
|
|
141
|
+
},
|
|
142
|
+
generatedFiles: compiledHandlerFiles.map((file) => ({ file: file.file, bytes: file.bytes, sha256: file.sha256 })),
|
|
143
|
+
generatedOutputs: compiledHandlerOutputs.map((file) => ({ file: file.file, kind: file.kind, bytes: file.bytes, sha256: file.sha256 })),
|
|
144
|
+
diagnostics
|
|
145
|
+
}, cwd);
|
|
146
|
+
return { artifact, diagnostics, failures, files: compiledHandlerFiles, outputFiles: compiledHandlerOutputs.length ? compiledHandlerOutputs : (wasmFile ? [{ file: path.relative(cwd, wasmFile).replace(/\\/g, '/'), kind: 'compiled-handler-wasm', bytes: fs.statSync(wasmFile).size }] : []), wasmFile };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { phaseName, buildNodeSchemaResponseCodecProof };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
|
|
5
|
+
const NODE_EVENT_REFERENCE_ADAPTER_VERSION = 'pulse.node-event-reference-adapter.v1';
|
|
6
|
+
const DEFAULT_MAX_QUEUE_DEPTH = 1024;
|
|
7
|
+
|
|
8
|
+
const NODE_EVENT_ADAPTER_CODES = Object.freeze({
|
|
9
|
+
OPTIONS_INVALID: 'PULSE_NODE_EVENT_ADAPTER_OPTIONS_INVALID',
|
|
10
|
+
QUEUE_FULL: 'PULSE_NODE_EVENT_QUEUE_FULL',
|
|
11
|
+
ACCEPTANCE_FULL: runtimeHost.EVENT_EMIT_CODES.QUEUE_FULL,
|
|
12
|
+
INSTANCE_BUSY: 'PULSE_NODE_EVENT_INSTANCE_BUSY',
|
|
13
|
+
EXECUTOR_INVALID: 'PULSE_NODE_EVENT_EXECUTOR_INVALID',
|
|
14
|
+
CANCELLED: 'PULSE_NODE_EVENT_CANCELLED'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function adapterError(code, message, detail = {}) {
|
|
18
|
+
return new runtimeHost.PulseRuntimeContractError(code, message, {
|
|
19
|
+
detail: Object.freeze({ ...detail })
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireOptions(input) {
|
|
24
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
25
|
+
throw adapterError(NODE_EVENT_ADAPTER_CODES.OPTIONS_INVALID, 'Pulse Node event adapter options must be a data object.');
|
|
26
|
+
}
|
|
27
|
+
const prototype = Object.getPrototypeOf(input);
|
|
28
|
+
const unknown = Object.keys(input).filter((key) => key !== 'id' && key !== 'maxQueueDepth').sort();
|
|
29
|
+
if ((prototype !== Object.prototype && prototype !== null) || unknown.length > 0) {
|
|
30
|
+
throw adapterError(
|
|
31
|
+
NODE_EVENT_ADAPTER_CODES.OPTIONS_INVALID,
|
|
32
|
+
'Pulse Node event adapter options contain unsupported fields.',
|
|
33
|
+
{ unknown }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return input;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createNodeEventReferenceAdapter(options = {}) {
|
|
40
|
+
const input = requireOptions(options);
|
|
41
|
+
const id = input.id === undefined ? 'pulse.node-event-reference' : input.id;
|
|
42
|
+
const maxQueueDepth = input.maxQueueDepth === undefined ? DEFAULT_MAX_QUEUE_DEPTH : input.maxQueueDepth;
|
|
43
|
+
const outbound = runtimeHost.createEventRecordingAdapter({ id, maxQueueDepth });
|
|
44
|
+
const descriptor = outbound.eventAdapter;
|
|
45
|
+
const ingress = [];
|
|
46
|
+
let draining = false;
|
|
47
|
+
let activeInvocations = 0;
|
|
48
|
+
let maximumActiveInvocations = 0;
|
|
49
|
+
let enqueuedCount = 0;
|
|
50
|
+
let completedCount = 0;
|
|
51
|
+
|
|
52
|
+
function requireNotCancelled(signal, message) {
|
|
53
|
+
if (signal && signal.aborted) {
|
|
54
|
+
throw adapterError(NODE_EVENT_ADAPTER_CODES.CANCELLED, message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function enqueue(inputFrame, execution = {}) {
|
|
59
|
+
requireNotCancelled(execution.signal, 'Pulse Node event ingress was cancelled before host acceptance.');
|
|
60
|
+
if (ingress.length >= descriptor.maxQueueDepth) {
|
|
61
|
+
throw adapterError(
|
|
62
|
+
NODE_EVENT_ADAPTER_CODES.QUEUE_FULL,
|
|
63
|
+
`Pulse Node event adapter ${JSON.stringify(descriptor.id)} reached its bounded ingress queue capacity.`,
|
|
64
|
+
{ maxQueueDepth: descriptor.maxQueueDepth }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const normalizer = runtimeHost.createEventRecordingAdapter({
|
|
68
|
+
id: 'pulse.node-event-ingress-normalizer',
|
|
69
|
+
maxQueueDepth: 1
|
|
70
|
+
});
|
|
71
|
+
normalizer.dispatch({ kind: 'event.emit', capability: 'event.emit', frame: inputFrame });
|
|
72
|
+
const frame = normalizer.acceptedFrames()[0];
|
|
73
|
+
ingress.push(frame);
|
|
74
|
+
enqueuedCount += 1;
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function acceptOutbound(inputFrame, execution = {}) {
|
|
79
|
+
requireNotCancelled(execution.signal, 'Pulse Node event emit was cancelled before host acceptance.');
|
|
80
|
+
return outbound.dispatch({ kind: 'event.emit', capability: 'event.emit', frame: inputFrame });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function drain(executor, execution = {}) {
|
|
84
|
+
if (typeof executor !== 'function') {
|
|
85
|
+
throw adapterError(NODE_EVENT_ADAPTER_CODES.EXECUTOR_INVALID, 'Pulse Node event adapter drain requires an executor function.');
|
|
86
|
+
}
|
|
87
|
+
if (draining) {
|
|
88
|
+
throw adapterError(
|
|
89
|
+
NODE_EVENT_ADAPTER_CODES.INSTANCE_BUSY,
|
|
90
|
+
`Pulse Node event adapter ${JSON.stringify(descriptor.id)} already owns an active instance drain.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
draining = true;
|
|
94
|
+
const outcomes = [];
|
|
95
|
+
try {
|
|
96
|
+
while (ingress.length > 0) {
|
|
97
|
+
const frame = ingress.shift();
|
|
98
|
+
activeInvocations += 1;
|
|
99
|
+
maximumActiveInvocations = Math.max(maximumActiveInvocations, activeInvocations);
|
|
100
|
+
let result;
|
|
101
|
+
try {
|
|
102
|
+
result = await executor(frame, Object.freeze({ signal: execution.signal, adapter: api }));
|
|
103
|
+
} finally {
|
|
104
|
+
activeInvocations -= 1;
|
|
105
|
+
}
|
|
106
|
+
completedCount += 1;
|
|
107
|
+
outcomes.push(Object.freeze({ frame, result }));
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze(outcomes);
|
|
110
|
+
} finally {
|
|
111
|
+
draining = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const api = Object.freeze({
|
|
116
|
+
version: NODE_EVENT_REFERENCE_ADAPTER_VERSION,
|
|
117
|
+
id: descriptor.id,
|
|
118
|
+
eventAdapter: descriptor,
|
|
119
|
+
enqueue,
|
|
120
|
+
acceptOutbound,
|
|
121
|
+
emit: acceptOutbound,
|
|
122
|
+
drain,
|
|
123
|
+
pendingFrames() { return Object.freeze([...ingress]); },
|
|
124
|
+
acceptedFrames() { return outbound.acceptedFrames(); },
|
|
125
|
+
summary() {
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
enqueued: enqueuedCount,
|
|
128
|
+
completed: completedCount,
|
|
129
|
+
pending: ingress.length,
|
|
130
|
+
acceptedOutbound: outbound.acceptedFrames().length,
|
|
131
|
+
activeInvocations,
|
|
132
|
+
maximumActiveInvocations,
|
|
133
|
+
draining,
|
|
134
|
+
automaticLoopback: false
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return api;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = Object.freeze({
|
|
142
|
+
NODE_EVENT_REFERENCE_ADAPTER_VERSION,
|
|
143
|
+
NODE_EVENT_ADAPTER_CODES,
|
|
144
|
+
createNodeEventReferenceAdapter
|
|
145
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const compiler = require('./compiler');
|
|
4
|
+
const runtime = require('./runtime');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
compiler,
|
|
8
|
+
runtime,
|
|
9
|
+
nodeAdapter: compiler.nodeAdapter,
|
|
10
|
+
assetsProviderProof: compiler.assetsProviderProof,
|
|
11
|
+
nodeCompiledWasmAssetsLifecycleProof: compiler.nodeCompiledWasmAssetsLifecycleProof,
|
|
12
|
+
routeHandlerEffectProof: compiler.routeHandlerEffectProof,
|
|
13
|
+
routeHandlerLiveOriginProof: compiler.routeHandlerLiveOriginProof,
|
|
14
|
+
routeHandlerCompiledWasmBridge: compiler.routeHandlerCompiledWasmBridge,
|
|
15
|
+
requestJsonBodyProof: compiler.requestJsonBodyProof,
|
|
16
|
+
schemaDecodeResultProof: compiler.schemaDecodeResultProof,
|
|
17
|
+
schemaResponseCodecProof: compiler.schemaResponseCodecProof,
|
|
18
|
+
backendJsonRequestBodyProof: compiler.backendJsonRequestBodyProof,
|
|
19
|
+
assetsProvider: runtime.assetsProvider,
|
|
20
|
+
routeHandlerEffects: runtime.routeHandlerEffects,
|
|
21
|
+
canonicalApiRuntime: runtime.canonicalApiRuntime,
|
|
22
|
+
compiledWasmNodeAdapterKv: runtime.compiledWasmNodeAdapterKv,
|
|
23
|
+
buildNodeAdapter: compiler.buildNodeAdapter,
|
|
24
|
+
buildNodeAdapterSource: compiler.buildNodeAdapterSource,
|
|
25
|
+
buildNodeAssetsProviderProof: compiler.buildNodeAssetsProviderProof,
|
|
26
|
+
buildNodeCompiledWasmAssetsLifecycleProof: compiler.buildNodeCompiledWasmAssetsLifecycleProof,
|
|
27
|
+
buildNodeRouteHandlerEffectProof: compiler.buildNodeRouteHandlerEffectProof,
|
|
28
|
+
buildNodeLiveOriginRouteHandlerEffectProof: compiler.buildNodeLiveOriginRouteHandlerEffectProof,
|
|
29
|
+
buildNodeCompiledRouteHandlerEffectBridge: compiler.buildNodeCompiledRouteHandlerEffectBridge,
|
|
30
|
+
buildNodeRequestJsonBodyProof: compiler.buildNodeRequestJsonBodyProof,
|
|
31
|
+
buildNodeSchemaDecodeResultProof: compiler.buildNodeSchemaDecodeResultProof,
|
|
32
|
+
buildNodeSchemaResponseCodecProof: compiler.buildNodeSchemaResponseCodecProof,
|
|
33
|
+
buildNodeBackendJsonRequestBodyProof: compiler.buildNodeBackendJsonRequestBodyProof,
|
|
34
|
+
buildNodeCompiledWasmRouteHandlerEffectBridge: compiler.buildNodeCompiledWasmRouteHandlerEffectBridge,
|
|
35
|
+
createPulseWasmCompiledNodeAdapterWithKv: runtime.createPulseWasmCompiledNodeAdapterWithKv,
|
|
36
|
+
createNodeAssetsProvider: runtime.createNodeAssetsProvider,
|
|
37
|
+
createNodeCompiledWasmAssetsLifecycleRuntime: runtime.createNodeCompiledWasmAssetsLifecycleRuntime,
|
|
38
|
+
createDefaultStoresFromPlan: runtime.createDefaultStoresFromPlan,
|
|
39
|
+
createNodeAssetsHandler: runtime.createNodeAssetsHandler,
|
|
40
|
+
createNodeRouteHandlerEffectProvider: runtime.createNodeRouteHandlerEffectProvider,
|
|
41
|
+
createCanonicalNodeRuntime: runtime.createCanonicalNodeRuntime,
|
|
42
|
+
executeCanonicalProgram: runtime.executeCanonicalProgram,
|
|
43
|
+
createNodeLiveOriginRouteHandlerEffectProvider: runtime.createNodeLiveOriginRouteHandlerEffectProvider,
|
|
44
|
+
createDefaultBackendsFromPlan: runtime.createDefaultBackendsFromPlan,
|
|
45
|
+
createDefaultRouteEffectBackendsFromPlan: runtime.createDefaultBackendsFromPlan,
|
|
46
|
+
normalizeLiveOriginsFromResolvedConfig: runtime.normalizeLiveOriginsFromResolvedConfig,
|
|
47
|
+
normalizeHeaders: runtime.normalizeHeaders,
|
|
48
|
+
normalizeRouteEffectHeaders: runtime.normalizeHeaders,
|
|
49
|
+
createNodeHandler: runtime.createNodeHandler,
|
|
50
|
+
createNodeServer: runtime.createNodeServer,
|
|
51
|
+
nodeRequestToHostRequest: runtime.nodeRequestToHostRequest,
|
|
52
|
+
normalizeNodeHeaders: runtime.normalizeNodeHeaders,
|
|
53
|
+
normalizeRawHeaders: runtime.normalizeRawHeaders,
|
|
54
|
+
normalizeNodePath: runtime.normalizeNodePath,
|
|
55
|
+
writeNodeResponse: runtime.writeNodeResponse,
|
|
56
|
+
groupHeaders: runtime.groupHeaders
|
|
57
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
|
|
5
|
+
const NODE_JAVASCRIPT_ASSETS_ADAPTER_VERSION = 'pulse.node-javascript-assets-adapter.v1';
|
|
6
|
+
|
|
7
|
+
function contentTypeForKey(key) {
|
|
8
|
+
const extension = String(key).toLowerCase().split('.').pop();
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
css: 'text/css; charset=utf-8',
|
|
11
|
+
html: 'text/html; charset=utf-8',
|
|
12
|
+
js: 'text/javascript; charset=utf-8',
|
|
13
|
+
json: 'application/json; charset=utf-8',
|
|
14
|
+
mjs: 'text/javascript; charset=utf-8',
|
|
15
|
+
svg: 'image/svg+xml',
|
|
16
|
+
txt: 'text/plain; charset=utf-8',
|
|
17
|
+
webp: 'image/webp',
|
|
18
|
+
png: 'image/png',
|
|
19
|
+
jpg: 'image/jpeg',
|
|
20
|
+
jpeg: 'image/jpeg',
|
|
21
|
+
gif: 'image/gif',
|
|
22
|
+
ico: 'image/x-icon'
|
|
23
|
+
})[extension] || 'application/octet-stream';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function responseHeaders(payload, found) {
|
|
27
|
+
const headers = new Headers(payload && payload.headers);
|
|
28
|
+
if (!headers.has('content-type')) {
|
|
29
|
+
headers.set(
|
|
30
|
+
'content-type',
|
|
31
|
+
found ? contentTypeForKey(payload && payload.key) : 'text/plain; charset=utf-8'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (payload && payload.cacheControl !== undefined) {
|
|
35
|
+
headers.set('cache-control', String(payload.cacheControl));
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bodyValue(value) {
|
|
41
|
+
if (value === undefined || value === null) return '';
|
|
42
|
+
if (typeof value === 'string' || Buffer.isBuffer(value) || value instanceof Uint8Array) return value;
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createNodeJavascriptAssetsLookup(bindingCapabilities) {
|
|
47
|
+
if (!bindingCapabilities || typeof bindingCapabilities.kv !== 'function') {
|
|
48
|
+
throw new TypeError('Node JavaScript Assets requires provider-owned KV capabilities.');
|
|
49
|
+
}
|
|
50
|
+
return async function nodeJavascriptAssetsLookup(payload = {}) {
|
|
51
|
+
const store = bindingCapabilities.kv(payload.store);
|
|
52
|
+
const entry = store.get(payload.key);
|
|
53
|
+
const found = entry !== undefined;
|
|
54
|
+
const headers = responseHeaders(payload, found);
|
|
55
|
+
if (!found) return runtimeHost.markOpaqueResponse(new Response(null, { status: 404, headers }));
|
|
56
|
+
const method = String(payload.method || 'GET').toUpperCase();
|
|
57
|
+
return runtimeHost.markOpaqueResponse(new Response(method === 'HEAD' ? null : bodyValue(entry), {
|
|
58
|
+
status: 200,
|
|
59
|
+
headers
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = Object.freeze({
|
|
65
|
+
NODE_JAVASCRIPT_ASSETS_ADAPTER_VERSION,
|
|
66
|
+
contentTypeForKey,
|
|
67
|
+
createNodeJavascriptAssetsLookup
|
|
68
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
|
|
5
|
+
const NODE_JAVASCRIPT_BINDINGS_ADAPTER_VERSION = 'pulse.node-javascript-bindings-adapter.v1';
|
|
6
|
+
|
|
7
|
+
function recordEntries(input, label = 'binding record') {
|
|
8
|
+
if (input === undefined || input === null) return [];
|
|
9
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
10
|
+
throw new TypeError(`Pulse Node JavaScript ${label} must be an object record.`);
|
|
11
|
+
}
|
|
12
|
+
const prototype = Object.getPrototypeOf(input);
|
|
13
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
14
|
+
throw new TypeError(`Pulse Node JavaScript ${label} must be an ordinary object record.`);
|
|
15
|
+
}
|
|
16
|
+
const entries = [];
|
|
17
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
18
|
+
if (typeof key !== 'string') {
|
|
19
|
+
throw new TypeError(`Pulse Node JavaScript ${label} may not contain symbol keys.`);
|
|
20
|
+
}
|
|
21
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
22
|
+
if (!descriptor || !descriptor.enumerable) continue;
|
|
23
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
24
|
+
throw new TypeError(`Pulse Node JavaScript ${label} may only contain enumerable data properties.`);
|
|
25
|
+
}
|
|
26
|
+
entries.push([key, descriptor.value]);
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createNodeJavascriptBindingCapabilities(options = {}) {
|
|
32
|
+
const config = new Map();
|
|
33
|
+
const secrets = new Map();
|
|
34
|
+
const stores = new Map();
|
|
35
|
+
|
|
36
|
+
for (const [rawName, rawValue] of recordEntries(options.config, 'config')) {
|
|
37
|
+
const name = runtimeHost.normalizeBindingName('config', rawName, options);
|
|
38
|
+
config.set(name, runtimeHost.normalizeBindingValue('config.get', name, rawValue, options));
|
|
39
|
+
}
|
|
40
|
+
for (const [rawName, rawValue] of recordEntries(options.secrets, 'secrets')) {
|
|
41
|
+
const name = runtimeHost.normalizeBindingName('secret', rawName, options);
|
|
42
|
+
secrets.set(name, runtimeHost.normalizeBindingValue('secret.get', name, rawValue, options));
|
|
43
|
+
}
|
|
44
|
+
for (const [rawNamespace, values] of recordEntries(options.kv, 'KV namespaces')) {
|
|
45
|
+
const namespace = runtimeHost.normalizeKvNamespace(rawNamespace, options);
|
|
46
|
+
const entries = new Map();
|
|
47
|
+
for (const [rawKey, value] of recordEntries(values, `KV namespace ${JSON.stringify(namespace)}`)) {
|
|
48
|
+
const key = runtimeHost.normalizeKvKey(rawKey, options);
|
|
49
|
+
entries.set(key, runtimeHost.cloneKvValue(value, options));
|
|
50
|
+
}
|
|
51
|
+
stores.set(namespace, entries);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function storeFor(namespaceInput) {
|
|
55
|
+
const namespace = runtimeHost.normalizeKvNamespace(namespaceInput, options);
|
|
56
|
+
if (!stores.has(namespace)) stores.set(namespace, new Map());
|
|
57
|
+
return Object.freeze({ namespace, store: stores.get(namespace) });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
version: NODE_JAVASCRIPT_BINDINGS_ADAPTER_VERSION,
|
|
62
|
+
config(nameInput) {
|
|
63
|
+
const name = runtimeHost.normalizeBindingName('config', nameInput, options);
|
|
64
|
+
return config.has(name) ? config.get(name) : undefined;
|
|
65
|
+
},
|
|
66
|
+
secret(nameInput) {
|
|
67
|
+
const name = runtimeHost.normalizeBindingName('secret', nameInput, options);
|
|
68
|
+
return secrets.has(name) ? secrets.get(name) : undefined;
|
|
69
|
+
},
|
|
70
|
+
kv(namespaceInput) {
|
|
71
|
+
const { store } = storeFor(namespaceInput);
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
get(keyInput) {
|
|
74
|
+
const key = runtimeHost.normalizeKvKey(keyInput, options);
|
|
75
|
+
return runtimeHost.cloneKvValue(store.has(key) ? store.get(key) : undefined, {
|
|
76
|
+
...options,
|
|
77
|
+
allowUndefined: true
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
put(keyInput, value) {
|
|
81
|
+
const key = runtimeHost.normalizeKvKey(keyInput, options);
|
|
82
|
+
store.set(key, runtimeHost.cloneKvValue(value, options));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function withNodeBindingCapabilities(capabilities, options = {}) {
|
|
91
|
+
const value = capabilities && typeof capabilities === 'object' ? capabilities : {};
|
|
92
|
+
const output = { ...value };
|
|
93
|
+
const needsConfig = typeof value.config !== 'function';
|
|
94
|
+
const needsSecret = typeof value.secret !== 'function';
|
|
95
|
+
const needsKv = typeof value.kv !== 'function';
|
|
96
|
+
if (!needsConfig && !needsSecret && !needsKv) return Object.freeze(output);
|
|
97
|
+
const configured = options.bindings || createNodeJavascriptBindingCapabilities(options);
|
|
98
|
+
if (needsConfig) output.config = configured.config;
|
|
99
|
+
if (needsSecret) output.secret = configured.secret;
|
|
100
|
+
if (needsKv) output.kv = configured.kv;
|
|
101
|
+
return Object.freeze(output);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function nodeJavascriptRedactionValues(options = {}) {
|
|
105
|
+
const output = [];
|
|
106
|
+
for (const [, value] of recordEntries(options.secrets, 'secrets')) {
|
|
107
|
+
if (typeof value === 'string' && value.length > 0) output.push(value);
|
|
108
|
+
}
|
|
109
|
+
for (const value of Array.isArray(options.redactionValues) ? options.redactionValues : []) {
|
|
110
|
+
if (typeof value === 'string' && value.length > 0) output.push(value);
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze([...new Set(output)]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = Object.freeze({
|
|
116
|
+
NODE_JAVASCRIPT_BINDINGS_ADAPTER_VERSION,
|
|
117
|
+
createNodeJavascriptBindingCapabilities,
|
|
118
|
+
nodeJavascriptRedactionValues,
|
|
119
|
+
withNodeBindingCapabilities
|
|
120
|
+
});
|