@pulse-compute/runtime 0.0.0 → 1.0.0-beta.2
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 +126 -2
- package/docs/API.md +572 -0
- package/docs/guides/routing.md +202 -0
- package/docs/preview-scope.md +142 -0
- package/package.json +38 -5
- package/src/host.d.ts +420 -0
- package/src/host.js +115 -0
- package/src/index.d.ts +215 -0
- package/src/index.js +11 -0
- package/src/internal/bindings.js +281 -0
- package/src/internal/body.js +254 -0
- package/src/internal/conditional-kv.js +199 -0
- package/src/internal/context.js +275 -0
- package/src/internal/effect-adapter.js +877 -0
- package/src/internal/errors.js +27 -0
- package/src/internal/event-emission.js +182 -0
- package/src/internal/event-execution.js +496 -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 +604 -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 +487 -0
- package/src/internal/schema.js +340 -0
- package/src/package.d.ts +127 -0
- package/src/package.js +19 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PulseRuntimeContractError } = require('./errors.js');
|
|
4
|
+
const { encodeSchemaValue, requireExplicitSchemaId } = require('./schema.js');
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_FETCH_METHODS = Object.freeze(new Set(['GET', 'HEAD', 'POST']));
|
|
7
|
+
const SUPPORTED_FETCH_INIT_FIELDS = Object.freeze(new Set(['method', 'headers', 'body', 'json', 'schema', 'timeoutMs']));
|
|
8
|
+
|
|
9
|
+
function fetchContractError(code, message, detail, cause) {
|
|
10
|
+
return new PulseRuntimeContractError(code, message, {
|
|
11
|
+
cause,
|
|
12
|
+
detail: detail === undefined ? undefined : Object.freeze({ ...detail })
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isPlainRecord(value) {
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeFetchUrl(value) {
|
|
23
|
+
if (typeof value !== 'string') {
|
|
24
|
+
throw fetchContractError('PULSE_FETCH_URL_INVALID', 'Pulse fetch URLs must be absolute HTTP or HTTPS strings.', {
|
|
25
|
+
type: typeof value
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = new URL(value);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw fetchContractError('PULSE_FETCH_URL_INVALID', `Invalid Pulse fetch URL ${String(value)}.`, { url: String(value) }, error);
|
|
33
|
+
}
|
|
34
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
35
|
+
throw fetchContractError(
|
|
36
|
+
'PULSE_FETCH_URL_INVALID',
|
|
37
|
+
`Pulse fetch only supports absolute HTTP and HTTPS URLs; received ${parsed.protocol || '<none>'}.`,
|
|
38
|
+
{ url: parsed.toString(), protocol: parsed.protocol }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return parsed.toString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeTimeout(value) {
|
|
45
|
+
if (value === undefined) return undefined;
|
|
46
|
+
const timeoutMs = Number(value);
|
|
47
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
48
|
+
throw fetchContractError(
|
|
49
|
+
'PULSE_FETCH_TIMEOUT_INVALID',
|
|
50
|
+
'Pulse fetch timeoutMs must be a positive safe integer.',
|
|
51
|
+
{ timeoutMs: value }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return timeoutMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateHeader(name, value) {
|
|
58
|
+
if (typeof name !== 'string' || typeof value !== 'string') {
|
|
59
|
+
throw fetchContractError(
|
|
60
|
+
'PULSE_FETCH_HEADERS_INVALID',
|
|
61
|
+
'Pulse fetch header names and values must be strings.',
|
|
62
|
+
{ nameType: typeof name, valueType: typeof value }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const validation = new Headers();
|
|
67
|
+
validation.append(name, value);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw fetchContractError(
|
|
70
|
+
'PULSE_FETCH_HEADERS_INVALID',
|
|
71
|
+
`Pulse fetch header ${JSON.stringify(name)} is invalid.`,
|
|
72
|
+
{ name },
|
|
73
|
+
error
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze([name, value]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeFetchHeaders(input) {
|
|
80
|
+
if (input === undefined) return Object.freeze([]);
|
|
81
|
+
const pairs = [];
|
|
82
|
+
if (Array.isArray(input)) {
|
|
83
|
+
for (const pair of input) {
|
|
84
|
+
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
85
|
+
throw fetchContractError(
|
|
86
|
+
'PULSE_FETCH_HEADERS_INVALID',
|
|
87
|
+
'Pulse fetch headers must contain [name, value] pairs.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
pairs.push(validateHeader(pair[0], pair[1]));
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze(pairs);
|
|
93
|
+
}
|
|
94
|
+
if (!isPlainRecord(input)) {
|
|
95
|
+
throw fetchContractError(
|
|
96
|
+
'PULSE_FETCH_HEADERS_INVALID',
|
|
97
|
+
'Pulse fetch headers must be a string record or [name, value] pairs.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
for (const [name, value] of Object.entries(input)) pairs.push(validateHeader(name, value));
|
|
101
|
+
return Object.freeze(pairs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasHeader(headers, name) {
|
|
105
|
+
const normalized = String(name).toLowerCase();
|
|
106
|
+
return headers.some(([header]) => header.toLowerCase() === normalized);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeFetchInit(input, options = {}) {
|
|
110
|
+
if (input !== undefined && !isPlainRecord(input)) {
|
|
111
|
+
throw fetchContractError('PULSE_FETCH_INIT_INVALID', 'Pulse fetch init must be an ordinary object.');
|
|
112
|
+
}
|
|
113
|
+
const value = input || {};
|
|
114
|
+
for (const field of Object.keys(value)) {
|
|
115
|
+
if (!SUPPORTED_FETCH_INIT_FIELDS.has(field)) {
|
|
116
|
+
throw fetchContractError(
|
|
117
|
+
'PULSE_FETCH_INIT_FIELD_UNSUPPORTED',
|
|
118
|
+
`Pulse fetch init field ${JSON.stringify(field)} is outside the portable contract.`,
|
|
119
|
+
{ field }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const method = value.method === undefined ? 'GET' : String(value.method).toUpperCase();
|
|
125
|
+
if (!SUPPORTED_FETCH_METHODS.has(method)) {
|
|
126
|
+
throw fetchContractError(
|
|
127
|
+
'PULSE_FETCH_METHOD_UNSUPPORTED',
|
|
128
|
+
`Pulse fetch method ${method} is outside the portable GET, HEAD, and POST contract.`,
|
|
129
|
+
{ method }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const headers = [...normalizeFetchHeaders(value.headers)];
|
|
134
|
+
const hasBody = Object.prototype.hasOwnProperty.call(value, 'body');
|
|
135
|
+
const hasJson = Object.prototype.hasOwnProperty.call(value, 'json');
|
|
136
|
+
const hasSchema = Object.prototype.hasOwnProperty.call(value, 'schema');
|
|
137
|
+
if (hasBody && hasJson) {
|
|
138
|
+
throw fetchContractError(
|
|
139
|
+
'PULSE_FETCH_BODY_AMBIGUOUS',
|
|
140
|
+
'Pulse fetch init accepts either body or json, not both.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (hasSchema && !hasJson) {
|
|
144
|
+
throw fetchContractError(
|
|
145
|
+
'PULSE_FETCH_SCHEMA_WITHOUT_JSON',
|
|
146
|
+
'Pulse fetch schema is only valid together with the semantic json field.'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const schemaId = hasSchema
|
|
150
|
+
? requireExplicitSchemaId(value.schema, options, 'fetch-request')
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
let body;
|
|
154
|
+
let bodyMode = 'none';
|
|
155
|
+
if (hasBody) {
|
|
156
|
+
if (typeof value.body !== 'string') {
|
|
157
|
+
throw fetchContractError('PULSE_FETCH_BODY_INVALID', 'Pulse fetch body must be a string.', { type: typeof value.body });
|
|
158
|
+
}
|
|
159
|
+
body = value.body;
|
|
160
|
+
bodyMode = 'text';
|
|
161
|
+
} else if (hasJson) {
|
|
162
|
+
if (!hasHeader(headers, 'content-type')) {
|
|
163
|
+
headers.push(Object.freeze(['content-type', 'application/json; charset=utf-8']));
|
|
164
|
+
}
|
|
165
|
+
const encoded = encodeSchemaValue(schemaId, value.json, options, {
|
|
166
|
+
source: 'fetch-request',
|
|
167
|
+
headers,
|
|
168
|
+
contentType: headers.find(([name]) => name.toLowerCase() === 'content-type')?.[1],
|
|
169
|
+
operationId: options.operationId || 'fetch-request:encode',
|
|
170
|
+
effectId: options.effectId
|
|
171
|
+
});
|
|
172
|
+
body = encoded.text;
|
|
173
|
+
bodyMode = 'json';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ((method === 'GET' || method === 'HEAD') && bodyMode !== 'none') {
|
|
177
|
+
throw fetchContractError(
|
|
178
|
+
'PULSE_FETCH_BODY_METHOD_UNSUPPORTED',
|
|
179
|
+
`Pulse ${method} fetch operations cannot include a body.`,
|
|
180
|
+
{ method, bodyMode }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Object.freeze({
|
|
185
|
+
method,
|
|
186
|
+
headers: Object.freeze(headers),
|
|
187
|
+
body,
|
|
188
|
+
bodyMode,
|
|
189
|
+
...(hasSchema ? { schema: schemaId } : {}),
|
|
190
|
+
timeoutMs: normalizeTimeout(value.timeoutMs)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeFetchRequest(url, init, options = {}) {
|
|
195
|
+
return Object.freeze({ url: normalizeFetchUrl(url), init: normalizeFetchInit(init, options) });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fetchRequestInitForHost(init, signal) {
|
|
199
|
+
const normalized = init && typeof init === 'object' && typeof init.bodyMode === 'string'
|
|
200
|
+
? init
|
|
201
|
+
: normalizeFetchInit(init);
|
|
202
|
+
const headers = new Headers();
|
|
203
|
+
for (const [name, value] of normalized.headers) headers.append(name, value);
|
|
204
|
+
return Object.freeze({
|
|
205
|
+
method: normalized.method,
|
|
206
|
+
headers,
|
|
207
|
+
body: normalized.bodyMode === 'none' ? undefined : normalized.body,
|
|
208
|
+
signal,
|
|
209
|
+
redirect: 'follow'
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = Object.freeze({
|
|
214
|
+
SUPPORTED_FETCH_INIT_FIELDS,
|
|
215
|
+
SUPPORTED_FETCH_METHODS,
|
|
216
|
+
fetchRequestInitForHost,
|
|
217
|
+
normalizeFetchHeaders,
|
|
218
|
+
normalizeFetchInit,
|
|
219
|
+
normalizeFetchRequest,
|
|
220
|
+
normalizeFetchUrl
|
|
221
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { executeRouter, routerEntries } = require('./router.js');
|
|
4
|
+
const {
|
|
5
|
+
PulseRuntimeContractError,
|
|
6
|
+
PulseUnhandledError,
|
|
7
|
+
createOpaqueFetchResponse,
|
|
8
|
+
isPulseFetchResponse,
|
|
9
|
+
markOpaqueResponse,
|
|
10
|
+
responseBodyClass,
|
|
11
|
+
wrapStructuredResponse
|
|
12
|
+
} = require('./response.js');
|
|
13
|
+
|
|
14
|
+
module.exports = Object.freeze({
|
|
15
|
+
PulseRuntimeContractError,
|
|
16
|
+
PulseUnhandledError,
|
|
17
|
+
createOpaqueFetchResponse,
|
|
18
|
+
isPulseFetchResponse,
|
|
19
|
+
markOpaqueResponse,
|
|
20
|
+
responseBodyClass,
|
|
21
|
+
executeRouter,
|
|
22
|
+
routerEntries,
|
|
23
|
+
wrapStructuredResponse
|
|
24
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Portable mirror of @pulse-compute/wasm-contracts/logging. The authoring
|
|
4
|
+
// runtime intentionally has no dependency on compiler packages.
|
|
5
|
+
const REPORTING_LEVELS = Object.freeze({ off: 0, error: 1, warn: 2, info: 3, debug: 4 });
|
|
6
|
+
const LOG_METHODS = Object.freeze(['error', 'warn', 'info', 'debug']);
|
|
7
|
+
const DEFAULT_REPORTING_LEVEL = 'info';
|
|
8
|
+
|
|
9
|
+
function reportingLevel(value) {
|
|
10
|
+
if (Number.isInteger(value) && value >= 0 && value <= 4) return value;
|
|
11
|
+
const name = value === undefined || value === null
|
|
12
|
+
? DEFAULT_REPORTING_LEVEL
|
|
13
|
+
: String(value).trim().toLowerCase();
|
|
14
|
+
if (!Object.prototype.hasOwnProperty.call(REPORTING_LEVELS, name)) {
|
|
15
|
+
throw new TypeError('Pulse reporting must be off, error, warn, info, or debug.');
|
|
16
|
+
}
|
|
17
|
+
return REPORTING_LEVELS[name];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createPulseLogger(options = {}) {
|
|
21
|
+
const threshold = reportingLevel(options.reporting);
|
|
22
|
+
const redact = typeof options.redact === 'function' ? options.redact : String;
|
|
23
|
+
const emit = typeof options.emit === 'function' ? options.emit : undefined;
|
|
24
|
+
const observe = typeof options.observe === 'function' ? options.observe : undefined;
|
|
25
|
+
const target = options.target == null ? null : String(options.target);
|
|
26
|
+
const provider = options.provider == null ? null : String(options.provider);
|
|
27
|
+
const logger = {};
|
|
28
|
+
|
|
29
|
+
for (const name of LOG_METHODS) {
|
|
30
|
+
const level = REPORTING_LEVELS[name];
|
|
31
|
+
logger[name] = (message) => {
|
|
32
|
+
if (typeof message !== 'string') throw new TypeError(`ctx.log.${name} requires one string message.`);
|
|
33
|
+
if (level > threshold) return;
|
|
34
|
+
let safeMessage;
|
|
35
|
+
try { safeMessage = String(redact(message)); }
|
|
36
|
+
catch (_) { safeMessage = '<redacted>'; }
|
|
37
|
+
const event = Object.freeze({
|
|
38
|
+
version: 'pulse.log-event.v1',
|
|
39
|
+
type: 'log',
|
|
40
|
+
level,
|
|
41
|
+
name,
|
|
42
|
+
message: safeMessage,
|
|
43
|
+
target,
|
|
44
|
+
provider
|
|
45
|
+
});
|
|
46
|
+
if (observe) {
|
|
47
|
+
try { observe(event); }
|
|
48
|
+
catch (_) { /* Logging evidence is best effort. */ }
|
|
49
|
+
}
|
|
50
|
+
if (emit) {
|
|
51
|
+
try { emit(level, safeMessage, event); }
|
|
52
|
+
catch (_) { /* Provider logging failures never fail the request. */ }
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return Object.freeze(logger);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = Object.freeze({
|
|
60
|
+
REPORTING_LEVELS,
|
|
61
|
+
LOG_METHODS,
|
|
62
|
+
DEFAULT_REPORTING_LEVEL,
|
|
63
|
+
reportingLevel,
|
|
64
|
+
createPulseLogger
|
|
65
|
+
});
|