@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,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createStructuredBodyReader, normalizeBodyLimit } = require('./body.js');
|
|
4
|
+
const { normalizeFetchRequest } = require('./fetch.js');
|
|
5
|
+
const { decodeSchemaText, strictSchemaPolicy } = require('./schema.js');
|
|
6
|
+
const {
|
|
7
|
+
cloneKvValue,
|
|
8
|
+
normalizeBindingName,
|
|
9
|
+
normalizeKvKey,
|
|
10
|
+
normalizeKvNamespace
|
|
11
|
+
} = require('./bindings.js');
|
|
12
|
+
const { bindPackageRuntimeContext } = require('./package-runtime.js');
|
|
13
|
+
const { normalizeEventEmission } = require('./event-emission.js');
|
|
14
|
+
const { createPulseLogger } = require('./logging.js');
|
|
15
|
+
const {
|
|
16
|
+
createJsonResult,
|
|
17
|
+
createResponseResult,
|
|
18
|
+
createTextResult,
|
|
19
|
+
normalizeFetchResponse,
|
|
20
|
+
projectFetchResponse
|
|
21
|
+
} = require('./response.js');
|
|
22
|
+
|
|
23
|
+
function headersToPairs(headers) {
|
|
24
|
+
const pairs = [];
|
|
25
|
+
for (const [name, value] of headers.entries()) pairs.push(Object.freeze([name, value]));
|
|
26
|
+
return Object.freeze(pairs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRequestHeaderPairs(headers, fallback) {
|
|
30
|
+
if (!Array.isArray(headers)) return headersToPairs(fallback);
|
|
31
|
+
return Object.freeze(headers.map((entry) => {
|
|
32
|
+
if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError('Pulse request headers must be [name, value] pairs.');
|
|
33
|
+
return Object.freeze([String(entry[0]), String(entry[1])]);
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createRequestView(request, requestHeaders, effectExecution, options = {}) {
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
const headers = normalizeRequestHeaderPairs(requestHeaders, request.headers);
|
|
40
|
+
const body = createStructuredBodyReader(request, {
|
|
41
|
+
label: 'request',
|
|
42
|
+
maxBytes: normalizeBodyLimit(options.maxRequestBodyBytes ?? options.maxStructuredBodyBytes),
|
|
43
|
+
contentType: request.headers.get('content-type') || '',
|
|
44
|
+
signal: options.signal || request.signal
|
|
45
|
+
});
|
|
46
|
+
let textEffect;
|
|
47
|
+
let jsonEffect;
|
|
48
|
+
const schemaJsonEffects = new Map();
|
|
49
|
+
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
method: request.method.toUpperCase(),
|
|
52
|
+
url: request.url,
|
|
53
|
+
path: url.pathname || '/',
|
|
54
|
+
headers,
|
|
55
|
+
header(name) {
|
|
56
|
+
const lower = String(name).toLowerCase();
|
|
57
|
+
const match = headers.find(([header]) => header.toLowerCase() === lower);
|
|
58
|
+
return match ? match[1] : undefined;
|
|
59
|
+
},
|
|
60
|
+
text() {
|
|
61
|
+
if (!textEffect) textEffect = effectExecution.local('request.body.text', () => body.text());
|
|
62
|
+
return textEffect;
|
|
63
|
+
},
|
|
64
|
+
json(schemaId) {
|
|
65
|
+
if (schemaId === undefined && !strictSchemaPolicy(options)) {
|
|
66
|
+
if (!jsonEffect) jsonEffect = effectExecution.local('request.body.json', () => body.json());
|
|
67
|
+
return jsonEffect;
|
|
68
|
+
}
|
|
69
|
+
const key = schemaId === undefined ? '<missing>' : String(schemaId);
|
|
70
|
+
if (!schemaJsonEffects.has(key)) {
|
|
71
|
+
schemaJsonEffects.set(key, effectExecution.local(`request.body.json:${key}`, async () => decodeSchemaText(
|
|
72
|
+
schemaId,
|
|
73
|
+
await body.text(),
|
|
74
|
+
options,
|
|
75
|
+
{
|
|
76
|
+
source: 'request',
|
|
77
|
+
headers,
|
|
78
|
+
operationId: `request:${key}`
|
|
79
|
+
}
|
|
80
|
+
)));
|
|
81
|
+
}
|
|
82
|
+
return schemaJsonEffects.get(key);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createStateView(map) {
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
get(key) {
|
|
90
|
+
requireString('state key', key);
|
|
91
|
+
return map.get(key);
|
|
92
|
+
},
|
|
93
|
+
set(key, value) {
|
|
94
|
+
requireString('state key', key);
|
|
95
|
+
requireString('state value', value);
|
|
96
|
+
map.set(key, value);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function requireString(label, value) {
|
|
102
|
+
if (typeof value !== 'string') throw new TypeError(`Pulse ${label} must be a string.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createFetchOperation(effectExecution, fetchEffect, fetchRequest, options, effectId) {
|
|
106
|
+
const projectionOptions = Object.freeze({
|
|
107
|
+
...options,
|
|
108
|
+
maxBodyBytes: normalizeBodyLimit(options.maxFetchBodyBytes ?? options.maxStructuredBodyBytes),
|
|
109
|
+
signal: options.signal,
|
|
110
|
+
requestMethod: fetchRequest.init.method,
|
|
111
|
+
effectId
|
|
112
|
+
});
|
|
113
|
+
const operation = effectExecution.project(
|
|
114
|
+
fetchEffect,
|
|
115
|
+
'response',
|
|
116
|
+
(value) => normalizeFetchResponse(value, projectionOptions)
|
|
117
|
+
);
|
|
118
|
+
Object.defineProperties(operation, {
|
|
119
|
+
text: {
|
|
120
|
+
enumerable: false,
|
|
121
|
+
value: () => effectExecution.project(
|
|
122
|
+
operation,
|
|
123
|
+
'text',
|
|
124
|
+
(response) => projectFetchResponse(response, 'text', undefined, projectionOptions)
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
json: {
|
|
128
|
+
enumerable: false,
|
|
129
|
+
value: (schemaId) => effectExecution.project(
|
|
130
|
+
operation,
|
|
131
|
+
'json',
|
|
132
|
+
(response) => projectFetchResponse(response, 'json', schemaId, projectionOptions)
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return operation;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createContext(frame) {
|
|
140
|
+
const effects = frame.effectExecution;
|
|
141
|
+
if (!effects) throw new TypeError('Pulse context requires an execution-owned JavaScript effect execution.');
|
|
142
|
+
const state = createStateView(frame.state);
|
|
143
|
+
const req = frame.req;
|
|
144
|
+
const options = Object.freeze({
|
|
145
|
+
...(frame.executionOptions || {}),
|
|
146
|
+
signal: frame.signal
|
|
147
|
+
});
|
|
148
|
+
let fetchSequence = 0;
|
|
149
|
+
const log = createPulseLogger({
|
|
150
|
+
reporting: options.reporting,
|
|
151
|
+
redact(message) { return effects.redactValue(message); },
|
|
152
|
+
emit: options.log,
|
|
153
|
+
observe: options.onLogObservation,
|
|
154
|
+
target: options.target || 'javascript',
|
|
155
|
+
provider: options.provider
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const eventContext = frame.event !== undefined;
|
|
159
|
+
const ctx = {
|
|
160
|
+
...(eventContext ? {} : { req }),
|
|
161
|
+
state,
|
|
162
|
+
log,
|
|
163
|
+
fetch(url, init) {
|
|
164
|
+
fetchSequence += 1;
|
|
165
|
+
const effectId = `fetch-${fetchSequence}`;
|
|
166
|
+
const fetchRequest = normalizeFetchRequest(url, init, {
|
|
167
|
+
...options,
|
|
168
|
+
operationId: `fetch-request:${effectId}`,
|
|
169
|
+
effectId
|
|
170
|
+
});
|
|
171
|
+
const fetchEffect = effects.dispatch({
|
|
172
|
+
id: effectId,
|
|
173
|
+
kind: 'fetch',
|
|
174
|
+
providerKind: 'fetch',
|
|
175
|
+
operation: 'dispatch',
|
|
176
|
+
capability: 'fetch',
|
|
177
|
+
url: fetchRequest.url,
|
|
178
|
+
init: fetchRequest.init
|
|
179
|
+
});
|
|
180
|
+
return createFetchOperation(effects, fetchEffect, fetchRequest, options, effectId);
|
|
181
|
+
},
|
|
182
|
+
parallel(record) { return effects.parallel(record); },
|
|
183
|
+
config: Object.freeze({
|
|
184
|
+
get(name) {
|
|
185
|
+
const normalizedName = normalizeBindingName('config', name, options);
|
|
186
|
+
return effects.dispatch({
|
|
187
|
+
kind: 'config.get',
|
|
188
|
+
providerKind: 'config',
|
|
189
|
+
operation: 'get',
|
|
190
|
+
capability: 'config.get',
|
|
191
|
+
name: normalizedName
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}),
|
|
195
|
+
secret: Object.freeze({
|
|
196
|
+
get(name) {
|
|
197
|
+
const normalizedName = normalizeBindingName('secret', name, options);
|
|
198
|
+
return effects.dispatch({
|
|
199
|
+
kind: 'secret.get',
|
|
200
|
+
providerKind: 'secret',
|
|
201
|
+
operation: 'get',
|
|
202
|
+
capability: 'secret.get',
|
|
203
|
+
name: normalizedName
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
kv(name) {
|
|
208
|
+
const namespace = normalizeKvNamespace(name, options);
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
...Object.fromEntries(['getVersioned', 'insertIfAbsent', 'compareAndSwap'].map((operation) => [operation, (key, generationOrValue, value) => effects.dispatch({
|
|
211
|
+
kind: `kv.${operation}`, providerKind: 'kv', operation, capability: `kv.${operation}`, namespace, key,
|
|
212
|
+
...(operation === 'compareAndSwap' ? { generation: generationOrValue, value } : operation === 'insertIfAbsent' ? { value: generationOrValue } : {})
|
|
213
|
+
})])),
|
|
214
|
+
get(key) {
|
|
215
|
+
const normalizedKey = normalizeKvKey(key, options);
|
|
216
|
+
return effects.dispatch({
|
|
217
|
+
kind: 'kv.get',
|
|
218
|
+
providerKind: 'kv',
|
|
219
|
+
operation: 'get',
|
|
220
|
+
capability: 'kv.get',
|
|
221
|
+
namespace,
|
|
222
|
+
key: normalizedKey
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
put(key, value) {
|
|
226
|
+
const normalizedKey = normalizeKvKey(key, options);
|
|
227
|
+
const normalizedValue = cloneKvValue(value, options);
|
|
228
|
+
return effects.dispatch({
|
|
229
|
+
kind: 'kv.put',
|
|
230
|
+
providerKind: 'kv',
|
|
231
|
+
operation: 'put',
|
|
232
|
+
capability: 'kv.put',
|
|
233
|
+
namespace,
|
|
234
|
+
key: normalizedKey,
|
|
235
|
+
value: normalizedValue
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
emit(type, event) {
|
|
241
|
+
const outboundFrame = normalizeEventEmission(type, event, options);
|
|
242
|
+
return effects.dispatch({
|
|
243
|
+
kind: 'event.emit',
|
|
244
|
+
providerKind: 'event',
|
|
245
|
+
operation: 'emit',
|
|
246
|
+
capability: 'event.emit',
|
|
247
|
+
frame: outboundFrame
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (eventContext) {
|
|
253
|
+
ctx.event = frame.event;
|
|
254
|
+
} else {
|
|
255
|
+
ctx.json = (value, descriptor) => createJsonResult(value, descriptor, options);
|
|
256
|
+
ctx.text = (value, responseOptions) => createTextResult(value, responseOptions);
|
|
257
|
+
ctx.response = (input) => createResponseResult(input);
|
|
258
|
+
}
|
|
259
|
+
if (!eventContext && frame.routeContext) {
|
|
260
|
+
ctx.param = (name) => {
|
|
261
|
+
requireString('route parameter name', name);
|
|
262
|
+
return frame.params[name];
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const frozen = Object.freeze(ctx);
|
|
266
|
+
if (!eventContext) bindPackageRuntimeContext(frozen, frame);
|
|
267
|
+
return frozen;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createEventContext(frame) {
|
|
271
|
+
if (!frame || !frame.event) throw new TypeError('Pulse event context requires one normalized event view.');
|
|
272
|
+
return createContext({ ...frame, routeContext: false });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = Object.freeze({ createContext, createEventContext, createRequestView });
|