@pulse-compute/runtime 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 +125 -2
- package/docs/API.md +539 -0
- package/docs/guides/routing.md +184 -0
- package/docs/preview-scope.md +142 -0
- package/package.json +38 -5
- package/src/host.d.ts +376 -0
- package/src/host.js +114 -0
- package/src/index.d.ts +189 -0
- package/src/index.js +11 -0
- package/src/internal/bindings.js +281 -0
- package/src/internal/body.js +254 -0
- package/src/internal/context.js +271 -0
- package/src/internal/effect-adapter.js +857 -0
- package/src/internal/errors.js +27 -0
- package/src/internal/event-emission.js +182 -0
- package/src/internal/event-execution.js +495 -0
- package/src/internal/fetch.js +221 -0
- package/src/internal/index.js +24 -0
- package/src/internal/logging.js +65 -0
- package/src/internal/package-runtime.js +597 -0
- package/src/internal/path.js +130 -0
- package/src/internal/redaction.js +231 -0
- package/src/internal/response.js +415 -0
- package/src/internal/router.js +459 -0
- package/src/internal/schema.js +340 -0
- package/src/package.d.ts +127 -0
- package/src/package.js +19 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Portable runtime realization of the repository-owned event contract. This
|
|
4
|
+
// module intentionally exposes direct JavaScript execution only; transports,
|
|
5
|
+
// provider delivery, and Native execution remain out of scope. Outbound event
|
|
6
|
+
// acceptance is a separate execution-owned effect and never loops back here.
|
|
7
|
+
|
|
8
|
+
const { createEventContext } = require('./context.js');
|
|
9
|
+
const { createJavascriptEffectExecution } = require('./effect-adapter.js');
|
|
10
|
+
const { PulseRuntimeContractError, PulseUnhandledError } = require('./errors.js');
|
|
11
|
+
const { reportingLevel } = require('./logging.js');
|
|
12
|
+
const { createRedactionState } = require('./redaction.js');
|
|
13
|
+
const { validateSchemaValue } = require('./schema.js');
|
|
14
|
+
|
|
15
|
+
const EVENT_FRAME_VERSION = 'pulse.event-frame.v1';
|
|
16
|
+
const EVENT_EXECUTION_RESULT_VERSION = 'pulse.event-execution-result.v1';
|
|
17
|
+
const EVENT_REGISTRATION_READER = Symbol.for('pulse.runtime.event-registration-reader.v1');
|
|
18
|
+
const EVENT_DEFAULT_LIMITS = Object.freeze({
|
|
19
|
+
maxTypeBytes: 128,
|
|
20
|
+
maxSchemaIdBytes: 256,
|
|
21
|
+
maxPayloadBytes: 65_536,
|
|
22
|
+
maxPayloadDepth: 32,
|
|
23
|
+
maxPayloadEntries: 4_096,
|
|
24
|
+
maxEvents: 256
|
|
25
|
+
});
|
|
26
|
+
const EVENT_LIMIT_KEYS = Object.freeze(Object.keys(EVENT_DEFAULT_LIMITS));
|
|
27
|
+
const SCHEMA_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$/;
|
|
28
|
+
|
|
29
|
+
const EVENT_RUNTIME_CODES = Object.freeze({
|
|
30
|
+
LIMIT_INVALID: 'PULSE_RUNTIME_EVENT_LIMIT_INVALID',
|
|
31
|
+
LIMIT_EXCEEDED: 'PULSE_RUNTIME_EVENT_LIMIT_EXCEEDED',
|
|
32
|
+
FRAME_INVALID: 'PULSE_RUNTIME_EVENT_FRAME_INVALID',
|
|
33
|
+
TYPE_INVALID: 'PULSE_RUNTIME_EVENT_TYPE_INVALID',
|
|
34
|
+
SCHEMA_ID_INVALID: 'PULSE_RUNTIME_EVENT_SCHEMA_ID_INVALID',
|
|
35
|
+
PAYLOAD_REQUIRED: 'PULSE_RUNTIME_EVENT_PAYLOAD_REQUIRED',
|
|
36
|
+
PAYLOAD_FORBIDDEN: 'PULSE_RUNTIME_EVENT_PAYLOAD_FORBIDDEN',
|
|
37
|
+
PAYLOAD_INVALID: 'PULSE_RUNTIME_EVENT_PAYLOAD_INVALID',
|
|
38
|
+
HANDLER_NOT_FOUND: 'PULSE_RUNTIME_EVENT_HANDLER_NOT_FOUND',
|
|
39
|
+
REGISTRATION_INVALID: 'PULSE_RUNTIME_EVENT_REGISTRATION_INVALID',
|
|
40
|
+
SCHEMA_MISMATCH: 'PULSE_RUNTIME_EVENT_SCHEMA_MISMATCH',
|
|
41
|
+
HANDLER_ASYNC_REQUIRED: 'PULSE_RUNTIME_EVENT_HANDLER_ASYNC_REQUIRED',
|
|
42
|
+
HANDLER_RESULT_INVALID: 'PULSE_RUNTIME_EVENT_HANDLER_RESULT_INVALID',
|
|
43
|
+
CANCELLED: 'PULSE_RUNTIME_EVENT_CANCELLED',
|
|
44
|
+
COMPLETION_DUPLICATE: 'PULSE_RUNTIME_EVENT_COMPLETION_DUPLICATE'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function contractError(code, message, detail) {
|
|
48
|
+
return new PulseRuntimeContractError(code, message, detail === undefined ? {} : { detail: Object.freeze({ ...detail }) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ownDataValue(value, key) {
|
|
52
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function')) return undefined;
|
|
53
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
54
|
+
return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
55
|
+
? descriptor.value
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireDataObject(value, label, code = EVENT_RUNTIME_CODES.FRAME_INVALID) {
|
|
60
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
61
|
+
throw contractError(code, `Pulse ${label} must be a plain data object.`);
|
|
62
|
+
}
|
|
63
|
+
const prototype = Object.getPrototypeOf(value);
|
|
64
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
65
|
+
throw contractError(code, `Pulse ${label} must be a plain data object.`);
|
|
66
|
+
}
|
|
67
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
68
|
+
throw contractError(code, `Pulse ${label} must not contain symbol keys.`);
|
|
69
|
+
}
|
|
70
|
+
for (const key of Object.keys(value)) {
|
|
71
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
72
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
73
|
+
throw contractError(code, `Pulse ${label}.${key} must be an own data property.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requireKnownKeys(value, allowed, label, code = EVENT_RUNTIME_CODES.FRAME_INVALID) {
|
|
80
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key)).sort();
|
|
81
|
+
if (unknown.length > 0) {
|
|
82
|
+
throw contractError(code, `Pulse ${label} contains unsupported fields: ${unknown.join(', ')}.`, { unknown });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function utf8ByteLength(value) {
|
|
87
|
+
return new TextEncoder().encode(String(value)).byteLength;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeEventLimits(input) {
|
|
91
|
+
const source = input === undefined ? {} : input;
|
|
92
|
+
requireDataObject(source, 'event limits', EVENT_RUNTIME_CODES.LIMIT_INVALID);
|
|
93
|
+
requireKnownKeys(source, EVENT_LIMIT_KEYS, 'event limits', EVENT_RUNTIME_CODES.LIMIT_INVALID);
|
|
94
|
+
const limits = {};
|
|
95
|
+
for (const key of EVENT_LIMIT_KEYS) {
|
|
96
|
+
const value = source[key] === undefined ? EVENT_DEFAULT_LIMITS[key] : Number(source[key]);
|
|
97
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
98
|
+
throw contractError(EVENT_RUNTIME_CODES.LIMIT_INVALID, `Pulse event limit ${key} must be a positive safe integer.`, { key, value });
|
|
99
|
+
}
|
|
100
|
+
limits[key] = value;
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze(limits);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeEventType(value, limits) {
|
|
106
|
+
if (typeof value !== 'string' || value.length === 0 || utf8ByteLength(value) > limits.maxTypeBytes) {
|
|
107
|
+
throw contractError(EVENT_RUNTIME_CODES.TYPE_INVALID, 'Pulse event type must be a bounded non-empty string.');
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeEventSchemaId(value, limits) {
|
|
113
|
+
if (value === null) return null;
|
|
114
|
+
if (typeof value !== 'string' || !SCHEMA_ID_PATTERN.test(value) || utf8ByteLength(value) > limits.maxSchemaIdBytes) {
|
|
115
|
+
throw contractError(EVENT_RUNTIME_CODES.SCHEMA_ID_INVALID, 'Pulse event schemaId must be a bounded dotted schema ID or null.');
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function requireDataArray(value, path) {
|
|
121
|
+
if (Object.getOwnPropertySymbols(value).length > 0 || Object.keys(value).some((key) => !/^(?:0|[1-9][0-9]*)$/.test(key))) {
|
|
122
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, `Pulse ${path} must be a dense data-only array.`);
|
|
123
|
+
}
|
|
124
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
125
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
126
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
127
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, `Pulse ${path} must be a dense data-only array.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function cloneEventPayload(input, limits) {
|
|
133
|
+
let entries = 0;
|
|
134
|
+
const ancestors = new Set();
|
|
135
|
+
|
|
136
|
+
function visit(value, path, depth) {
|
|
137
|
+
if (depth > limits.maxPayloadDepth) {
|
|
138
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, 'Pulse event payload exceeds its nesting limit.', { path });
|
|
139
|
+
}
|
|
140
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
141
|
+
if (typeof value === 'number') {
|
|
142
|
+
if (!Number.isFinite(value)) throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, `Pulse ${path} must be finite.`);
|
|
143
|
+
return Object.is(value, -0) ? 0 : value;
|
|
144
|
+
}
|
|
145
|
+
if (!value || typeof value !== 'object') {
|
|
146
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, `Pulse ${path} must contain only JSON data.`);
|
|
147
|
+
}
|
|
148
|
+
if (ancestors.has(value)) throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, 'Pulse event payload must not contain cycles.', { path });
|
|
149
|
+
ancestors.add(value);
|
|
150
|
+
let output;
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
requireDataArray(value, path);
|
|
153
|
+
entries += value.length;
|
|
154
|
+
output = value.map((entry, index) => visit(entry, `${path}[${index}]`, depth + 1));
|
|
155
|
+
} else {
|
|
156
|
+
requireDataObject(value, path, EVENT_RUNTIME_CODES.PAYLOAD_INVALID);
|
|
157
|
+
const keys = Object.keys(value).sort();
|
|
158
|
+
entries += keys.length;
|
|
159
|
+
output = Object.fromEntries(keys.map((key) => [key, visit(ownDataValue(value, key), `${path}.${key}`, depth + 1)]));
|
|
160
|
+
}
|
|
161
|
+
ancestors.delete(value);
|
|
162
|
+
if (entries > limits.maxPayloadEntries) {
|
|
163
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, 'Pulse event payload exceeds its entry limit.', { entries });
|
|
164
|
+
}
|
|
165
|
+
for (const child of Object.values(output)) {
|
|
166
|
+
if (child && typeof child === 'object' && !Object.isFrozen(child)) Object.freeze(child);
|
|
167
|
+
}
|
|
168
|
+
return Object.freeze(output);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const payload = visit(input, 'event payload', 0);
|
|
172
|
+
const bytes = utf8ByteLength(JSON.stringify(payload));
|
|
173
|
+
if (bytes > limits.maxPayloadBytes) {
|
|
174
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_INVALID, 'Pulse event payload exceeds its UTF-8 JSON byte limit.', { bytes });
|
|
175
|
+
}
|
|
176
|
+
return payload;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeEventFrame(input, limitsInput) {
|
|
180
|
+
const limits = normalizeEventLimits(limitsInput);
|
|
181
|
+
requireDataObject(input, 'event frame');
|
|
182
|
+
requireKnownKeys(input, ['version', 'type', 'schemaId', 'payload'], 'event frame');
|
|
183
|
+
if (ownDataValue(input, 'version') !== EVENT_FRAME_VERSION || !Object.prototype.hasOwnProperty.call(input, 'schemaId')) {
|
|
184
|
+
throw contractError(EVENT_RUNTIME_CODES.FRAME_INVALID, `Pulse event frame requires ${EVENT_FRAME_VERSION}, type, and schemaId.`);
|
|
185
|
+
}
|
|
186
|
+
const type = normalizeEventType(ownDataValue(input, 'type'), limits);
|
|
187
|
+
const schemaId = normalizeEventSchemaId(ownDataValue(input, 'schemaId'), limits);
|
|
188
|
+
const hasPayload = Object.prototype.hasOwnProperty.call(input, 'payload');
|
|
189
|
+
if (schemaId === null && hasPayload) {
|
|
190
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_FORBIDDEN, 'Pulse no-payload event frames must omit payload.');
|
|
191
|
+
}
|
|
192
|
+
if (schemaId !== null && !hasPayload) {
|
|
193
|
+
throw contractError(EVENT_RUNTIME_CODES.PAYLOAD_REQUIRED, 'Pulse schema-bound event frames require payload.');
|
|
194
|
+
}
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
version: EVENT_FRAME_VERSION,
|
|
197
|
+
type,
|
|
198
|
+
schemaId,
|
|
199
|
+
...(hasPayload ? { payload: cloneEventPayload(ownDataValue(input, 'payload'), limits) } : {})
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function eventRegistrations(application, limits) {
|
|
204
|
+
const descriptor = application && (typeof application === 'object' || typeof application === 'function')
|
|
205
|
+
? Object.getOwnPropertyDescriptor(application, EVENT_REGISTRATION_READER)
|
|
206
|
+
: undefined;
|
|
207
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value') || typeof descriptor.value !== 'function') return Object.freeze([]);
|
|
208
|
+
const entries = descriptor.value();
|
|
209
|
+
if (!Array.isArray(entries)) throw contractError(EVENT_RUNTIME_CODES.REGISTRATION_INVALID, 'Pulse event registration reader returned an invalid table.');
|
|
210
|
+
if (entries.length > limits.maxEvents) {
|
|
211
|
+
throw contractError(EVENT_RUNTIME_CODES.LIMIT_EXCEEDED, 'Pulse event registration table exceeds its configured limit.', {
|
|
212
|
+
count: entries.length,
|
|
213
|
+
maxEvents: limits.maxEvents
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return entries;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeRegistration(input, limits) {
|
|
220
|
+
if (!input || typeof input !== 'object'
|
|
221
|
+
|| ownDataValue(input, 'version') !== 'pulse.event-registration.v1') {
|
|
222
|
+
throw contractError(EVENT_RUNTIME_CODES.REGISTRATION_INVALID, 'Pulse event registration is invalid.');
|
|
223
|
+
}
|
|
224
|
+
const declaration = ownDataValue(input, 'declaration');
|
|
225
|
+
const handler = ownDataValue(input, 'handler');
|
|
226
|
+
if (!declaration || typeof declaration !== 'object'
|
|
227
|
+
|| ownDataValue(declaration, 'version') !== 'pulse.event-declaration.v1'
|
|
228
|
+
|| typeof handler !== 'function') {
|
|
229
|
+
throw contractError(EVENT_RUNTIME_CODES.REGISTRATION_INVALID, 'Pulse event registration is invalid.');
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
type: normalizeEventType(ownDataValue(input, 'type'), limits),
|
|
234
|
+
schemaId: normalizeEventSchemaId(ownDataValue(declaration, 'schemaId'), limits),
|
|
235
|
+
handler
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw contractError(EVENT_RUNTIME_CODES.REGISTRATION_INVALID, 'Pulse event registration is invalid.', {
|
|
239
|
+
causeCode: errorField(error, 'code') || null
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function selectEventRegistration(application, type, limits) {
|
|
245
|
+
let selected;
|
|
246
|
+
const types = new Set();
|
|
247
|
+
for (const input of eventRegistrations(application, limits)) {
|
|
248
|
+
const registration = normalizeRegistration(input, limits);
|
|
249
|
+
if (types.has(registration.type)) {
|
|
250
|
+
throw contractError(EVENT_RUNTIME_CODES.REGISTRATION_INVALID, 'Pulse event registration table contains duplicate event types.', {
|
|
251
|
+
type: registration.type
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
types.add(registration.type);
|
|
255
|
+
if (registration.type === type) selected = registration;
|
|
256
|
+
}
|
|
257
|
+
if (selected) return selected;
|
|
258
|
+
throw contractError(EVENT_RUNTIME_CODES.HANDLER_NOT_FOUND, `Pulse has no event handler registered for ${JSON.stringify(type)}.`, { type });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function errorField(error, field) {
|
|
262
|
+
const value = ownDataValue(error, field);
|
|
263
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function executionError(category, error, redaction) {
|
|
267
|
+
const safe = redaction.redactError(error);
|
|
268
|
+
const code = errorField(safe, 'code');
|
|
269
|
+
const name = errorField(safe, 'name') || 'Error';
|
|
270
|
+
const messages = {
|
|
271
|
+
'invalid-frame': errorField(safe, 'message') || 'Pulse event frame is invalid.',
|
|
272
|
+
'handler-not-found': errorField(safe, 'message') || 'Pulse event handler was not found.',
|
|
273
|
+
'schema-mismatch': 'Pulse event frame schemaId does not match the registered event schema.',
|
|
274
|
+
'schema-validation-failed': 'Pulse event payload failed its declared schema.',
|
|
275
|
+
'handler-failed': 'Pulse contained an unexpected event handler or runtime failure.',
|
|
276
|
+
'completion-invalid': 'Pulse event handlers must resolve with undefined.',
|
|
277
|
+
cancelled: 'Pulse event execution was cancelled.',
|
|
278
|
+
'disposal-failed': 'Pulse event execution disposal failed.',
|
|
279
|
+
internal: 'Pulse event execution failed.'
|
|
280
|
+
};
|
|
281
|
+
return Object.freeze({
|
|
282
|
+
category,
|
|
283
|
+
name,
|
|
284
|
+
...(code ? { code } : {}),
|
|
285
|
+
message: redaction.redactString(messages[category] || messages.internal)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function createEventCompletion(redaction = createRedactionState()) {
|
|
290
|
+
let settled = false;
|
|
291
|
+
function claim() {
|
|
292
|
+
if (settled) throw contractError(EVENT_RUNTIME_CODES.COMPLETION_DUPLICATE, 'Pulse event execution may complete exactly once.');
|
|
293
|
+
settled = true;
|
|
294
|
+
}
|
|
295
|
+
return Object.freeze({
|
|
296
|
+
complete() {
|
|
297
|
+
claim();
|
|
298
|
+
return Object.freeze({ version: EVENT_EXECUTION_RESULT_VERSION, status: 'completed' });
|
|
299
|
+
},
|
|
300
|
+
fail(category, error) {
|
|
301
|
+
claim();
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
version: EVENT_EXECUTION_RESULT_VERSION,
|
|
304
|
+
status: 'failed',
|
|
305
|
+
error: executionError(category, error, redaction)
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function effectExecutionOptions(options, application, frame) {
|
|
312
|
+
return {
|
|
313
|
+
effectAdapter: options.effectAdapter,
|
|
314
|
+
capabilities: options.capabilities,
|
|
315
|
+
application,
|
|
316
|
+
event: frame,
|
|
317
|
+
executionKind: 'event',
|
|
318
|
+
signal: options.signal,
|
|
319
|
+
maxEffects: options.maxEffects,
|
|
320
|
+
maxBindingNameBytes: options.maxBindingNameBytes,
|
|
321
|
+
maxBindingValueBytes: options.maxBindingValueBytes,
|
|
322
|
+
maxKvNamespaceBytes: options.maxKvNamespaceBytes,
|
|
323
|
+
maxKvKeyBytes: options.maxKvKeyBytes,
|
|
324
|
+
maxKvValueBytes: options.maxKvValueBytes,
|
|
325
|
+
maxKvValueDepth: options.maxKvValueDepth,
|
|
326
|
+
maxKvValueEntries: options.maxKvValueEntries,
|
|
327
|
+
schemaCodecs: options.schemaCodecs,
|
|
328
|
+
strict: options.strict,
|
|
329
|
+
target: options.target,
|
|
330
|
+
provider: options.provider,
|
|
331
|
+
redactionValues: options.redactionValues,
|
|
332
|
+
onEffectObservation: options.onEffectObservation
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function observeLoggingConfiguration(options) {
|
|
337
|
+
if (typeof options.onLogObservation !== 'function') return;
|
|
338
|
+
const level = reportingLevel(options.reporting);
|
|
339
|
+
const name = Object.freeze(['off', 'error', 'warn', 'info', 'debug'])[level];
|
|
340
|
+
try {
|
|
341
|
+
options.onLogObservation(Object.freeze({
|
|
342
|
+
version: 'pulse.log-event.v1',
|
|
343
|
+
type: 'logging-config',
|
|
344
|
+
reporting: Object.freeze({ name, level }),
|
|
345
|
+
target: options.target || 'javascript',
|
|
346
|
+
provider: options.provider || null
|
|
347
|
+
}));
|
|
348
|
+
} catch (_) {
|
|
349
|
+
// Logging evidence is best effort.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function executeEvent(application, inputFrame, options = {}) {
|
|
354
|
+
const initialRedaction = createRedactionState(options.redactionValues);
|
|
355
|
+
const initialCompletion = createEventCompletion(initialRedaction);
|
|
356
|
+
let frame;
|
|
357
|
+
let validatedFrame;
|
|
358
|
+
let registration;
|
|
359
|
+
let payload;
|
|
360
|
+
let limits;
|
|
361
|
+
let preflightStage = 'frame';
|
|
362
|
+
try {
|
|
363
|
+
limits = normalizeEventLimits(options.eventLimits);
|
|
364
|
+
frame = normalizeEventFrame(inputFrame, limits);
|
|
365
|
+
preflightStage = 'selection';
|
|
366
|
+
registration = selectEventRegistration(application, frame.type, limits);
|
|
367
|
+
if (registration.schemaId !== frame.schemaId) {
|
|
368
|
+
throw contractError(EVENT_RUNTIME_CODES.SCHEMA_MISMATCH, 'Pulse event frame schemaId does not match its registered event declaration.', {
|
|
369
|
+
type: frame.type,
|
|
370
|
+
expectedSchemaId: registration.schemaId,
|
|
371
|
+
actualSchemaId: frame.schemaId
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
preflightStage = 'schema';
|
|
375
|
+
payload = frame.schemaId === null
|
|
376
|
+
? null
|
|
377
|
+
: cloneEventPayload(validateSchemaValue(frame.schemaId, frame.payload, {
|
|
378
|
+
schemaCodecs: options.schemaCodecs,
|
|
379
|
+
strict: true,
|
|
380
|
+
target: options.target,
|
|
381
|
+
provider: options.provider
|
|
382
|
+
}, {
|
|
383
|
+
source: 'event-ingress',
|
|
384
|
+
operationId: `event:${frame.type}`
|
|
385
|
+
}), limits);
|
|
386
|
+
validatedFrame = Object.freeze({
|
|
387
|
+
version: frame.version,
|
|
388
|
+
type: frame.type,
|
|
389
|
+
schemaId: frame.schemaId,
|
|
390
|
+
...(frame.schemaId === null ? {} : { payload })
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const code = errorField(error, 'code');
|
|
394
|
+
const category = code === EVENT_RUNTIME_CODES.HANDLER_NOT_FOUND
|
|
395
|
+
? 'handler-not-found'
|
|
396
|
+
: code === EVENT_RUNTIME_CODES.SCHEMA_MISMATCH
|
|
397
|
+
? 'schema-mismatch'
|
|
398
|
+
: preflightStage === 'schema' || (code && (code.startsWith('PULSE_SCHEMA_') || code === 'PULSE_BODY_TOO_LARGE'))
|
|
399
|
+
? 'schema-validation-failed'
|
|
400
|
+
: code === EVENT_RUNTIME_CODES.LIMIT_EXCEEDED || code === EVENT_RUNTIME_CODES.REGISTRATION_INVALID
|
|
401
|
+
? 'internal'
|
|
402
|
+
: 'invalid-frame';
|
|
403
|
+
return initialCompletion.fail(category, error);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (options.signal && options.signal.aborted) {
|
|
407
|
+
return initialCompletion.fail('cancelled', contractError(EVENT_RUNTIME_CODES.CANCELLED, 'Pulse event execution was cancelled before handler entry.'));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let effectExecution;
|
|
411
|
+
try {
|
|
412
|
+
effectExecution = createJavascriptEffectExecution(effectExecutionOptions(options, application, validatedFrame));
|
|
413
|
+
} catch (error) {
|
|
414
|
+
return initialCompletion.fail('internal', error);
|
|
415
|
+
}
|
|
416
|
+
const completion = createEventCompletion({
|
|
417
|
+
redactError(error) { return effectExecution.redactError(error); },
|
|
418
|
+
redactString(value) { return String(effectExecution.redactValue(String(value))); }
|
|
419
|
+
});
|
|
420
|
+
const executionOptions = Object.freeze({
|
|
421
|
+
...options,
|
|
422
|
+
signal: options.signal,
|
|
423
|
+
redactJsonTraceValue(value) { return effectExecution.redactValue(value); }
|
|
424
|
+
});
|
|
425
|
+
const event = Object.freeze({ type: validatedFrame.type, payload });
|
|
426
|
+
let context;
|
|
427
|
+
try {
|
|
428
|
+
context = createEventContext({
|
|
429
|
+
event,
|
|
430
|
+
state: new Map(),
|
|
431
|
+
effectExecution,
|
|
432
|
+
signal: options.signal,
|
|
433
|
+
executionOptions
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
try { await effectExecution.close(); } catch (_) { /* Original context failure remains primary. */ }
|
|
437
|
+
return completion.fail('internal', error);
|
|
438
|
+
}
|
|
439
|
+
observeLoggingConfiguration(options);
|
|
440
|
+
|
|
441
|
+
let handlerError;
|
|
442
|
+
let handlerFailureCategory = 'handler-failed';
|
|
443
|
+
let output;
|
|
444
|
+
try {
|
|
445
|
+
const returned = registration.handler(context);
|
|
446
|
+
if (!returned || typeof returned.then !== 'function') {
|
|
447
|
+
handlerFailureCategory = 'completion-invalid';
|
|
448
|
+
throw contractError(EVENT_RUNTIME_CODES.HANDLER_ASYNC_REQUIRED, 'Pulse event handlers must return a Promise resolving to undefined.');
|
|
449
|
+
}
|
|
450
|
+
output = await returned;
|
|
451
|
+
} catch (error) {
|
|
452
|
+
handlerError = error;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await effectExecution.assertIdle();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (handlerError === undefined) handlerError = error;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let disposalError;
|
|
461
|
+
try {
|
|
462
|
+
await effectExecution.close();
|
|
463
|
+
} catch (error) {
|
|
464
|
+
disposalError = error;
|
|
465
|
+
}
|
|
466
|
+
if (typeof options.onEffectSummary === 'function') {
|
|
467
|
+
try { options.onEffectSummary(effectExecution.summary()); }
|
|
468
|
+
catch (_) { /* Observation callbacks do not own event completion. */ }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (options.signal && options.signal.aborted) {
|
|
472
|
+
return completion.fail('cancelled', contractError(EVENT_RUNTIME_CODES.CANCELLED, 'Pulse event execution was cancelled.'));
|
|
473
|
+
}
|
|
474
|
+
if (handlerError !== undefined) {
|
|
475
|
+
if (handlerFailureCategory === 'completion-invalid') {
|
|
476
|
+
return completion.fail('completion-invalid', handlerError);
|
|
477
|
+
}
|
|
478
|
+
const safe = effectExecution.redactError(handlerError);
|
|
479
|
+
return completion.fail('handler-failed', safe instanceof PulseUnhandledError ? safe : new PulseUnhandledError(safe));
|
|
480
|
+
}
|
|
481
|
+
if (output !== undefined) {
|
|
482
|
+
return completion.fail('completion-invalid', contractError(EVENT_RUNTIME_CODES.HANDLER_RESULT_INVALID, 'Pulse event handlers must resolve with undefined.'));
|
|
483
|
+
}
|
|
484
|
+
if (disposalError !== undefined) return completion.fail('disposal-failed', disposalError);
|
|
485
|
+
return completion.complete();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = Object.freeze({
|
|
489
|
+
EVENT_FRAME_VERSION,
|
|
490
|
+
EVENT_EXECUTION_RESULT_VERSION,
|
|
491
|
+
EVENT_RUNTIME_CODES,
|
|
492
|
+
normalizeEventFrame,
|
|
493
|
+
createEventCompletion,
|
|
494
|
+
executeEvent
|
|
495
|
+
});
|