@pulse-compute/provider-fastly 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 +52 -1
- package/dist/config-schema.json +73 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -5
- package/src/build/canonical-target.js +436 -0
- package/src/build/entities-native.js +564 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/native-http-effects.js +871 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2109 -0
- package/src/compiler/assets-package-out-parity-proof.js +338 -0
- package/src/compiler/assets-provider-proof.js +499 -0
- package/src/compiler/config-provider-resolution.js +573 -0
- package/src/compiler/fastly-adapter.js +564 -0
- package/src/compiler/fastly-command-entry.js +390 -0
- package/src/compiler/fastly-config-secret-serve.js +323 -0
- package/src/compiler/fastly-hostcall-binding.js +598 -0
- package/src/compiler/fastly-readiness.js +477 -0
- package/src/compiler/index.js +40 -0
- package/src/compiler/lifecycle-parity-contract.js +70 -0
- package/src/compiler/lifecycle-parity-proof.js +260 -0
- package/src/compiler/route-handler-effect-proof.js +255 -0
- package/src/compiler/runtime-provider-kv.js +237 -0
- package/src/compiler/runtime-provider-shape.js +109 -0
- package/src/config-api.js +63 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +73 -0
- package/src/index.js +3 -0
- package/src/index.ts +118 -0
- package/src/javascript/assets-adapter.js +122 -0
- package/src/javascript/bindings-adapter.js +237 -0
- package/src/javascript/errors.js +26 -0
- package/src/javascript/fetch-adapter.js +93 -0
- package/src/javascript/grip-broadcast.js +261 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +26 -0
- package/src/javascript/package-effects.js +68 -0
- package/src/javascript/runtime-host.js +108 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +776 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +260 -0
- package/src/javascript/target.js +73 -0
- package/src/javascript/test-runtime.js +407 -0
- package/src/provider-contract.js +72 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +597 -0
- package/src/runtime/index.js +28 -0
- package/src/runtime/provider-primitives-contract.js +272 -0
- package/src/runtime/provider-primitives.js +82 -0
- package/src/runtime/route-handler-effects.js +507 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-http-effects-host.js +390 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +599 -0
- package/src/toolchain/config.js +128 -0
- package/src/toolchain/index.js +343 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
const { createFastlyJavascriptHandler } = require('./lifecycle.js');
|
|
5
|
+
const { createHash } = require('./sha256.js');
|
|
6
|
+
|
|
7
|
+
const FASTLY_JAVASCRIPT_TEST_RUNTIME_VERSION = 'pulse.fastly-javascript-test-runtime.v1';
|
|
8
|
+
const FASTLY_JAVASCRIPT_LOCAL_EXECUTION_MODE = 'provider-emulation';
|
|
9
|
+
|
|
10
|
+
function recordEntries(input, label) {
|
|
11
|
+
if (input === undefined || input === null) return [];
|
|
12
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
13
|
+
throw new TypeError(`Pulse Fastly JavaScript ${label} fixtures must be an object record.`);
|
|
14
|
+
}
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
17
|
+
if (typeof key !== 'string') throw new TypeError(`Pulse Fastly JavaScript ${label} fixtures may not use symbol keys.`);
|
|
18
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
19
|
+
if (!descriptor || !descriptor.enumerable) continue;
|
|
20
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
21
|
+
throw new TypeError(`Pulse Fastly JavaScript ${label} fixtures may only use data properties.`);
|
|
22
|
+
}
|
|
23
|
+
entries.push([key, descriptor.value]);
|
|
24
|
+
}
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cloneJson(value) {
|
|
29
|
+
if (value === undefined) return undefined;
|
|
30
|
+
return JSON.parse(JSON.stringify(value));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function canonicalizeJson(value) {
|
|
34
|
+
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
|
35
|
+
if (!value || typeof value !== 'object') return value;
|
|
36
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalizeJson(value[key])]));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function semanticBodySha256(body) {
|
|
40
|
+
if (body === null) return null;
|
|
41
|
+
try {
|
|
42
|
+
return createHash('sha256').update(JSON.stringify(canonicalizeJson(JSON.parse(body)))).digest('hex');
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fixtureHeaders(input, defaultContentType) {
|
|
49
|
+
const headers = new Headers();
|
|
50
|
+
if (Array.isArray(input)) {
|
|
51
|
+
for (const pair of input) {
|
|
52
|
+
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
53
|
+
throw new TypeError('Fastly JavaScript fetch fixture headers must be [name, value] pairs.');
|
|
54
|
+
}
|
|
55
|
+
headers.append(String(pair[0]), String(pair[1]));
|
|
56
|
+
}
|
|
57
|
+
} else if (input instanceof Headers) {
|
|
58
|
+
for (const [name, value] of input.entries()) headers.append(name, value);
|
|
59
|
+
} else {
|
|
60
|
+
for (const [name, value] of recordEntries(input, 'fetch header')) headers.append(name, String(value));
|
|
61
|
+
}
|
|
62
|
+
if (defaultContentType && !headers.has('content-type')) headers.set('content-type', defaultContentType);
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function delayWithSignal(ms, signal) {
|
|
67
|
+
const delay = Math.max(0, Number(ms) || 0);
|
|
68
|
+
if (delay === 0) return Promise.resolve();
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
let timer;
|
|
71
|
+
const finish = (callback, value) => {
|
|
72
|
+
if (timer) clearTimeout(timer);
|
|
73
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
74
|
+
callback(value);
|
|
75
|
+
};
|
|
76
|
+
const onAbort = () => finish(reject, signal.reason || new DOMException('Aborted', 'AbortError'));
|
|
77
|
+
if (signal && signal.aborted) return onAbort();
|
|
78
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
79
|
+
timer = setTimeout(() => finish(resolve), delay);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fixtureKey(fetches, url, method) {
|
|
84
|
+
const exact = `${String(method || 'GET').toUpperCase()} ${String(url)}`;
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(fetches, exact)) return exact;
|
|
86
|
+
if (Object.prototype.hasOwnProperty.call(fetches, String(url))) return String(url);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fixtureResponse(fixture, init) {
|
|
91
|
+
const status = Number(fixture.status || 200);
|
|
92
|
+
let body = null;
|
|
93
|
+
let defaultContentType;
|
|
94
|
+
if (Array.isArray(fixture.chunks)) {
|
|
95
|
+
body = new ReadableStream({
|
|
96
|
+
start(controller) {
|
|
97
|
+
for (const chunk of fixture.chunks) {
|
|
98
|
+
controller.enqueue(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
|
|
99
|
+
}
|
|
100
|
+
controller.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
defaultContentType = 'application/octet-stream';
|
|
104
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'value')) {
|
|
105
|
+
body = JSON.stringify(fixture.value);
|
|
106
|
+
defaultContentType = 'application/json; charset=utf-8';
|
|
107
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'json')) {
|
|
108
|
+
body = JSON.stringify(fixture.json);
|
|
109
|
+
defaultContentType = 'application/json; charset=utf-8';
|
|
110
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'text')) {
|
|
111
|
+
body = String(fixture.text);
|
|
112
|
+
defaultContentType = 'text/plain; charset=utf-8';
|
|
113
|
+
} else if (Object.prototype.hasOwnProperty.call(fixture, 'body')) {
|
|
114
|
+
body = fixture.body == null ? null : fixture.body;
|
|
115
|
+
}
|
|
116
|
+
const method = String(init.method || 'GET').toUpperCase();
|
|
117
|
+
const bodyAllowed = method !== 'HEAD'
|
|
118
|
+
&& status !== 204
|
|
119
|
+
&& status !== 205
|
|
120
|
+
&& status !== 304
|
|
121
|
+
&& !(status >= 100 && status < 200);
|
|
122
|
+
return new Response(bodyAllowed ? body : null, {
|
|
123
|
+
status,
|
|
124
|
+
headers: fixtureHeaders(fixture.headers, defaultContentType)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function initialKvRecord(value) {
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
json: cloneJson(value),
|
|
131
|
+
body: typeof value === 'string' ? value : JSON.stringify(value)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createFastlyJavascriptLocalEnvironment(options = {}) {
|
|
136
|
+
const bindings = options.bindings && typeof options.bindings === 'object' ? options.bindings : {};
|
|
137
|
+
const kvBindings = bindings.kv && typeof bindings.kv === 'object' ? bindings.kv : {};
|
|
138
|
+
const configValues = new Map(recordEntries(options.config, 'config').map(([key, value]) => [key, String(value)]));
|
|
139
|
+
const secretValues = new Map(recordEntries(options.secrets, 'secret').map(([key, value]) => [key, String(value)]));
|
|
140
|
+
const physicalKv = new Map();
|
|
141
|
+
const observations = {
|
|
142
|
+
constructors: [],
|
|
143
|
+
fetches: [],
|
|
144
|
+
logs: [],
|
|
145
|
+
errors: []
|
|
146
|
+
};
|
|
147
|
+
let activeRequests = 0;
|
|
148
|
+
|
|
149
|
+
for (const [logicalName, values] of recordEntries(options.kv, 'KV namespace')) {
|
|
150
|
+
const resource = typeof kvBindings[logicalName] === 'string' ? kvBindings[logicalName].trim() : '';
|
|
151
|
+
if (!resource) continue;
|
|
152
|
+
if (!physicalKv.has(resource)) physicalKv.set(resource, new Map());
|
|
153
|
+
const entries = physicalKv.get(resource);
|
|
154
|
+
for (const [key, value] of recordEntries(values, `KV namespace ${logicalName}`)) {
|
|
155
|
+
entries.set(String(key), initialKvRecord(value));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function assertRequestScope(api, resource) {
|
|
160
|
+
if (activeRequests <= 0) {
|
|
161
|
+
throw new TypeError(`Fastly JavaScript local ${api}(${JSON.stringify(resource)}) was constructed outside request handling.`);
|
|
162
|
+
}
|
|
163
|
+
observations.constructors.push(Object.freeze({ api, resource: String(resource) }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class ConfigStore {
|
|
167
|
+
constructor(resource) {
|
|
168
|
+
assertRequestScope('ConfigStore', resource);
|
|
169
|
+
if (String(resource) !== String(bindings.configStore || '')) {
|
|
170
|
+
throw new TypeError(`Unknown Fastly local Config Store ${String(resource)}.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
get(name) {
|
|
174
|
+
return configValues.has(String(name)) ? configValues.get(String(name)) : null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class SecretStore {
|
|
179
|
+
constructor(resource) {
|
|
180
|
+
assertRequestScope('SecretStore', resource);
|
|
181
|
+
if (String(resource) !== String(bindings.secretStore || '')) {
|
|
182
|
+
throw new TypeError(`Unknown Fastly local Secret Store ${String(resource)}.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async get(name) {
|
|
186
|
+
if (!secretValues.has(String(name))) return null;
|
|
187
|
+
const value = secretValues.get(String(name));
|
|
188
|
+
return Object.freeze({ plaintext: () => value });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class KVStore {
|
|
193
|
+
constructor(resource) {
|
|
194
|
+
assertRequestScope('KVStore', resource);
|
|
195
|
+
this.resource = String(resource);
|
|
196
|
+
if (!physicalKv.has(this.resource)) physicalKv.set(this.resource, new Map());
|
|
197
|
+
}
|
|
198
|
+
async get(key) {
|
|
199
|
+
const record = physicalKv.get(this.resource).get(String(key));
|
|
200
|
+
if (!record) return null;
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
json: async () => cloneJson(record.json),
|
|
203
|
+
text: async () => String(record.body),
|
|
204
|
+
get body() {
|
|
205
|
+
const bytes = new TextEncoder().encode(String(record.body));
|
|
206
|
+
return new ReadableStream({
|
|
207
|
+
start(controller) {
|
|
208
|
+
controller.enqueue(bytes);
|
|
209
|
+
controller.close();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async put(key, body) {
|
|
216
|
+
const text = String(body);
|
|
217
|
+
let value;
|
|
218
|
+
try { value = JSON.parse(text); }
|
|
219
|
+
catch (error) { throw new TypeError('Fastly local KVStore.put requires JSON text.', { cause: error }); }
|
|
220
|
+
physicalKv.get(this.resource).set(String(key), Object.freeze({ json: cloneJson(value), body: text }));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const fetches = Object.fromEntries(recordEntries(options.fetches, 'fetch'));
|
|
225
|
+
const fallback = options.networkFetch === true ? (options.fetchImplementation || globalThis.fetch) : undefined;
|
|
226
|
+
async function fetchImplementation(url, init = {}) {
|
|
227
|
+
const headers = new Headers(init.headers);
|
|
228
|
+
const body = init.body === undefined || init.body === null ? null : String(init.body);
|
|
229
|
+
observations.fetches.push(Object.freeze({
|
|
230
|
+
url: String(url),
|
|
231
|
+
method: String(init.method || 'GET').toUpperCase(),
|
|
232
|
+
backend: init.backend === undefined ? null : String(init.backend),
|
|
233
|
+
authenticated: headers.has('authorization'),
|
|
234
|
+
contentType: headers.get('content-type'),
|
|
235
|
+
headerNames: Object.freeze([...headers.keys()].map((name) => name.toLowerCase()).sort()),
|
|
236
|
+
hasBody: body !== null,
|
|
237
|
+
bodyBytes: body === null ? 0 : new TextEncoder().encode(body).byteLength,
|
|
238
|
+
bodySha256: body === null ? null : createHash('sha256').update(body).digest('hex'),
|
|
239
|
+
bodySemanticSha256: semanticBodySha256(body)
|
|
240
|
+
}));
|
|
241
|
+
const key = fixtureKey(fetches, url, init.method);
|
|
242
|
+
if (key === undefined) {
|
|
243
|
+
if (typeof fallback === 'function') return fallback(url, init);
|
|
244
|
+
throw new Error(`No Fastly JavaScript fetch fixture is configured for ${String(init.method || 'GET')} ${String(url)}.`);
|
|
245
|
+
}
|
|
246
|
+
const fixture = fetches[key] || {};
|
|
247
|
+
if (fixture.kind === 'network-error' || fixture.networkError === true) {
|
|
248
|
+
throw new Error(fixture.message || 'Configured Fastly JavaScript fetch network failure.');
|
|
249
|
+
}
|
|
250
|
+
if (fixture.kind === 'timeout' || fixture.timeout === true) {
|
|
251
|
+
await new Promise((_resolve, reject) => {
|
|
252
|
+
const signal = init.signal;
|
|
253
|
+
const onAbort = () => reject(signal.reason || new DOMException('Aborted', 'AbortError'));
|
|
254
|
+
if (signal && signal.aborted) onAbort();
|
|
255
|
+
else if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
await delayWithSignal(fixture.delayMs, init.signal);
|
|
259
|
+
return fixtureResponse(fixture, init);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const localConsole = Object.freeze({
|
|
263
|
+
error(value) { observations.logs.push(Object.freeze({ level: 'error', message: String(value) })); },
|
|
264
|
+
warn(value) { observations.logs.push(Object.freeze({ level: 'warn', message: String(value) })); },
|
|
265
|
+
info(value) { observations.logs.push(Object.freeze({ level: 'info', message: String(value) })); },
|
|
266
|
+
debug(value) { observations.logs.push(Object.freeze({ level: 'debug', message: String(value) })); },
|
|
267
|
+
log(value) { observations.logs.push(Object.freeze({ level: 'log', message: String(value) })); }
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
version: FASTLY_JAVASCRIPT_TEST_RUNTIME_VERSION,
|
|
272
|
+
mode: FASTLY_JAVASCRIPT_LOCAL_EXECUTION_MODE,
|
|
273
|
+
provider: 'fastly',
|
|
274
|
+
providerReality: false,
|
|
275
|
+
bindings,
|
|
276
|
+
apis: Object.freeze({ ConfigStore, SecretStore, KVStore }),
|
|
277
|
+
fetchImplementation,
|
|
278
|
+
console: localConsole,
|
|
279
|
+
beginRequest() { activeRequests += 1; },
|
|
280
|
+
endRequest() { activeRequests -= 1; },
|
|
281
|
+
recordError(error) {
|
|
282
|
+
observations.errors.push(Object.freeze({
|
|
283
|
+
name: error && error.name ? String(error.name) : 'Error',
|
|
284
|
+
code: error && error.code ? String(error.code) : null
|
|
285
|
+
}));
|
|
286
|
+
},
|
|
287
|
+
snapshot() {
|
|
288
|
+
return Object.freeze({
|
|
289
|
+
mode: FASTLY_JAVASCRIPT_LOCAL_EXECUTION_MODE,
|
|
290
|
+
providerReality: false,
|
|
291
|
+
realityRunner: 'fastly compute serve',
|
|
292
|
+
constructors: Object.freeze([...observations.constructors]),
|
|
293
|
+
fetches: Object.freeze([...observations.fetches]),
|
|
294
|
+
logs: Object.freeze([...observations.logs]),
|
|
295
|
+
errors: Object.freeze([...observations.errors])
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createFastlyJavascriptTestRequest(input = {}) {
|
|
302
|
+
const method = String(input.method || 'GET').toUpperCase();
|
|
303
|
+
const headers = new Headers();
|
|
304
|
+
for (const [name, value] of input.headers || []) headers.append(String(name), String(value));
|
|
305
|
+
const init = { method, headers };
|
|
306
|
+
if (method !== 'GET' && method !== 'HEAD' && input.body !== undefined) {
|
|
307
|
+
init.body = typeof input.body === 'string' || input.body instanceof Uint8Array
|
|
308
|
+
? input.body
|
|
309
|
+
: JSON.stringify(input.body);
|
|
310
|
+
}
|
|
311
|
+
return new Request(input.url || `https://pulse-fastly.local${String(input.path || '/')}`, init);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function responseKind(response) {
|
|
315
|
+
const contentType = String(response.headers.get('content-type') || '').toLowerCase();
|
|
316
|
+
if (contentType.includes('application/json') || contentType.includes('+json')) return 'json';
|
|
317
|
+
if (contentType.startsWith('text/')) return 'text';
|
|
318
|
+
return 'response';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function executeFastlyJavascriptLocalRequest(application, request, options = {}) {
|
|
322
|
+
const environment = options.environment || createFastlyJavascriptLocalEnvironment(options);
|
|
323
|
+
let effectSummary;
|
|
324
|
+
const handler = createFastlyJavascriptHandler(application, {
|
|
325
|
+
apis: environment.apis,
|
|
326
|
+
bindings: options.bindings || environment.bindings,
|
|
327
|
+
fetchImplementation: environment.fetchImplementation,
|
|
328
|
+
console: environment.console,
|
|
329
|
+
application: options.application,
|
|
330
|
+
maxEffects: options.maxEffects,
|
|
331
|
+
maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes,
|
|
332
|
+
maxFetchBodyBytes: options.maxFetchBodyBytes ?? options.maxBodyBytes,
|
|
333
|
+
maxStructuredBodyBytes: options.maxStructuredBodyBytes,
|
|
334
|
+
schemaCodecs: options.schemaCodecs,
|
|
335
|
+
strict: options.strict === true,
|
|
336
|
+
reporting: options.reporting,
|
|
337
|
+
onLogObservation: options.onLogObservation,
|
|
338
|
+
onJsonTrace: options.onJsonTrace,
|
|
339
|
+
onEffectObservation: options.onEffectObservation,
|
|
340
|
+
onEffectSummary(summary) {
|
|
341
|
+
effectSummary = summary;
|
|
342
|
+
if (typeof options.onEffectSummary === 'function') options.onEffectSummary(summary);
|
|
343
|
+
},
|
|
344
|
+
onError(error) {
|
|
345
|
+
environment.recordError(error);
|
|
346
|
+
if (typeof options.onError === 'function') options.onError(error);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
environment.beginRequest();
|
|
350
|
+
try {
|
|
351
|
+
const response = await handler(request);
|
|
352
|
+
return Object.freeze({ response, effectSummary, environment });
|
|
353
|
+
} finally {
|
|
354
|
+
environment.endRequest();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function executeFastlyJavascriptTestCase(application, testCase, options = {}) {
|
|
359
|
+
const environment = createFastlyJavascriptLocalEnvironment({
|
|
360
|
+
...options,
|
|
361
|
+
config: testCase.config || {},
|
|
362
|
+
secrets: testCase.secrets || {},
|
|
363
|
+
kv: testCase.kv || {},
|
|
364
|
+
fetches: testCase.fetches || {}
|
|
365
|
+
});
|
|
366
|
+
const request = createFastlyJavascriptTestRequest(testCase.request);
|
|
367
|
+
const execution = await executeFastlyJavascriptLocalRequest(application, request, {
|
|
368
|
+
...options,
|
|
369
|
+
environment
|
|
370
|
+
});
|
|
371
|
+
const method = String(testCase.request && testCase.request.method || 'GET').toUpperCase();
|
|
372
|
+
const body = method === 'HEAD' ? '' : Buffer.from(await execution.response.arrayBuffer()).toString('utf8');
|
|
373
|
+
const effectSummary = execution.effectSummary;
|
|
374
|
+
return Object.freeze({
|
|
375
|
+
version: FASTLY_JAVASCRIPT_TEST_RUNTIME_VERSION,
|
|
376
|
+
mode: FASTLY_JAVASCRIPT_LOCAL_EXECUTION_MODE,
|
|
377
|
+
providerReality: false,
|
|
378
|
+
response: Object.freeze({
|
|
379
|
+
status: execution.response.status,
|
|
380
|
+
kind: responseKind(execution.response),
|
|
381
|
+
bodyClass: runtimeHost.responseBodyClass(execution.response),
|
|
382
|
+
headers: runtimeHost.responseHeaderPairs(execution.response),
|
|
383
|
+
body
|
|
384
|
+
}),
|
|
385
|
+
effectCount: effectSummary ? effectSummary.ownedEffectCount : 0,
|
|
386
|
+
externalEffectCount: effectSummary ? effectSummary.effectCount : 0,
|
|
387
|
+
localEffectCount: effectSummary ? effectSummary.localEffectCount : 0,
|
|
388
|
+
continuations: Object.freeze((effectSummary && effectSummary.groups || []).map((group) => Object.freeze({
|
|
389
|
+
id: group.id,
|
|
390
|
+
kind: 'parallel',
|
|
391
|
+
effectIds: group.effectIds,
|
|
392
|
+
keys: group.keys
|
|
393
|
+
}))),
|
|
394
|
+
resolutionOrder: effectSummary ? effectSummary.resolutionOrder : Object.freeze([]),
|
|
395
|
+
provider: environment.snapshot()
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = Object.freeze({
|
|
400
|
+
FASTLY_JAVASCRIPT_TEST_RUNTIME_VERSION,
|
|
401
|
+
FASTLY_JAVASCRIPT_LOCAL_EXECUTION_MODE,
|
|
402
|
+
createFastlyJavascriptLocalEnvironment,
|
|
403
|
+
createFastlyJavascriptTestRequest,
|
|
404
|
+
executeFastlyJavascriptLocalRequest,
|
|
405
|
+
executeFastlyJavascriptTestCase,
|
|
406
|
+
responseKind
|
|
407
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const canonical = require('@pulse-compute/wasm-contracts/provider/canonical-provider');
|
|
4
|
+
const packageManifest = require('../package.json');
|
|
5
|
+
|
|
6
|
+
const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
|
|
7
|
+
id: 'fastly',
|
|
8
|
+
package: '@pulse-compute/provider-fastly',
|
|
9
|
+
providerVersion: packageManifest.version,
|
|
10
|
+
runtime: 'pulse.canonical-fastly-runtime.v2',
|
|
11
|
+
buildTarget: 'fastly-compute',
|
|
12
|
+
deployable: true,
|
|
13
|
+
localExecution: true,
|
|
14
|
+
capabilities: [
|
|
15
|
+
'request', 'response.json', 'response.text', 'response.custom', 'fetch',
|
|
16
|
+
'config.get', 'secret.get', 'kv.get', 'kv.put', 'assets.lookup',
|
|
17
|
+
'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify',
|
|
18
|
+
'opaque.pass-through'
|
|
19
|
+
],
|
|
20
|
+
lowering: {
|
|
21
|
+
request: 'fastly.compute.request',
|
|
22
|
+
'response.json': 'fastly.compute.response.json',
|
|
23
|
+
'response.text': 'fastly.compute.response.text',
|
|
24
|
+
'response.custom': 'fastly.compute.response.custom',
|
|
25
|
+
fetch: 'fastly.backend.fetch',
|
|
26
|
+
'config.get': 'fastly.config-store.get',
|
|
27
|
+
'secret.get': 'fastly.secret-store.get',
|
|
28
|
+
'kv.get': 'fastly.kv-store.get',
|
|
29
|
+
'kv.put': 'fastly.kv-store.put',
|
|
30
|
+
'assets.lookup': 'fastly.kv-store.assets.lookup',
|
|
31
|
+
'grip.channel': 'fastly.fanout.channel',
|
|
32
|
+
'grip.hold': 'fastly.fanout.hold',
|
|
33
|
+
'grip.publish': 'fastly.fanout.publish',
|
|
34
|
+
'grip.broadcast': 'fastly.grip.publish-control',
|
|
35
|
+
'jwt.verify': 'fastly.native.jwt.verify',
|
|
36
|
+
'opaque.pass-through': 'fastly.response-body.stream'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function validateStaticBackendBindings(metadata = {}, bindings = {}) {
|
|
41
|
+
if (bindings.dynamicBackends === true) return;
|
|
42
|
+
const configured = bindings.backends && typeof bindings.backends === 'object' ? bindings.backends : {};
|
|
43
|
+
for (const operation of metadata.providerOperations || []) {
|
|
44
|
+
if (!operation || operation.kind !== 'fetch') continue;
|
|
45
|
+
const origin = operation.resource && operation.resource.origin;
|
|
46
|
+
if (!origin || configured[origin]) continue;
|
|
47
|
+
const error = new Error(`No Fastly backend binding is configured for ${origin}.`);
|
|
48
|
+
error.name = 'CanonicalProviderContractError';
|
|
49
|
+
error.code = 'PULSE_FASTLY_BACKEND_REQUIRED';
|
|
50
|
+
error.detail = Object.freeze({ origin, effectId: operation.id, dynamicBackends: false });
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createFastlyLoweringPlan(metadata, bindings = {}) {
|
|
56
|
+
validateStaticBackendBindings(metadata, bindings);
|
|
57
|
+
return canonical.createProviderLoweringPlan(metadata, FASTLY_PROVIDER_DESCRIPTOR, {
|
|
58
|
+
...bindings,
|
|
59
|
+
grip: bindings.grip || 'fastly-fanout'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertFastlySupports(metadata) {
|
|
64
|
+
return canonical.assertProviderSupports(metadata, FASTLY_PROVIDER_DESCRIPTOR);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = Object.freeze({
|
|
68
|
+
FASTLY_PROVIDER_DESCRIPTOR,
|
|
69
|
+
createFastlyLoweringPlan,
|
|
70
|
+
validateStaticBackendBindings,
|
|
71
|
+
assertFastlySupports
|
|
72
|
+
});
|