@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.
@@ -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 });