@pulse-compute/provider-fastly 1.0.0-beta.8 → 1.0.0-beta.9
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 +6 -6
- package/dist/config-schema.json +13 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/build/canonical-target.js +5 -0
- package/src/build/entities-native.js +4 -13
- package/src/build/native-body-forwarding.as.ts +334 -0
- package/src/build/native-body-forwarding.js +69 -0
- package/src/build/native-handle-ownership.js +127 -0
- package/src/build/native-http-effects.js +4 -0
- package/src/build/native-platform-capabilities.js +45 -18
- package/src/build/native-stdio.js +20 -0
- package/src/build/native-string-values.js +2 -2
- package/src/build/native-value-budget.js +33 -1
- package/src/build/schema-admission.js +3 -3
- package/src/config-api.js +1 -0
- package/src/config-schema.json +13 -0
- package/src/index.ts +11 -0
- package/src/provider-contract.js +6 -3
- package/src/testing/native-platform-capabilities-host.js +93 -8
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +4 -0
- package/src/toolchain/index.js +1 -1
- package/src/toolchain/native-execution.js +6 -1
|
@@ -254,6 +254,14 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
254
254
|
}
|
|
255
255
|
const imports = {
|
|
256
256
|
fastly_async_io: {
|
|
257
|
+
is_ready(handle, out) {
|
|
258
|
+
const item = pending.get(Number(handle)) || bodies.get(Number(handle));
|
|
259
|
+
if (!item) return FASTLY_STATUS_BADF;
|
|
260
|
+
const ready = monotonicMs >= (item.readyAt || 0) ? 1 : 0;
|
|
261
|
+
writeU32(out, ready);
|
|
262
|
+
trace.push({module:'fastly_async_io',name:'is_ready',handle:Number(handle),ready,now:monotonicMs});
|
|
263
|
+
return FASTLY_STATUS_OK;
|
|
264
|
+
},
|
|
257
265
|
select(handles, count, timeout, done) {
|
|
258
266
|
if (Number(count) < 1 || Number(timeout) <= 0) return FASTLY_STATUS_ERROR;
|
|
259
267
|
const selected = Array.from({length:Number(count)},(_,i) => view().getUint32(Number(handles)+4*i,true));
|
|
@@ -270,6 +278,28 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
270
278
|
}
|
|
271
279
|
},
|
|
272
280
|
wasi_snapshot_preview1: {
|
|
281
|
+
fd_write(fd, iovs, count, writtenOut) {
|
|
282
|
+
// WASI errno values, independent of the Fastly status enum.
|
|
283
|
+
if (Number(fd) !== 1) return 8; // BADF
|
|
284
|
+
if (options.logWriteFailure === true) {
|
|
285
|
+
trace.push({ module: 'wasi_snapshot_preview1', name: 'fd_write', fd: 1, accepted: false });
|
|
286
|
+
return 29; // IO
|
|
287
|
+
}
|
|
288
|
+
const chunks = [];
|
|
289
|
+
for (let index = 0; index < Number(count); index++) {
|
|
290
|
+
const pointer = Number(iovs) + index * 8;
|
|
291
|
+
chunks.push(readBytes(view().getUint32(pointer, true), view().getUint32(pointer + 4, true)));
|
|
292
|
+
}
|
|
293
|
+
const bytes = Buffer.concat(chunks);
|
|
294
|
+
const written = Math.min(bytes.length, options.logWriteMaxBytes ?? bytes.length);
|
|
295
|
+
const emitted = bytes.subarray(0, written).toString('utf8');
|
|
296
|
+
if (typeof options.onLogWrite === 'function') options.onLogWrite(emitted);
|
|
297
|
+
const message = redactText(emitted);
|
|
298
|
+
writeU32(writtenOut, written);
|
|
299
|
+
logs.push(Object.freeze({ endpoint: 'stdout', message }));
|
|
300
|
+
trace.push({ module: 'wasi_snapshot_preview1', name: 'fd_write', fd: 1, count: Number(count), bytes: bytes.length, written, message, accepted: true });
|
|
301
|
+
return 0;
|
|
302
|
+
},
|
|
273
303
|
clock_time_get(clockId, precision, timeOut) {
|
|
274
304
|
if (Number(clockId) === 1 && typeof options.monotonicClock === 'function') {
|
|
275
305
|
const sample = options.monotonicClock(monotonicMs);
|
|
@@ -312,6 +342,11 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
312
342
|
fastly_log: {
|
|
313
343
|
endpoint_get(namePointer, nameLength, handleOut) {
|
|
314
344
|
const endpoint = readUtf8(namePointer, nameLength);
|
|
345
|
+
// Match Viceroy/Compute: these names belong to stdio or the platform.
|
|
346
|
+
if (/^(?:stdout$|stderr$|fst_managed_)/i.test(endpoint)) {
|
|
347
|
+
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
|
|
348
|
+
return FASTLY_STATUS_INVALID_ARGUMENT;
|
|
349
|
+
}
|
|
315
350
|
if (options.logEndpointFailure === true) {
|
|
316
351
|
trace.push({ module: 'fastly_log', name: 'endpoint_get', endpoint, opened: false });
|
|
317
352
|
return FASTLY_STATUS_ERROR;
|
|
@@ -426,6 +461,23 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
426
461
|
if (monotonicMs < item.readyAt) { writeU32(done, 0); return FASTLY_STATUS_OK; }
|
|
427
462
|
writeU32(done, 1); return imports.fastly_http_req.pending_req_wait(handle, responseOut, bodyOut);
|
|
428
463
|
},
|
|
464
|
+
send_async_streaming(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
465
|
+
const request = requests.get(Number(handle)), body = bodies.get(Number(bodyHandle));
|
|
466
|
+
if (!request || !body) return FASTLY_STATUS_BADF;
|
|
467
|
+
const backend = readUtf8(backendPointer, backendLength);
|
|
468
|
+
const fixture = resolveFixture(options.fixtures, backend, request.method, request.url);
|
|
469
|
+
requests.delete(Number(handle));
|
|
470
|
+
if (!fixture || fixture.sendStatus) { bodies.delete(Number(bodyHandle)); return Number(fixture?.sendStatus || 1); }
|
|
471
|
+
const pendingHandle = alloc('pending');
|
|
472
|
+
const record = {backend, method:request.method, url:redactText(request.url), headers:redactRuntimeHeaders(request.headers), bodyBytes:Buffer.alloc(0)};
|
|
473
|
+
outboundRequests.push(record);
|
|
474
|
+
const item = {request, backend, fixture, readyAt:fixture.earlyResponse ? monotonicMs + Number(fixture.delayMs || 0) : Infinity};
|
|
475
|
+
pending.set(pendingHandle, item);
|
|
476
|
+
body.streamingUpload = {item, record}; body.readyAt = monotonicMs + Number(options.uploadReadyDelayMs || 0);
|
|
477
|
+
writeU32(pendingOut, pendingHandle);
|
|
478
|
+
trace.push({module:'fastly_http_req',name:'send_async_streaming',bodyHandle:Number(bodyHandle),pendingHandle});
|
|
479
|
+
return FASTLY_STATUS_OK;
|
|
480
|
+
},
|
|
429
481
|
send_async(handle, bodyHandle, backendPointer, backendLength, pendingOut) {
|
|
430
482
|
const request = requests.get(Number(handle));
|
|
431
483
|
const body = bodies.get(Number(bodyHandle));
|
|
@@ -630,11 +682,30 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
630
682
|
responseHandle: Number(handle),
|
|
631
683
|
bodyHandle: Number(bodyHandle)
|
|
632
684
|
};
|
|
685
|
+
if (Number(streaming) === 1) responses.delete(Number(handle));
|
|
633
686
|
return FASTLY_STATUS_OK;
|
|
634
687
|
}
|
|
635
688
|
},
|
|
636
689
|
fastly_http_body: {
|
|
637
|
-
close(handle) {
|
|
690
|
+
close(handle) {
|
|
691
|
+
trace.push({module:'fastly_http_body',name:'close',handle:Number(handle)});
|
|
692
|
+
const body = bodies.get(Number(handle));
|
|
693
|
+
if (body?.streamingUpload) {
|
|
694
|
+
const {item,record} = body.streamingUpload;
|
|
695
|
+
record.bodyBytes = bodyBytes(handle);
|
|
696
|
+
if (item.fixture.echoBody) item.fixture = {...item.fixture,body:record.bodyBytes};
|
|
697
|
+
item.readyAt = item.fixture.neverHeaders ? Infinity : monotonicMs + Number(item.fixture.delayMs || 0);
|
|
698
|
+
}
|
|
699
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.finished = true; }
|
|
700
|
+
return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF;
|
|
701
|
+
},
|
|
702
|
+
abandon(handle) {
|
|
703
|
+
trace.push({module:'fastly_http_body',name:'abandon',handle:Number(handle)});
|
|
704
|
+
const body = bodies.get(Number(handle));
|
|
705
|
+
if (body?.streamingUpload) { body.streamingUpload.record.bodyBytes = bodyBytes(handle); body.streamingUpload.record.aborted = true; }
|
|
706
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) { downstream.bodyBytes = bodyBytes(handle); downstream.aborted = true; }
|
|
707
|
+
return bodies.delete(Number(handle)) ? FASTLY_STATUS_OK : FASTLY_STATUS_BADF;
|
|
708
|
+
},
|
|
638
709
|
new(handleOut) {
|
|
639
710
|
const handle = alloc('body');
|
|
640
711
|
bodies.set(handle, { bytes: Buffer.alloc(0), readOffset: 0, writes: [] });
|
|
@@ -653,19 +724,21 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
653
724
|
body.readOffset = offset + count;
|
|
654
725
|
body.readyAt = monotonicMs + Number(body.fixture && body.fixture.chunkDelayMs || 0);
|
|
655
726
|
writeU32(readOut, count);
|
|
656
|
-
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count });
|
|
727
|
+
trace.push({ module: 'fastly_http_body', name: 'read', handle: Number(handle), bytes: count, ...(options.traceBodyIo === true ? {pointer:Number(buffer), capacity:Number(bufferLength), now:monotonicMs} : {}) });
|
|
657
728
|
return FASTLY_STATUS_OK;
|
|
658
729
|
},
|
|
659
730
|
write(handle, buffer, bufferLength, end, writtenOut) {
|
|
660
731
|
const body = bodies.get(Number(handle));
|
|
661
732
|
if (!body) return FASTLY_STATUS_BADF;
|
|
662
733
|
if (downstream?.streaming && Number(handle) === downstream.bodyHandle && options.downstreamBodyWriteStatus) return Number(options.downstreamBodyWriteStatus);
|
|
663
|
-
if (requests.size > 1 && options.outboundBodyWriteStatus) return Number(options.outboundBodyWriteStatus);
|
|
664
|
-
const length = Math.min(Number(bufferLength), Number(options.bodyWriteChunkBytes
|
|
734
|
+
if ((requests.size > 1 || body.streamingUpload) && options.outboundBodyWriteStatus) return Number(options.outboundBodyWriteStatus);
|
|
735
|
+
const length = Math.min(Number(bufferLength), Number(options.bodyWriteChunkBytes ?? bufferLength));
|
|
665
736
|
const bytes = readBytes(buffer, length);
|
|
666
737
|
body.writes.push(bytes);
|
|
738
|
+
if (body.streamingUpload) body.readyAt = monotonicMs + Number(options.uploadWriteDelayMs || 0);
|
|
739
|
+
if (downstream?.streaming && downstream.bodyHandle === Number(handle)) body.readyAt = monotonicMs + Number(options.downstreamWriteDelayMs || 0);
|
|
667
740
|
writeU32(writtenOut, bytes.length);
|
|
668
|
-
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end) });
|
|
741
|
+
trace.push({ module: 'fastly_http_body', name: 'write', handle: Number(handle), bytes: bytes.length, end: Number(end), ...(options.traceBodyIo === true ? {pointer:Number(buffer), capacity:Number(bufferLength), now:monotonicMs} : {}) });
|
|
669
742
|
return FASTLY_STATUS_OK;
|
|
670
743
|
}
|
|
671
744
|
}
|
|
@@ -680,11 +753,16 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
680
753
|
// Throwing stops the Wasm invocation; it never fabricates an effect result.
|
|
681
754
|
const checkActive = () => { if (options.signal && options.signal.aborted) throw options.signal.reason || new Error('Request cancelled.'); };
|
|
682
755
|
checkActive();
|
|
683
|
-
|
|
756
|
+
// A test-only post-call hook can corrupt ABI outputs or advance the clock.
|
|
757
|
+
// It observes the real generated guest; it is never used by provider execution.
|
|
758
|
+
if (options.signal || options.hostcallDelayMs || options.afterHostcall) for (const [moduleName, namespace] of Object.entries(imports)) {
|
|
684
759
|
for (const [name, hostcall] of Object.entries(namespace)) if (typeof hostcall === 'function') namespace[name] = (...args) => {
|
|
685
760
|
checkActive(); const result = hostcall(...args);
|
|
686
761
|
monotonicMs += Number(options.hostcallDelayMs?.[`${moduleName}.${name}`] || 0);
|
|
687
|
-
|
|
762
|
+
const replacement = options.afterHostcall?.({module:moduleName,name,args,result,trace,
|
|
763
|
+
writeU32,writeU64,readU32:pointer=>view().getUint32(Number(pointer),true),
|
|
764
|
+
advance:ms=>{monotonicMs+=ms;}});
|
|
765
|
+
checkActive(); return replacement ?? result;
|
|
688
766
|
};
|
|
689
767
|
}
|
|
690
768
|
const module = new WebAssembly.Module(wasm);
|
|
@@ -716,7 +794,7 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
716
794
|
const pulseLastError = typeof instance.exports.pulse_last_error_code === 'function'
|
|
717
795
|
? Number(instance.exports.pulse_last_error_code())
|
|
718
796
|
: undefined;
|
|
719
|
-
if (downstream?.streaming && !downstream.finished) { downstream.bodyBytes = bodyBytes(downstream.bodyHandle); downstream.aborted = true; }
|
|
797
|
+
if (downstream?.streaming && !downstream.finished) { downstream.bodyBytes = bodyBytes(downstream.bodyHandle) || downstream.bodyBytes; downstream.aborted = true; }
|
|
720
798
|
const handledJwtError = options.allowHandledJwtError === true
|
|
721
799
|
&& lastError === 1008
|
|
722
800
|
&& Boolean(downstream);
|
|
@@ -759,6 +837,13 @@ function executeFastlyNativePlatformCapabilities(input, options = {}) {
|
|
|
759
837
|
monotonicMs,
|
|
760
838
|
transportResources: Object.freeze({pendingRequests: pending.size,
|
|
761
839
|
outboundRequests: requests.size - Number(requests.has(downstreamRequestHandle))}),
|
|
840
|
+
// Non-streaming send transfers these two handles even though the fixture
|
|
841
|
+
// keeps their bytes for assertions. Pending work is counted separately:
|
|
842
|
+
// Fastly has no cancellation hostcall for an unresolved pending request.
|
|
843
|
+
forwardingResources: Object.freeze({
|
|
844
|
+
bodies: bodies.size - Number(!downstream?.streaming && bodies.has(downstream?.bodyHandle)),
|
|
845
|
+
responses: responses.size - Number(!downstream?.streaming && responses.has(downstream?.responseHandle))
|
|
846
|
+
}),
|
|
762
847
|
instance
|
|
763
848
|
});
|
|
764
849
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_BODY_BYTES = 1048576;
|
|
4
|
+
function forwardingError(message) {
|
|
5
|
+
const error = new TypeError(message);
|
|
6
|
+
error.code = 'PULSE_FASTLY_BODY_FORWARDING_INVALID';
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
function normalizeBodyForwarding(value, maxDurationMs) {
|
|
10
|
+
if (value === undefined) return undefined;
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
12
|
+
|| Object.keys(value).some(key => !['requestMaxBytes', 'responseMaxBytes'].includes(key))) {
|
|
13
|
+
throw forwardingError('fastly.bodyForwarding requires requestMaxBytes and responseMaxBytes.');
|
|
14
|
+
}
|
|
15
|
+
for (const key of ['requestMaxBytes', 'responseMaxBytes']) {
|
|
16
|
+
if (!Number.isInteger(value[key]) || value[key] < 1 || value[key] > MAX_BODY_BYTES) {
|
|
17
|
+
throw forwardingError(`fastly.bodyForwarding.${key} must be an integer from 1 to ${MAX_BODY_BYTES}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isInteger(maxDurationMs) || maxDurationMs < 1 || maxDurationMs > 30000) {
|
|
21
|
+
throw forwardingError('fastly.bodyForwarding requires maxDurationMs from 1 to 30000.');
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze({requestMaxBytes: value.requestMaxBytes, responseMaxBytes: value.responseMaxBytes});
|
|
24
|
+
}
|
|
25
|
+
module.exports = {normalizeBodyForwarding, forwardingError, MAX_BODY_BYTES};
|
package/src/toolchain/config.js
CHANGED
|
@@ -34,6 +34,7 @@ function normalizeFastlyProviderConfig(value) {
|
|
|
34
34
|
: {};
|
|
35
35
|
return Object.freeze({
|
|
36
36
|
kind: 'fastly',
|
|
37
|
+
bodyForwarding: require('./body-forwarding.js').normalizeBodyForwarding(input.bodyForwarding, input.maxDurationMs),
|
|
37
38
|
maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(input.maxDurationMs),
|
|
38
39
|
version: input.version ? String(input.version) : undefined,
|
|
39
40
|
bindings: Object.freeze({
|
|
@@ -71,6 +72,7 @@ function fastlyProjectConfigDocument(config) {
|
|
|
71
72
|
kind: 'fastly',
|
|
72
73
|
bindings: config.bindings,
|
|
73
74
|
maxDurationMs: config.maxDurationMs,
|
|
75
|
+
bodyForwarding: config.bodyForwarding,
|
|
74
76
|
build: config.build,
|
|
75
77
|
local: config.local,
|
|
76
78
|
providerSpecificUserland: false
|
|
@@ -104,6 +106,8 @@ const FASTLY_PROJECT_CONFIG_REFERENCE = Object.freeze({
|
|
|
104
106
|
})
|
|
105
107
|
]),
|
|
106
108
|
fields: Object.freeze([
|
|
109
|
+
Object.freeze({section:'fastly',path:'fastly.bodyForwarding',type:'object',default:'omitted',scope:'experimental Fastly Native forwarding',description:'Opt in to one terminal opaque POST with inline req.body() and a static backend. Requires maxDurationMs and both byte limits. Final close hands queued delivery to Fastly; it is not a client receipt.',diagnostics:Object.freeze(['PULSE_FASTLY_BODY_FORWARDING_INVALID'])}),
|
|
110
|
+
...['requestMaxBytes','responseMaxBytes'].map(name => Object.freeze({section:'fastly',path:`fastly.bodyForwarding.${name}`,type:'integer',allowed:'1–1048576',default:'required with bodyForwarding',scope:'experimental Fastly Native forwarding',description:'Independent actual-byte bound, including EOF verification; no unbounded default. Fixed 16 KiB raw scratch is reused across the sequential upload and response pumps.'})),
|
|
107
111
|
Object.freeze({section:'fastly',path:'fastly.maxWasmBytes',type:'integer',allowed:'positive safe integer',default:'4194304 (4 MiB)',scope:'fastly() facade',description:'Facade option projected to build.maxWasmBytes. Conventional profiles configure fastly.build.maxWasmBytes.'}),
|
|
108
112
|
Object.freeze({section:'fastly',path:'fastly.build.maxWasmBytes',type:'integer',allowed:'positive safe integer',default:'4194304 (4 MiB)',scope:'Native final Wasm artifact',description:'Inclusive byte budget checked for newly compiled and reused artifacts. Raising it does not increase runtime memory or change Fastly service limits. The fastly() facade accepts maxWasmBytes.'}),
|
|
109
113
|
Object.freeze({section:'fastly',path:'fastly.maxDurationMs',type:'integer',allowed:'1–30000',default:'omitted',scope:'HTTP request execution',description:'One provider-owned monotonic budget through buffered response handoff; expiry attempts 504 before headers commit and never asserts rollback of dispatched writes. No CPU preemption or post-handoff delivery guarantee.'}),
|
package/src/toolchain/index.js
CHANGED
|
@@ -271,7 +271,7 @@ function createDriver() {
|
|
|
271
271
|
execute: fastlyRuntime.executeCanonicalProgram,
|
|
272
272
|
prepareNativeExecution: require('./native-execution.js').prepareFastlyNativeExecution,
|
|
273
273
|
createLoweringPlan(metadata, providerConfig = {}) {
|
|
274
|
-
return createFastlyLoweringPlan(metadata, providerConfig.bindings || {});
|
|
274
|
+
return createFastlyLoweringPlan(metadata, providerConfig.bindings || {}, providerConfig);
|
|
275
275
|
},
|
|
276
276
|
inspectRealization(invocation) {
|
|
277
277
|
const realization = inspectFastlyCanonicalTarget({
|
|
@@ -59,7 +59,12 @@ function prepareFastlyNativeExecution(invocation) {
|
|
|
59
59
|
response: Object.freeze({
|
|
60
60
|
...execution.response,
|
|
61
61
|
kind: type.includes('application/json') || type.includes('+json') ? 'json' : type.startsWith('text/') ? 'text' : 'response',
|
|
62
|
-
|
|
62
|
+
// Forwarding filters origin metadata into a new response handle, so
|
|
63
|
+
// its streamed payload remains opaque even though origin is false.
|
|
64
|
+
// Pre-commit errors use ordinary non-streaming structured responses.
|
|
65
|
+
bodyClass: execution.response.origin === true
|
|
66
|
+
|| (native.plan.capabilities.includes('request.body.forward') && execution.response.streaming === 1)
|
|
67
|
+
? 'opaque' : 'structured'
|
|
63
68
|
}),
|
|
64
69
|
// The fixture ABI has hostcall traces, not canonical continuation telemetry.
|
|
65
70
|
effectCount: null,
|