@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.
@@ -0,0 +1,340 @@
1
+ 'use strict';
2
+
3
+ const { PulseRuntimeContractError } = require('./errors.js');
4
+
5
+ function schemaRuntimeError(code, message, detail, cause) {
6
+ const error = new PulseRuntimeContractError(code, message, {
7
+ detail: detail === undefined ? undefined : Object.freeze({ ...detail }),
8
+ cause
9
+ });
10
+ if (code === 'PULSE_BODY_TOO_LARGE') error.name = 'BodyTooLargeError';
11
+ else if (code === 'PULSE_SCHEMA_CONTENT_TYPE' || code === 'PULSE_SCHEMA_JSON_MALFORMED' || code === 'PULSE_SCHEMA_DECODE') error.name = 'SchemaDecodeError';
12
+ else if (code === 'PULSE_SCHEMA_ENCODE') error.name = 'SchemaEncodeError';
13
+ else if (
14
+ code === 'PULSE_SCHEMA_REQUIRED'
15
+ || code === 'PULSE_SCHEMA_REFERENCE'
16
+ || code === 'PULSE_SCHEMA_CODECS_UNAVAILABLE'
17
+ || code === 'PULSE_SCHEMA_ID_INVALID'
18
+ || code === 'PULSE_RESPONSE_CASE_REFERENCE'
19
+ || code === 'PULSE_RESPONSE_DESCRIPTOR_INVALID'
20
+ ) error.name = 'SchemaReferenceError';
21
+ return error;
22
+ }
23
+
24
+ function schemaCodecs(options = {}) {
25
+ const codecs = options.schemaCodecs;
26
+ return codecs && typeof codecs === 'object' ? codecs : undefined;
27
+ }
28
+
29
+ function schemaRegistry(options = {}) {
30
+ return schemaCodecs(options)?.registry;
31
+ }
32
+
33
+ function strictSchemaPolicy(options = {}) {
34
+ const codecs = schemaCodecs(options);
35
+ return options.strict === true
36
+ && Boolean(codecs && Array.isArray(codecs.ids) && codecs.ids.length > 0);
37
+ }
38
+
39
+ function requireSchemaCodecs(options, schemaId, source) {
40
+ const codecs = schemaCodecs(options);
41
+ if (!codecs || typeof codecs.has !== 'function' || typeof codecs.decodeJsonText !== 'function' || typeof codecs.encodeJsonText !== 'function') {
42
+ throw schemaRuntimeError(
43
+ 'PULSE_SCHEMA_CODECS_UNAVAILABLE',
44
+ `Pulse cannot use schema ${JSON.stringify(String(schemaId))} because the project codec registry is unavailable.`,
45
+ { schemaId: String(schemaId), source }
46
+ );
47
+ }
48
+ if (!codecs.has(String(schemaId))) {
49
+ throw schemaRuntimeError(
50
+ 'PULSE_SCHEMA_REFERENCE',
51
+ `Unknown compiled schema ${String(schemaId)}.`,
52
+ { schemaId: String(schemaId), source }
53
+ );
54
+ }
55
+ return codecs;
56
+ }
57
+
58
+ function requireSchemaValueCodecs(options, schemaId, source) {
59
+ const codecs = schemaCodecs(options);
60
+ if (!codecs || typeof codecs.has !== 'function' || typeof codecs.decode !== 'function') {
61
+ throw schemaRuntimeError(
62
+ 'PULSE_SCHEMA_CODECS_UNAVAILABLE',
63
+ `Pulse cannot use schema ${JSON.stringify(String(schemaId))} because in-memory project codecs are unavailable.`,
64
+ { schemaId: String(schemaId), source }
65
+ );
66
+ }
67
+ if (!codecs.has(String(schemaId))) {
68
+ throw schemaRuntimeError(
69
+ 'PULSE_SCHEMA_REFERENCE',
70
+ `Unknown compiled schema ${String(schemaId)}.`,
71
+ { schemaId: String(schemaId), source }
72
+ );
73
+ }
74
+ return codecs;
75
+ }
76
+
77
+ function requireSchemaId(schemaId, options, source) {
78
+ if (schemaId === undefined) {
79
+ if (strictSchemaPolicy(options)) {
80
+ throw schemaRuntimeError(
81
+ 'PULSE_SCHEMA_REQUIRED',
82
+ `Strict Pulse ${source} JSON requires a registered schema ID.`,
83
+ { source, strict: true }
84
+ );
85
+ }
86
+ return undefined;
87
+ }
88
+ return requireExplicitSchemaId(schemaId, options, source);
89
+ }
90
+
91
+ function requireExplicitSchemaId(schemaId, options, source) {
92
+ if (typeof schemaId !== 'string' || schemaId.length === 0) {
93
+ throw schemaRuntimeError(
94
+ 'PULSE_SCHEMA_ID_INVALID',
95
+ 'Pulse schema IDs must be non-empty strings.',
96
+ { source, schemaIdType: typeof schemaId }
97
+ );
98
+ }
99
+ requireSchemaCodecs(options, schemaId, source);
100
+ return schemaId;
101
+ }
102
+
103
+ function validateSchemaValue(schemaId, value, options = {}, context = {}) {
104
+ const source = String(context.source || 'json-value');
105
+ if (typeof schemaId !== 'string' || schemaId.length === 0) {
106
+ throw schemaRuntimeError(
107
+ 'PULSE_SCHEMA_ID_INVALID',
108
+ 'Pulse schema IDs must be non-empty strings.',
109
+ { source, schemaIdType: typeof schemaId }
110
+ );
111
+ }
112
+ const codecs = requireSchemaValueCodecs(options, schemaId, source);
113
+ return codecs.decode(schemaId, value, source);
114
+ }
115
+
116
+ function headerValue(headers, name) {
117
+ const lower = String(name).toLowerCase();
118
+ if (headers && typeof headers.get === 'function') return headers.get(lower) || '';
119
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers || {});
120
+ const match = entries.find(([key]) => String(key).toLowerCase() === lower);
121
+ return match ? String(match[1]) : '';
122
+ }
123
+
124
+ function normalizeContentType(value) {
125
+ return String(value || '').trim().toLowerCase().replace(/[ \t]+/g, ' ');
126
+ }
127
+
128
+ function assertSchemaContentType(options, headers, schemaId, source) {
129
+ const policy = String(schemaRegistry(options)?.contentTypePolicy || 'accept-json-or-missing');
130
+ const contentType = normalizeContentType(headerValue(headers, 'content-type'));
131
+ const mediaType = contentType.split(';')[0].trim();
132
+ const json = mediaType === 'application/json' || mediaType.endsWith('+json');
133
+ if (json || (!contentType && policy === 'accept-json-or-missing')) return contentType;
134
+ throw schemaRuntimeError(
135
+ 'PULSE_SCHEMA_CONTENT_TYPE',
136
+ `Schema ${String(schemaId)} requires a JSON content type.`,
137
+ { schemaId: String(schemaId), source, contentType, contentTypePolicy: policy }
138
+ );
139
+ }
140
+
141
+ function utf8Bytes(value) {
142
+ return new TextEncoder().encode(String(value)).byteLength;
143
+ }
144
+
145
+ function assertSchemaBodySize(options, text, schemaId, source) {
146
+ const configured = Number(schemaRegistry(options)?.maxBytes);
147
+ const maxBytes = Number.isSafeInteger(configured) && configured > 0 ? configured : 65_536;
148
+ const bytes = utf8Bytes(text);
149
+ if (bytes <= maxBytes) return bytes;
150
+ throw schemaRuntimeError(
151
+ 'PULSE_BODY_TOO_LARGE',
152
+ `Pulse ${source} schema body exceeds the ${maxBytes} byte limit.`,
153
+ { schemaId: String(schemaId), source, bytes, maxBytes }
154
+ );
155
+ }
156
+
157
+ function traceErrorFields(error) {
158
+ const detail = error && (error.detail || error.details) || {};
159
+ return {
160
+ errorCode: error && error.code || 'PULSE_SCHEMA_OPERATION_FAILED',
161
+ errorPath: detail.path || detail.field || null,
162
+ expected: detail.expected || null,
163
+ actualKind: detail.actualKind || detail.actual || null
164
+ };
165
+ }
166
+
167
+ function emitSchemaTrace(options, event, value) {
168
+ const codecs = schemaCodecs(options);
169
+ if (!codecs || typeof codecs.createTraceEvent !== 'function' || typeof options.onJsonTrace !== 'function') return;
170
+ const redact = typeof options.redactJsonTraceValue === 'function'
171
+ ? options.redactJsonTraceValue
172
+ : undefined;
173
+ const headers = redact && event.headers !== undefined
174
+ ? redact(event.headers)
175
+ : event.headers;
176
+ const normalized = codecs.createTraceEvent({
177
+ ...event,
178
+ ...(headers === undefined ? {} : { headers }),
179
+ ...(value === undefined ? {} : { semanticValue: value }),
180
+ target: event.target || options.target || 'javascript',
181
+ provider: event.provider || options.provider || null
182
+ }, {
183
+ redact
184
+ });
185
+ options.onJsonTrace(normalized);
186
+ }
187
+
188
+ function decodeSchemaText(schemaId, text, options = {}, context = {}) {
189
+ const source = String(context.source || 'json');
190
+ const id = requireSchemaId(schemaId, options, source);
191
+ if (id === undefined) {
192
+ try {
193
+ return JSON.parse(String(text));
194
+ } catch (cause) {
195
+ throw schemaRuntimeError('PULSE_BODY_DECODE', `Pulse ${source} body could not be decoded as JSON.`, { source }, cause);
196
+ }
197
+ }
198
+ let contentType = normalizeContentType(headerValue(context.headers, 'content-type'));
199
+ const event = {
200
+ boundary: source === 'request' ? 'request' : 'fetch-response',
201
+ schemaId: id,
202
+ responseCaseId: null,
203
+ operationId: String(context.operationId || `${source}:${id}`),
204
+ status: context.status ?? null,
205
+ headers: context.headers || [],
206
+ bodyOwnership: source === 'request' ? 'request-snapshot' : 'fetched-response-snapshot',
207
+ effectId: context.effectId || null,
208
+ groupId: context.groupId || null
209
+ };
210
+ try {
211
+ contentType = assertSchemaContentType(options, context.headers, id, source);
212
+ assertSchemaBodySize(options, text, id, source);
213
+ const codecs = requireSchemaCodecs(options, id, source);
214
+ const value = codecs.decodeJsonText(id, String(text), source);
215
+ emitSchemaTrace(options, {
216
+ ...event,
217
+ kind: source === 'request' ? 'json.decode.request' : 'json.decode.fetch',
218
+ contentType: contentType || null
219
+ }, value);
220
+ return value;
221
+ } catch (error) {
222
+ emitSchemaTrace(options, {
223
+ ...event,
224
+ kind: 'json.decode.error',
225
+ contentType: contentType || null,
226
+ ...traceErrorFields(error)
227
+ });
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ function encodeSchemaValue(schemaId, value, options = {}, context = {}) {
233
+ const source = String(context.source || 'application-response');
234
+ const id = requireSchemaId(schemaId, options, source);
235
+ if (id === undefined) {
236
+ let text;
237
+ try {
238
+ text = JSON.stringify(value);
239
+ } catch (cause) {
240
+ throw schemaRuntimeError('PULSE_RUNTIME_JSON_SERIALIZE_FAILED', `Pulse ${source} JSON could not be serialized.`, { source }, cause);
241
+ }
242
+ if (text === undefined) {
243
+ throw schemaRuntimeError('PULSE_RUNTIME_JSON_SERIALIZE_FAILED', `Pulse ${source} JSON must serialize to a JSON value.`, { source });
244
+ }
245
+ return Object.freeze({ schemaId: null, text, value });
246
+ }
247
+ const codecs = requireSchemaCodecs(options, id, source);
248
+ try {
249
+ const text = codecs.encodeJsonText(id, value, source);
250
+ const normalized = codecs.decodeJsonText(id, text, `${source}-trace`);
251
+ emitSchemaTrace(options, {
252
+ kind: source === 'fetch-request' ? 'json.encode.fetch' : 'json.encode.response',
253
+ boundary: source,
254
+ schemaId: id,
255
+ responseCaseId: context.responseCaseId || null,
256
+ operationId: String(context.operationId || `${source}:${context.responseCaseId || id}`),
257
+ status: context.status ?? null,
258
+ contentType: normalizeContentType(context.contentType || 'application/json; charset=utf-8'),
259
+ headers: context.headers || [],
260
+ bodyOwnership: 'application-owned',
261
+ effectId: context.effectId || null,
262
+ groupId: context.groupId || null
263
+ }, normalized);
264
+ return Object.freeze({ schemaId: id, text, value: normalized });
265
+ } catch (error) {
266
+ emitSchemaTrace(options, {
267
+ kind: 'json.encode.error',
268
+ boundary: source,
269
+ schemaId: id,
270
+ responseCaseId: context.responseCaseId || null,
271
+ operationId: String(context.operationId || `${source}:${context.responseCaseId || id}`),
272
+ status: context.status ?? null,
273
+ contentType: normalizeContentType(context.contentType || 'application/json; charset=utf-8'),
274
+ headers: context.headers || [],
275
+ bodyOwnership: 'application-owned',
276
+ effectId: context.effectId || null,
277
+ groupId: context.groupId || null,
278
+ ...traceErrorFields(error)
279
+ });
280
+ throw error;
281
+ }
282
+ }
283
+
284
+ function resolveResponseDescriptor(descriptor, options = {}) {
285
+ if (typeof descriptor === 'string') {
286
+ const codecs = schemaCodecs(options);
287
+ const responseCase = codecs && typeof codecs.responseCase === 'function'
288
+ ? codecs.responseCase(descriptor)
289
+ : undefined;
290
+ if (!responseCase) {
291
+ throw schemaRuntimeError(
292
+ 'PULSE_RESPONSE_CASE_REFERENCE',
293
+ `Unknown compiled response case ${descriptor}.`,
294
+ { responseCaseId: descriptor, source: 'application-response' }
295
+ );
296
+ }
297
+ requireSchemaId(String(responseCase.schemaId), options, 'application-response');
298
+ return Object.freeze({
299
+ responseCaseId: descriptor,
300
+ schemaId: String(responseCase.schemaId),
301
+ status: Number(responseCase.status),
302
+ headers: undefined
303
+ });
304
+ }
305
+ const value = descriptor === undefined ? {} : descriptor;
306
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
307
+ throw schemaRuntimeError(
308
+ 'PULSE_RESPONSE_DESCRIPTOR_INVALID',
309
+ 'Pulse JSON response metadata must be an object or registered response-case ID.',
310
+ { source: 'application-response', valueType: Array.isArray(value) ? 'array' : typeof value }
311
+ );
312
+ }
313
+ const hasSchema = Object.prototype.hasOwnProperty.call(value, 'schema');
314
+ const schemaId = hasSchema
315
+ ? requireExplicitSchemaId(value.schema, options, 'application-response')
316
+ : requireSchemaId(undefined, options, 'application-response');
317
+ return Object.freeze({
318
+ responseCaseId: null,
319
+ schemaId,
320
+ status: value.status,
321
+ headers: value.headers
322
+ });
323
+ }
324
+
325
+ module.exports = Object.freeze({
326
+ assertSchemaBodySize,
327
+ assertSchemaContentType,
328
+ decodeSchemaText,
329
+ emitSchemaTrace,
330
+ encodeSchemaValue,
331
+ requireSchemaCodecs,
332
+ requireSchemaValueCodecs,
333
+ requireExplicitSchemaId,
334
+ requireSchemaId,
335
+ resolveResponseDescriptor,
336
+ schemaCodecs,
337
+ schemaRegistry,
338
+ strictSchemaPolicy,
339
+ validateSchemaValue
340
+ });
@@ -0,0 +1,127 @@
1
+ import type { PulseContext, PulseParallelEffect } from './index';
2
+
3
+ export declare const PACKAGE_RUNTIME_BRIDGE_VERSION: 'pulse.package-runtime-bridge.v1';
4
+ export declare const PACKAGE_SCHEMA_CODEC_BRIDGE_VERSION: 'pulse.first-party-embedded-schema-codec-bridge.v1';
5
+
6
+ export interface PulsePackageRuntimeContext {
7
+ /**
8
+ * Detached request metadata view. URL, method, headers, and lifecycle signal
9
+ * are preserved, but the application-owned request body is unavailable.
10
+ * Body projection methods reject with
11
+ * `PULSE_RUNTIME_PACKAGE_REQUEST_BODY_UNAVAILABLE`.
12
+ */
13
+ readonly request: Request;
14
+ readonly params: Readonly<Record<string, string>>;
15
+ readonly path: {
16
+ readonly absolute: string;
17
+ readonly relative: string;
18
+ };
19
+ readonly signal?: AbortSignal;
20
+ }
21
+
22
+ export interface PulsePackageEffectOperation<Result = unknown> {
23
+ readonly kind: string;
24
+ readonly capability: string;
25
+ readonly result: string;
26
+ /** Type-only result carrier. It is not inspected at runtime. */
27
+ readonly __result?: Result;
28
+ }
29
+
30
+ export interface PulsePackageRuntimeDefinition<
31
+ Operations extends Readonly<Record<string, PulsePackageEffectOperation<unknown>>>
32
+ > {
33
+ readonly package: string;
34
+ readonly contractId: string;
35
+ readonly providerKind: string;
36
+ readonly operations: Operations;
37
+ }
38
+
39
+ export type PulsePackageOperationResult<Operation> =
40
+ Operation extends PulsePackageEffectOperation<infer Result> ? Result : unknown;
41
+
42
+ export interface PulsePackageRuntime<
43
+ Operations extends Readonly<Record<string, PulsePackageEffectOperation<unknown>>>
44
+ > {
45
+ readonly version: typeof PACKAGE_RUNTIME_BRIDGE_VERSION;
46
+ readonly package: string;
47
+ readonly contractId: string;
48
+ readonly providerKind: string;
49
+ context(ctx: PulseContext): PulsePackageRuntimeContext;
50
+ effect<Key extends keyof Operations & string>(
51
+ ctx: PulseContext,
52
+ operation: Key,
53
+ payload?: Readonly<Record<string, unknown>>,
54
+ ): PulseParallelEffect<PulsePackageOperationResult<Operations[Key]>>;
55
+ effect<Key extends keyof Operations & string, Result>(
56
+ ctx: PulseContext,
57
+ operation: Key,
58
+ payload: Readonly<Record<string, unknown>> | undefined,
59
+ projector: (value: unknown) => Result | PromiseLike<Result>,
60
+ ): PulseParallelEffect<Result>;
61
+ }
62
+
63
+ export interface PulsePackageSchemaDeclaration {
64
+ readonly input: string | null;
65
+ readonly output: string | null;
66
+ }
67
+
68
+ export type PulsePackageSchemaReadonly<Value> =
69
+ Value extends (...args: never[]) => unknown ? Value
70
+ : Value extends readonly (infer Entry)[] ? readonly PulsePackageSchemaReadonly<Entry>[]
71
+ : Value extends object ? { readonly [Key in keyof Value]: PulsePackageSchemaReadonly<Value[Key]> }
72
+ : Value;
73
+
74
+ export interface PulsePackageSchemaCodecBridge {
75
+ decodeEmbeddedJson<Value = unknown>(schemaId: string, packageOwnedText: string): PulsePackageSchemaReadonly<Value>;
76
+ encodeEmbeddedJson(schemaId: string, packageOwnedValue: unknown): string;
77
+ }
78
+
79
+ export interface PulsePackageSchemaCodecRuntime {
80
+ readonly version: typeof PACKAGE_SCHEMA_CODEC_BRIDGE_VERSION;
81
+ readonly package: string;
82
+ readonly contractId: string;
83
+ bind(ctx: PulseContext, declaration: PulsePackageSchemaDeclaration): PulsePackageSchemaCodecBridge;
84
+ }
85
+
86
+ export interface PulsePackageSchemaCodecRuntimeDefinition {
87
+ readonly package: string;
88
+ readonly contractId: string;
89
+ }
90
+
91
+ export declare const TRUSTED_PACKAGE_EFFECT_CATALOG: Readonly<Record<string, {
92
+ readonly contractId: string;
93
+ readonly providerKind: string;
94
+ readonly operations: Readonly<Record<string, {
95
+ readonly kind: string;
96
+ readonly capability: string;
97
+ readonly result: string;
98
+ }>>;
99
+ }>>;
100
+
101
+ export declare function clonePackageEffectPayload<T extends Readonly<Record<string, unknown>>>(
102
+ payload: T,
103
+ limits?: {
104
+ readonly maxBytes?: number;
105
+ readonly maxDepth?: number;
106
+ readonly maxEntries?: number;
107
+ },
108
+ ): T;
109
+
110
+ export declare function createPackageRuntime<
111
+ const Operations extends Readonly<Record<string, PulsePackageEffectOperation<unknown>>>
112
+ >(definition: PulsePackageRuntimeDefinition<Operations>): PulsePackageRuntime<Operations>;
113
+
114
+ export declare function createPackageSchemaCodecRuntime(
115
+ definition: PulsePackageSchemaCodecRuntimeDefinition,
116
+ ): PulsePackageSchemaCodecRuntime;
117
+
118
+ declare const packageRuntime: Readonly<{
119
+ PACKAGE_RUNTIME_BRIDGE_VERSION: typeof PACKAGE_RUNTIME_BRIDGE_VERSION;
120
+ PACKAGE_SCHEMA_CODEC_BRIDGE_VERSION: typeof PACKAGE_SCHEMA_CODEC_BRIDGE_VERSION;
121
+ TRUSTED_PACKAGE_EFFECT_CATALOG: typeof TRUSTED_PACKAGE_EFFECT_CATALOG;
122
+ clonePackageEffectPayload: typeof clonePackageEffectPayload;
123
+ createPackageRuntime: typeof createPackageRuntime;
124
+ createPackageSchemaCodecRuntime: typeof createPackageSchemaCodecRuntime;
125
+ }>;
126
+
127
+ export default packageRuntime;
package/src/package.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ PACKAGE_RUNTIME_BRIDGE_VERSION,
5
+ PACKAGE_SCHEMA_CODEC_BRIDGE_VERSION,
6
+ TRUSTED_PACKAGE_EFFECT_CATALOG,
7
+ clonePackageEffectPayload,
8
+ createPackageRuntime,
9
+ createPackageSchemaCodecRuntime
10
+ } = require('./internal/package-runtime.js');
11
+
12
+ module.exports = Object.freeze({
13
+ PACKAGE_RUNTIME_BRIDGE_VERSION,
14
+ PACKAGE_SCHEMA_CODEC_BRIDGE_VERSION,
15
+ TRUSTED_PACKAGE_EFFECT_CATALOG,
16
+ clonePackageEffectPayload,
17
+ createPackageRuntime,
18
+ createPackageSchemaCodecRuntime
19
+ });