@pulse-compute/provider-fastly 1.0.0-beta.10 → 1.0.0-beta.11
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/package.json +11 -11
- package/src/build/canonical-target.js +6 -0
- package/src/build/effect-invocations.js +6 -4
- package/src/build/host-extensions.js +254 -0
- package/src/build/lifecycle.js +50 -0
- package/src/build/native-body-forwarding.as.ts +28 -44
- package/src/build/native-body-forwarding.js +15 -11
- package/src/build/native-fetch-accounting.as.ts +133 -0
- package/src/build/native-fetch-accounting.js +84 -0
- package/src/build/native-fetch-bounds.js +47 -0
- package/src/build/native-fetch-cohorts.as.ts +126 -0
- package/src/build/native-fetch-cohorts.js +65 -0
- package/src/build/native-fetch-deadlines.as.ts +13 -8
- package/src/build/native-fetch-deadlines.js +13 -4
- package/src/build/native-fetch-errors.js +2 -2
- package/src/build/native-fetch-headers.js +52 -0
- package/src/build/native-fetch-outcomes.js +87 -0
- package/src/build/native-fetch-timing.as.ts +74 -0
- package/src/build/native-fetch-timing.js +50 -0
- package/src/build/native-forwarding-accounting.as.ts +153 -0
- package/src/build/native-forwarding-accounting.js +47 -0
- package/src/build/native-header-lookup.js +2 -2
- package/src/build/native-http-effects.js +10 -4
- package/src/build/native-platform-capabilities.js +89 -39
- package/src/build/native-request-body.js +6 -5
- package/src/build/{request-budget.as.ts → request-budget-source.js} +12 -4
- package/src/build/request-budget.js +7 -8
- package/src/build/response-authority.js +56 -0
- package/src/javascript/fetch-adapter.js +3 -0
- package/src/javascript/runtime-host.js +1 -0
- package/src/provider-contract.js +2 -1
- package/src/testing/native-http-effects-host.js +9 -0
- package/src/testing/native-platform-capabilities-host.js +10 -0
- package/src/toolchain/index.js +2 -1
- package/src/toolchain/native-execution.js +7 -2
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
> **Supported entry points:** `@pulse-compute/provider-fastly`, `@pulse-compute/provider-fastly/contract`, `@pulse-compute/provider-fastly/toolchain`, `@pulse-compute/provider-fastly/config-schema`, `@pulse-compute/provider-fastly/runtime/canonical-api-runtime`, `@pulse-compute/provider-fastly/build/canonical-target`, `@pulse-compute/provider-fastly/testing/fastly-cli`<br>
|
|
8
8
|
> **Stability:** The listed entry points are supported for the Beta; all other exported subpaths are implementation-only.<br>
|
|
9
9
|
> **npm:** [`@pulse-compute/provider-fastly`](https://www.npmjs.com/package/@pulse-compute/provider-fastly)<br>
|
|
10
|
-
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.11/packages/provider-fastly/)
|
|
11
11
|
>
|
|
12
|
-
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.11` package policy.
|
|
13
13
|
<!-- pulse-package-status:end -->
|
|
14
14
|
|
|
15
15
|
Fastly provider configuration, canonical capability lowering, local conformance execution, Compute target generation, and explicit external-tool boundaries.
|
|
@@ -49,7 +49,7 @@ trusted wall-clock authority. It does not fall back to Node execution.
|
|
|
49
49
|
|
|
50
50
|
## Documentation
|
|
51
51
|
|
|
52
|
-
- [Fastly package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
53
|
-
- [Project configuration](https://pulsecompute.io/v1.0.0-beta.
|
|
54
|
-
- [Fastly capabilities guide](https://pulsecompute.io/v1.0.0-beta.
|
|
55
|
-
- [Environment variables](https://pulsecompute.io/v1.0.0-beta.
|
|
52
|
+
- [Fastly package guide](https://pulsecompute.io/v1.0.0-beta.11/packages/provider-fastly/)
|
|
53
|
+
- [Project configuration](https://pulsecompute.io/v1.0.0-beta.11/reference/project-config/#fastly-provider-options)
|
|
54
|
+
- [Fastly capabilities guide](https://pulsecompute.io/v1.0.0-beta.11/guides/fastly-capabilities/)
|
|
55
|
+
- [Environment variables](https://pulsecompute.io/v1.0.0-beta.11/reference/environment/#pulse-fastly-bin)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/provider-fastly",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.11",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@pulse-compute/crypto": "1.0.0-beta.
|
|
63
|
-
"@pulse-compute/jwt": "1.0.0-beta.
|
|
64
|
-
"@pulse-compute/runtime": "1.0.0-beta.
|
|
65
|
-
"@pulse-compute/s3": "1.0.0-beta.
|
|
66
|
-
"@pulse-compute/wasm-build-support": "1.0.0-beta.
|
|
67
|
-
"@pulse-compute/wasm-contracts": "1.0.0-beta.
|
|
68
|
-
"@pulse-compute/wasm-guest-link": "1.0.0-beta.
|
|
69
|
-
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.
|
|
70
|
-
"@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.
|
|
62
|
+
"@pulse-compute/crypto": "1.0.0-beta.11",
|
|
63
|
+
"@pulse-compute/jwt": "1.0.0-beta.11",
|
|
64
|
+
"@pulse-compute/runtime": "1.0.0-beta.11",
|
|
65
|
+
"@pulse-compute/s3": "1.0.0-beta.11",
|
|
66
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.11",
|
|
67
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.11",
|
|
68
|
+
"@pulse-compute/wasm-guest-link": "1.0.0-beta.11",
|
|
69
|
+
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.11",
|
|
70
|
+
"@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.11",
|
|
71
71
|
"esbuild": "0.28.1"
|
|
72
72
|
},
|
|
73
73
|
"types": "./dist/index.d.ts",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
88
88
|
"directory": "packages/provider-fastly"
|
|
89
89
|
},
|
|
90
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
90
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.11/packages/provider-fastly/",
|
|
91
91
|
"bugs": {
|
|
92
92
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
93
93
|
},
|
|
@@ -82,6 +82,12 @@ function assertExactProviderNative(native) {
|
|
|
82
82
|
}
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
|
+
if (native.manifest.hostExtensions) {
|
|
86
|
+
const evidence = native.manifest.hostExtensions;
|
|
87
|
+
if (evidence.finalWasmSha256 !== observedSha256) throw providerBuildError(
|
|
88
|
+
'PULSE_HOST_EXTENSION_IMPORT', 'Terminal service evidence does not identify the packaged Wasm.', { automaticFallback: false });
|
|
89
|
+
require('@pulse-compute/wasm-contracts/provider/toolchain').hostExtensions.auditWasm(native.wasm, evidence.importPolicy);
|
|
90
|
+
}
|
|
85
91
|
if (native.guestLink) {
|
|
86
92
|
const audit = native.guestLink.audit;
|
|
87
93
|
if (
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const { CANONICAL_EFFECT_INVOCATION_POLICY } = require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
|
|
4
4
|
|
|
5
5
|
function runtimeSource(options = {}) {
|
|
6
|
+
const lifecycle = options.lifecycle;
|
|
7
|
+
const delivery = options.packageCompletion ? 'result == 0 ? pulse_package_set_effect_failure(index) : pulse_set_effect_result(index, result)' : 'pulse_set_effect_result(index, result)';
|
|
6
8
|
return `
|
|
7
9
|
const PULSE_FASTLY_MAX_EFFECT_INVOCATIONS: i32 = ${CANONICAL_EFFECT_INVOCATION_POLICY.defaultMaxEffects}
|
|
8
10
|
let __pulse_invocation_count: i32 = 0
|
|
@@ -28,13 +30,13 @@ function __pulse_invocation_settle(index: i32, ticket: i32, result: i32): i32 {
|
|
|
28
30
|
|| ticket <= 0 || unchecked(__pulse_invocation_tickets[index]) != ticket) {
|
|
29
31
|
__pulse_fastly_fail(PULSE_ERROR_STATE, __pulse_invocation_failure, index); return 0
|
|
30
32
|
}
|
|
31
|
-
unchecked(__pulse_invocation_tickets[index] = 0)
|
|
32
|
-
|
|
33
|
+
${lifecycle?.site('effect-settle') ? lifecycle.site('effect-settle') + '\n' : ''} unchecked(__pulse_invocation_tickets[index] = 0)
|
|
34
|
+
${lifecycle ? lifecycle.site('effect-deliver', delivery) : 'return ' + delivery}
|
|
33
35
|
}
|
|
34
36
|
function __pulse_invocation_close(): void {
|
|
35
|
-
__pulse_invocation_closed = true
|
|
37
|
+
${lifecycle?.site('invocation-close') ? lifecycle.site('invocation-close') + '\n' : ''} __pulse_invocation_closed = true
|
|
36
38
|
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i += 1) unchecked(__pulse_invocation_tickets[i] = 0)
|
|
37
|
-
}
|
|
39
|
+
${lifecycle?.site('invocation-closed') ? lifecycle.site('invocation-closed') + '\n' : ''}}
|
|
38
40
|
`;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The provider owns the terminal service. Package selection and ABI admission
|
|
4
|
+
// use the shared contribution contract; trusted source is not sandboxed.
|
|
5
|
+
const contract = require('@pulse-compute/wasm-contracts/provider/toolchain').hostExtensions;
|
|
6
|
+
const { CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES } = require('@pulse-compute/wasm-contracts/handler/canonical-native-runtime');
|
|
7
|
+
const MAX_BACKEND_BYTES = Math.min(1024, CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES);
|
|
8
|
+
const { STATES } = require('./response-authority.js');
|
|
9
|
+
const ADMISSION = contract.normalizeAdmission({ version: contract.ADMISSION_VERSION,
|
|
10
|
+
protocols: [contract.VERSION], services: [contract.TERMINAL_SERVICE] });
|
|
11
|
+
const TARGET = Object.freeze({ provider: 'fastly', target: 'native', targetId: 'fastly-compute-native', hostExtensions: ADMISSION });
|
|
12
|
+
function fail(message, operation) { contract.fail('PLACEMENT', message, operation); }
|
|
13
|
+
const matches = contract.matches;
|
|
14
|
+
function validateBinding(effect, bindings = {}) {
|
|
15
|
+
const key = effect.resource?.value;
|
|
16
|
+
const backend = effect.resource?.kind === 'literal' && (bindings.backends || {})[key];
|
|
17
|
+
if (typeof key !== 'string' || typeof backend !== 'string' || !backend || backend.includes('\0') || Buffer.byteLength(backend) > MAX_BACKEND_BYTES)
|
|
18
|
+
contract.fail('BINDING', `${effect.package}.${effect.operation} requires a configured static backend binding.`, effect);
|
|
19
|
+
return backend;
|
|
20
|
+
}
|
|
21
|
+
function select(plan, options = {}) {
|
|
22
|
+
try { return selectPlan(plan, options); }
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code?.startsWith('PULSE_HOST_EXTENSION_') && plan.entry?.kind === 'router') {
|
|
25
|
+
const used = (plan.effects || []).filter(effect => effect.kind === 'host.extension');
|
|
26
|
+
const effect = used.find(effect => effect.package === error.detail?.package && effect.operation === error.detail?.operation) || used[0];
|
|
27
|
+
const entry = plan.routing?.entries?.find(entry => entry.stableId === effect?.routerEntryStableId);
|
|
28
|
+
const source = entry?.nativeBody?.source || plan.handlers?.find(handler => handler.id === effect?.routerEntryStableId)?.source || null;
|
|
29
|
+
// Lowered Router positions address generated dispatch text. Public errors
|
|
30
|
+
// must identify the authored handler and state the coarser source basis.
|
|
31
|
+
error.detail = Object.freeze({ ...error.detail, loc: source, sourceBasis: source ? 'authored-entry' : 'not-recorded' });
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function selectPlan(plan, options = {}) {
|
|
37
|
+
const effects = plan.effects || [];
|
|
38
|
+
const used = effects.filter(effect => effect.kind === 'host.extension');
|
|
39
|
+
if (!used.length) return [];
|
|
40
|
+
const reject = message => fail(message, used[0]);
|
|
41
|
+
// Reuse the canonical Router dispatcher and private terminal-route bodies.
|
|
42
|
+
// Admission bounds authored behavior; no new scheduler or package special case.
|
|
43
|
+
const routed = plan.entry?.kind === 'router';
|
|
44
|
+
if (used.length !== 1 || effects.length !== 1 || used[0].grouped || used[0].result?.mode !== 'return'
|
|
45
|
+
|| used[0].declaredResult !== 'host-terminal'
|
|
46
|
+
|| (plan.helpers || []).length || (plan.callables || []).length || (plan.stages || []).length
|
|
47
|
+
|| options.bodyForwarding || options.bindings?.dynamicBackends || options.guestUnits?.length
|
|
48
|
+
|| plan.packages?.application || plan.logging?.enabledStatements)
|
|
49
|
+
reject('only one direct terminal effect without surrounding effects or composition is admitted.');
|
|
50
|
+
const handlers = new Map((plan.handlers || []).map(handler => [handler.id, handler]));
|
|
51
|
+
const entries = plan.routing?.entries || [];
|
|
52
|
+
if (routed) {
|
|
53
|
+
if (!entries.length || entries.length !== handlers.size
|
|
54
|
+
|| entries.some(entry => entry.kind !== 'route' || entry.nativeBody?.family !== 'terminal-route'
|
|
55
|
+
|| !handlers.has(entry.stableId))
|
|
56
|
+
|| used[0].routerEntryKind !== 'route' || !handlers.has(used[0].routerEntryStableId)
|
|
57
|
+
|| !used[0].statementPath?.startsWith(`handlers.${used[0].routerEntryStableId}.body[`))
|
|
58
|
+
reject('terminal Router placement requires private route bodies without middleware or error composition.');
|
|
59
|
+
} else if (plan.entry?.kind !== 'handler' || handlers.size || entries.length
|
|
60
|
+
|| !used[0].statementPath?.startsWith('entry.body[')) {
|
|
61
|
+
reject('terminal placement requires a plain HTTP handler or one selected Router route.');
|
|
62
|
+
}
|
|
63
|
+
const dispatcherLocals = new Set((plan.locals || []).filter(local => local.scopeId === 'entry').map(local => local.id));
|
|
64
|
+
function expression(value, dispatcher = false) {
|
|
65
|
+
if (!value || typeof value !== 'object') return;
|
|
66
|
+
if (dispatcher && value.kind === 'local' && dispatcherLocals.has(value.id)) return;
|
|
67
|
+
if (dispatcher && value.kind === 'undefined') return;
|
|
68
|
+
if (dispatcher && value.kind === 'assignment' && value.operator === '='
|
|
69
|
+
&& value.target?.kind === 'local' && dispatcherLocals.has(value.target.id)) {
|
|
70
|
+
expression(value.value, true); return;
|
|
71
|
+
}
|
|
72
|
+
// Router exhaustion emits a literal status option; authored text siblings
|
|
73
|
+
// may use that same bounded response shape, without evaluating callbacks.
|
|
74
|
+
if (routed && value.kind === 'object' && value.entries?.every(entry => entry.kind === 'property'
|
|
75
|
+
&& entry.key?.kind === 'literal' && entry.key.value === 'status' && entry.value?.kind === 'literal'
|
|
76
|
+
&& Number.isInteger(entry.value.value) && entry.value.value >= 100 && entry.value.value <= 599)) return;
|
|
77
|
+
if (!['literal', 'context-read', 'binary', 'intrinsic'].includes(value.kind)) reject('unsupported expression in the terminal proof.');
|
|
78
|
+
if (value.kind === 'context-read' && !['req.path', 'req.method', 'req.url'].includes(value.path.join('.'))) reject('request mutation/body/state is outside this proof.');
|
|
79
|
+
if (value.kind === 'intrinsic' && value.name !== 'response.text' && !(dispatcher && value.name === 'router.match'))
|
|
80
|
+
reject('only a plain text response control is admitted beside the handoff.');
|
|
81
|
+
for (const child of [value.left, value.right, ...(value.arguments || [])]) if (child) expression(child, dispatcher);
|
|
82
|
+
}
|
|
83
|
+
let occurrences = 0;
|
|
84
|
+
const calls = new Map();
|
|
85
|
+
function statements(body, owner) {
|
|
86
|
+
const dispatcher = routed && owner === 'entry';
|
|
87
|
+
for (const [index, statement] of body.entries()) {
|
|
88
|
+
if (statement.kind === 'if') { expression(statement.test, dispatcher); statements(statement.then, owner); statements(statement.else, owner); }
|
|
89
|
+
else if (statement.kind === 'return') {
|
|
90
|
+
if (statement.value?.kind !== 'intrinsic' || statement.value.name !== 'response.text') reject('Ordinary terminal branches must return a text response.');
|
|
91
|
+
expression(statement.value, dispatcher);
|
|
92
|
+
} else if (statement.kind === 'effect') {
|
|
93
|
+
if (dispatcher || statement.effectId !== used[0].id || statement.result?.mode !== 'return'
|
|
94
|
+
|| (routed && owner !== used[0].routerEntryStableId)) reject('handoff must return directly from its selected route.');
|
|
95
|
+
occurrences++;
|
|
96
|
+
} else if (dispatcher && statement.kind === 'handler-call' && handlers.has(statement.handlerId)) {
|
|
97
|
+
calls.set(statement.handlerId, (calls.get(statement.handlerId) || 0) + 1);
|
|
98
|
+
} else if (dispatcher && statement.kind === 'local' && dispatcherLocals.has(statement.localId)) {
|
|
99
|
+
expression(statement.value, true);
|
|
100
|
+
} else if (dispatcher && statement.kind === 'expression') {
|
|
101
|
+
expression(statement.expression, true);
|
|
102
|
+
} else if (routed && !dispatcher && index === 0 && statement.kind === 'local'
|
|
103
|
+
&& statement.name === `__pulse_router_transferred_${entries.find(entry => entry.stableId === owner)?.index}`
|
|
104
|
+
&& handlers.get(owner)?.localIds.includes(statement.localId)
|
|
105
|
+
&& statement.value?.kind === 'literal' && statement.value.value === false) {
|
|
106
|
+
// The canonical route body's private transfer flag, not an authored token.
|
|
107
|
+
} else reject('only bounded route dispatch, pure conditions, direct handoff and text responses are admitted.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
statements(plan.entry.body, 'entry');
|
|
111
|
+
for (const handler of handlers.values()) {
|
|
112
|
+
if (calls.get(handler.id) !== 1) reject('each terminal route requires exactly one dispatcher call.');
|
|
113
|
+
statements(handler.body, handler.id);
|
|
114
|
+
}
|
|
115
|
+
if (occurrences !== 1) reject('exactly one terminal handoff site is required.');
|
|
116
|
+
const effect = used[0];
|
|
117
|
+
const inputs = effect.inputs || [];
|
|
118
|
+
if (inputs.length !== 2 || inputs[0].name !== 'resource' || inputs[1].name !== 'payload'
|
|
119
|
+
|| inputs[1].value?.kind !== 'object' || inputs[1].value.entries?.length !== 0
|
|
120
|
+
|| inputs[0].value?.kind !== 'object' || inputs[0].value.entries?.length !== 2
|
|
121
|
+
|| inputs[0].value.entries.some(entry => entry.kind !== 'property' || entry.key?.kind !== 'literal'
|
|
122
|
+
|| !['kind', 'value'].includes(entry.key.value) || entry.value?.kind !== 'literal'
|
|
123
|
+
|| entry.value.value !== effect.resource?.[entry.key.value])) reject('The terminal service accepts only a static resource and empty payload.');
|
|
124
|
+
const target = options.targetDescriptor ? { provider: 'fastly', ...options.targetDescriptor } : TARGET;
|
|
125
|
+
const [artifact] = contract.select(used, options.realizationArtifacts || [], target);
|
|
126
|
+
const data = artifact.data;
|
|
127
|
+
if (data.service !== contract.TERMINAL_SERVICE || data.imports.length !== 1
|
|
128
|
+
|| JSON.stringify(data.imports[0].parameters) !== '["i32","i32","i32"]'
|
|
129
|
+
|| JSON.stringify(data.imports[0].results) !== '["i32"]')
|
|
130
|
+
contract.fail('SIGNATURE', 'The terminal backend service requires (i32, i32, i32) -> i32.', effect);
|
|
131
|
+
const backend = validateBinding(effect, options.bindings || options);
|
|
132
|
+
return Object.freeze([Object.freeze({ artifact, effect, index: effects.indexOf(effect), backend })]);
|
|
133
|
+
}
|
|
134
|
+
// The terminal service uses driver-owned response authority and binds each
|
|
135
|
+
// selected contribution to a slot: static per-effect data (effect index, ABI
|
|
136
|
+
// entry, backend). Only one terminal handoff can win per request, so the
|
|
137
|
+
// dynamic state (dispatching slot, token, ticket, settlement) is request-scoped.
|
|
138
|
+
function slotSwitch(selected, slot, cases, fallback) {
|
|
139
|
+
return selected.map((item, n) => `if (${slot} == ${n}) ${cases(item, n)}`).join('\n ') + `\n ${fallback}`;
|
|
140
|
+
}
|
|
141
|
+
function source(selected) {
|
|
142
|
+
if (!selected.length) return '';
|
|
143
|
+
const contributed = [...new Map(selected.map(item => [item.artifact.id, item.artifact.data.source])).values()].join('\n');
|
|
144
|
+
return `
|
|
145
|
+
${contributed}
|
|
146
|
+
let __pulse_extension_status: i32 = -1
|
|
147
|
+
let __pulse_extension_slot: i32 = -1
|
|
148
|
+
let __pulse_extension_token: i32 = 0
|
|
149
|
+
let __pulse_extension_invocation: i32 = 0
|
|
150
|
+
let __pulse_extension_delivering: bool = false
|
|
151
|
+
let __pulse_extension_settled: bool = false
|
|
152
|
+
let __pulse_extension_handles_owned: bool = false
|
|
153
|
+
function __pulse_extension_slot_of(effectIndex: i32): i32 {
|
|
154
|
+
${selected.map((item, n) => `if (effectIndex == ${item.index}) return ${n}`).join('\n ')}
|
|
155
|
+
return -1
|
|
156
|
+
}
|
|
157
|
+
function __pulse_extension_backend(slot: i32): ArrayBuffer {
|
|
158
|
+
${slotSwitch(selected, 'slot', item => `return __pulse_fastly_utf8(${JSON.stringify(item.backend)})`, 'return new ArrayBuffer(0)')}
|
|
159
|
+
}
|
|
160
|
+
function __pulse_extension_call(slot: i32, request: i32, backend: ArrayBuffer): i32 {
|
|
161
|
+
${slotSwitch(selected, 'slot', item => `return ${item.artifact.data.entry}(request, changetype<usize>(backend), backend.byteLength)`, 'return -1')}
|
|
162
|
+
}
|
|
163
|
+
function __pulse_extension_cleanup(): void {
|
|
164
|
+
// This service borrows the request handle. Status zero transfers downstream
|
|
165
|
+
// response authority, not ownership of these local request/body handles.
|
|
166
|
+
// Nonzero/trapped dispatch remains uncertain and is left to host teardown.
|
|
167
|
+
if (!__pulse_extension_handles_owned || (__pulse_extension_slot >= 0 && __pulse_response_authority == ${STATES['outcome-unknown']})) return
|
|
168
|
+
__pulse_extension_handles_owned = false
|
|
169
|
+
const request = __pulse_fastly_request_handle, body = __pulse_fastly_request_body_handle
|
|
170
|
+
__pulse_fastly_request_handle = -1
|
|
171
|
+
__pulse_fastly_request_body_handle = -1
|
|
172
|
+
const requestStatus = __pulse_fetch_close_request(request)
|
|
173
|
+
const bodyStatus = __pulse_fetch_close_body(body)
|
|
174
|
+
if (requestStatus != 0 || bodyStatus != 0) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 285, __pulse_extension_slot)
|
|
175
|
+
}
|
|
176
|
+
function __pulse_extension_begin(effectIndex: i32, payload: __PulseFastlyValue): void {
|
|
177
|
+
const slot = __pulse_extension_slot_of(effectIndex)
|
|
178
|
+
if (slot < 0 || __pulse_response_authority != ${STATES['owned']} || !__pulse_invocation_started || __pulse_invocation_closed || __pulse_fastly_last_error != 0 || unchecked(__pulse_invocation_tickets[effectIndex]) <= 0) { __pulse_fastly_fail(PULSE_ERROR_STATE, 280, effectIndex); return }
|
|
179
|
+
// One bounded backend encoding and one opaque token per admitted invocation.
|
|
180
|
+
const backend = __pulse_extension_backend(slot)
|
|
181
|
+
if (backend.byteLength > ${MAX_BACKEND_BYTES} || __pulse_extension_token != 0) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 280, effectIndex); return }
|
|
182
|
+
// Allocate before dispatch, while we still own downstream response authority.
|
|
183
|
+
const value = new __PulseFastlyValue()
|
|
184
|
+
value.kind = 9 // private provider completion token, never an authored value
|
|
185
|
+
__pulse_extension_token = __pulse_fastly_put(value)
|
|
186
|
+
if (__pulse_extension_token <= 0 || __pulse_fastly_last_error != 0) return
|
|
187
|
+
// Recheck the shared request budget immediately before crossing the ABI.
|
|
188
|
+
if (!__pulse_fetch_request_check(effectIndex) || __pulse_invocation_closed || __pulse_fastly_last_error != 0) return
|
|
189
|
+
__pulse_extension_invocation = unchecked(__pulse_invocation_tickets[effectIndex])
|
|
190
|
+
__pulse_extension_slot = slot
|
|
191
|
+
if (!__pulse_response_begin()) return
|
|
192
|
+
__pulse_extension_status = __pulse_extension_call(slot, __pulse_fastly_request_handle, backend)
|
|
193
|
+
if (__pulse_extension_status != 0) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 281, effectIndex); return }
|
|
194
|
+
__pulse_response_transferred()
|
|
195
|
+
__pulse_extension_cleanup()
|
|
196
|
+
if (!__pulse_fetch_request_check(effectIndex) || __pulse_invocation_closed || __pulse_fastly_last_error != 0) return
|
|
197
|
+
__pulse_fastly_ready_effect(effectIndex, __pulse_extension_token)
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
// Typed contributions to the existing structural lifecycle emitter.
|
|
202
|
+
// Bodies see parameters and module globals, never the caller's local variables.
|
|
203
|
+
const OWNER = 'host-extension.terminal';
|
|
204
|
+
function hooks(selected) {
|
|
205
|
+
if (!selected.length) return [];
|
|
206
|
+
const mine = (effect) => `__pulse_extension_slot_of(${effect}) >= 0`;
|
|
207
|
+
// The dispatched slot only; an undispatched terminal effect can never settle.
|
|
208
|
+
const dispatched = (effect) => `__pulse_extension_slot_of(${effect}) == __pulse_extension_slot`;
|
|
209
|
+
return [
|
|
210
|
+
{ point: 'request.acquired', owner: OWNER, order: 0, body: ' __pulse_extension_handles_owned = true' },
|
|
211
|
+
// Authenticate the terminal value before the ordinary invocation consumes its
|
|
212
|
+
// ticket. A raw exported setter is not authority to complete this operation.
|
|
213
|
+
{ point: 'effect.settle', owner: OWNER, order: 0, body: ` if (!(${mine('effect')})) return true
|
|
214
|
+
if (!(${dispatched('effect')}) || __pulse_response_authority != ${STATES['transferred']} || __pulse_extension_settled || ticket != __pulse_extension_invocation || result != __pulse_extension_token || result <= 0) {
|
|
215
|
+
__pulse_fastly_fail(PULSE_ERROR_STATE, 284, effect); return false
|
|
216
|
+
}
|
|
217
|
+
return true` },
|
|
218
|
+
{ point: 'effect.deliver', owner: OWNER, order: 0,
|
|
219
|
+
before: ` __pulse_extension_delivering = ${mine('effect')}`,
|
|
220
|
+
after: ` __pulse_extension_delivering = false
|
|
221
|
+
if (${mine('effect')} && accepted == 1) __pulse_extension_settled = true` },
|
|
222
|
+
{ point: 'effect.result', owner: OWNER, order: 0, body: ` if (!(${mine('effect')})) return true
|
|
223
|
+
if (!(${dispatched('effect')}) || !__pulse_extension_delivering || __pulse_response_authority != ${STATES['transferred']} || __pulse_extension_settled || result != __pulse_extension_token || __pulse_invocation_closed) {
|
|
224
|
+
__pulse_fastly_fail(PULSE_ERROR_STATE, 284, effect); return false
|
|
225
|
+
}
|
|
226
|
+
return true` },
|
|
227
|
+
{ point: 'invocation.close', owner: OWNER, order: 0, body: ` __pulse_extension_cleanup()
|
|
228
|
+
__pulse_extension_token = 0
|
|
229
|
+
__pulse_extension_invocation = 0
|
|
230
|
+
__pulse_extension_delivering = false` },
|
|
231
|
+
{ point: 'result.claim', owner: OWNER, order: 0, body: ` if (__pulse_response_authority == ${STATES['owned']}) return false
|
|
232
|
+
if (__pulse_response_authority != ${STATES['transferred']} || !__pulse_extension_settled || result != __pulse_extension_token || __pulse_extension_token <= 0) { __pulse_fastly_fail(PULSE_ERROR_STATE, 283, -1); return true }
|
|
233
|
+
__pulse_response_complete()
|
|
234
|
+
__pulse_extension_token = 0
|
|
235
|
+
return true` }
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
function policy(selected, baseline) {
|
|
239
|
+
return contract.composeImportPolicy(baseline, selected.map(item => item.artifact));
|
|
240
|
+
}
|
|
241
|
+
function evidence(selected, lifecycle, importPolicy) {
|
|
242
|
+
if (!selected.length) return undefined;
|
|
243
|
+
return Object.freeze({ version: 'pulse.fastly-terminal-service.v1', service: contract.TERMINAL_SERVICE,
|
|
244
|
+
implementation: require('../provider-contract.js').FASTLY_PROVIDER_DESCRIPTOR.lowering['host.extension'],
|
|
245
|
+
contributions: importPolicy.contributions,
|
|
246
|
+
useSites: selected.map(({artifact,effect,index}) => ({ contributionId: artifact.id, effectId: effect.id, index,
|
|
247
|
+
source: effect.source || null })),
|
|
248
|
+
lifecycle: { executionVersion: lifecycle.plan.executionVersion, httpVersion: lifecycle.plan.httpVersion,
|
|
249
|
+
selectedPoints: lifecycle.plan.sites.map(site => ({ site: site.id, points: site.points.map(point => ({ point: point.point, kind: point.kind, contributions: point.contributions })) })),
|
|
250
|
+
coverage: 'selected-points; normal-return-only after/end/closed; no trap finalizer' },
|
|
251
|
+
limits: { backendBytes: MAX_BACKEND_BYTES, completionValues: 1 },
|
|
252
|
+
accounting: 'not-selected', importPolicy });
|
|
253
|
+
}
|
|
254
|
+
module.exports = { ADMISSION, TARGET, MAX_BACKEND_BYTES, validateBinding, select, matches, source, hooks, policy, evidence };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { executionLifecycle, httpLifecycle, lifecyclePlanner } = require('@pulse-compute/wasm-contracts/provider/toolchain');
|
|
4
|
+
const { createLifecycleEmitter } = require('@pulse-compute/wasm-runtime-core-as/compiler/canonical-native');
|
|
5
|
+
const forwarding = require('./native-body-forwarding.js');
|
|
6
|
+
const authority = require('./response-authority.js');
|
|
7
|
+
|
|
8
|
+
// Provider-owned site and ABI bindings. No anchors, source scanning or insertion
|
|
9
|
+
// order. The shared planner owns semantic roles; the shared emitter owns AS.
|
|
10
|
+
function create(plan, bindings, services = []) {
|
|
11
|
+
if (!Array.isArray(services)) throw new TypeError('Internal lifecycle services must be a list.');
|
|
12
|
+
const hooks = [...(forwarding.hooks(plan)), ...services];
|
|
13
|
+
const needsAuthority = hooks.some(hook => ['response.error', 'response.authority', 'result.claim'].includes(hook.point));
|
|
14
|
+
if (needsAuthority) hooks.push(...authority.hooks());
|
|
15
|
+
if (plan.packages?.application && hooks.length) throw Object.assign(new Error('Lifecycle contributions require the canonical driver emitter; package-owned emitters are not yet bound.'), { code: 'PULSE_FASTLY_LIFECYCLE_HOOK_INVALID' });
|
|
16
|
+
const sites = [
|
|
17
|
+
{ id: 'request-acquired', points: [{ point: 'request.acquired', parameters: {} }] },
|
|
18
|
+
{ id: 'effect-settle', points: [{ point: 'effect.settle', parameters: { effect: 'settle.index', ticket: 'settle.ticket', result: 'settle.result' }, reject: 'reject-settle' }] },
|
|
19
|
+
{ id: 'effect-deliver', points: [{ point: 'effect.deliver', parameters: { effect: 'settle.index', result: 'settle.result' }, afterParameters: { accepted: 'delivery.accepted' } }] },
|
|
20
|
+
{ id: 'effect-result', points: [{ point: 'effect.result', parameters: { effect: 'setter.index', result: 'setter.result' }, reject: 'reject-result' }] },
|
|
21
|
+
{ id: 'invocation-close', points: [{ point: 'invocation.close', parameters: {} }] },
|
|
22
|
+
{ id: 'invocation-end', points: [{ point: 'invocation.end', parameters: {} }] },
|
|
23
|
+
{ id: 'invocation-closed', points: [{ point: 'invocation.closed', parameters: {} }] },
|
|
24
|
+
{ id: 'response-error', points: [
|
|
25
|
+
{ point: 'response.authority', parameters: {}, reject: 'stop-error' },
|
|
26
|
+
{ point: 'response.error', parameters: {}, onClaim: 'stop-error' }
|
|
27
|
+
] },
|
|
28
|
+
{ id: 'response-send', points: [{ point: 'response.authority', parameters: {}, reject: 'reject-send' }] },
|
|
29
|
+
...(bindings.maxDurationMs === undefined ? [] : [{ id: 'response-timeout', points: [{ point: 'response.authority', parameters: {}, reject: 'stop-error' }] }]),
|
|
30
|
+
{ id: 'result-claim', points: [{ point: 'result.claim', parameters: { result: 'driver.result' }, onClaim: 'stop-error' }] }
|
|
31
|
+
];
|
|
32
|
+
const emitter = createLifecycleEmitter({
|
|
33
|
+
bindings: { version: lifecyclePlanner.BINDINGS_VERSION, executionVersion: executionLifecycle.VERSION, httpVersion: httpLifecycle.VERSION, sites },
|
|
34
|
+
contributions: hooks.map(({ point, owner, order }) => ({ point, owner, order })),
|
|
35
|
+
implementations: hooks.map(({ order, ...implementation }) => implementation),
|
|
36
|
+
values: {
|
|
37
|
+
'settle.index': { name: 'index', type: 'i32' }, 'settle.ticket': { name: 'ticket', type: 'i32' }, 'settle.result': { name: 'result', type: 'i32' },
|
|
38
|
+
'setter.index': { name: 'effectIndex', type: 'i32' }, 'setter.result': { name: 'handle', type: 'i32' },
|
|
39
|
+
'delivery.accepted': { name: '__pulse_delivery_accepted', type: 'i32' }, 'driver.result': { name: 'result', type: 'i32' }
|
|
40
|
+
},
|
|
41
|
+
actions: { 'reject-settle': 'return 0;', 'reject-result': 'return 0;', 'stop-error': 'return;', 'reject-send': '{ __pulse_fastly_fail(PULSE_ERROR_STATE, 282, -1); return PULSE_ERROR_STATE; }' }
|
|
42
|
+
});
|
|
43
|
+
return Object.freeze({ ...emitter, authority: needsAuthority,
|
|
44
|
+
// Legacy error/body/deadline owners instrument only the inner implementation.
|
|
45
|
+
// Structural wrappers keep shared authority outside them, regardless of pass order.
|
|
46
|
+
legacyName: name => emitter.active ? name + '_impl' : name,
|
|
47
|
+
supportSource: [emitter.source, ...(needsAuthority ? [authority.runtimeSource()] : [])].filter(Boolean).join('\n')
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
module.exports = Object.freeze({ create });
|
|
@@ -29,7 +29,6 @@ let __fwd_index: i32 = -1
|
|
|
29
29
|
let __fwd_result: i32 = 0
|
|
30
30
|
let __fwd_marker: i32 = 0
|
|
31
31
|
let __fwd_failure: i32 = 0
|
|
32
|
-
let __fwd_committed: bool = false
|
|
33
32
|
let __fwd_reserved: bool = false
|
|
34
33
|
let __fwd_header_count: i32 = 0
|
|
35
34
|
let __fwd_request_length: i32 = -1
|
|
@@ -42,13 +41,14 @@ function host_request_body(): i32 {
|
|
|
42
41
|
}
|
|
43
42
|
function __fwd_fail(status: i32): void {
|
|
44
43
|
if (__fwd_failure == 0) __fwd_failure = status;
|
|
44
|
+
if (__fwd_ra !== null) __fwd_ra!.fail(1,u32(status));
|
|
45
45
|
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 280, __fwd_index);
|
|
46
46
|
}
|
|
47
47
|
function __fwd_remaining(): i32 {
|
|
48
48
|
if (__fwd_failure != 0) return 0;
|
|
49
|
-
if (wasi_snapshot_preview1_clock_time_get(1, 1, __fwd_io) != 0) { __fwd_fail(504); return 0; }
|
|
49
|
+
if (wasi_snapshot_preview1_clock_time_get(1, 1, __fwd_io) != 0) { __fra_clock_failed = true; __fwd_fail(504); return 0; }
|
|
50
50
|
const now = load<i64>(__fwd_io);
|
|
51
|
-
if (now < 0 || now < __request_last_clock || now < __pulse_fetch_last_clock) { __fwd_fail(504); return 0; }
|
|
51
|
+
if (now < 0 || now < __request_last_clock || now < __pulse_fetch_last_clock) { __fra_clock_failed = true; __fwd_fail(504); return 0; }
|
|
52
52
|
__request_last_clock = now; __pulse_fetch_last_clock = now;
|
|
53
53
|
let remaining = f64(__request_deadline - now) / 1000000.0;
|
|
54
54
|
if (__request_expired || remaining <= 0) { __request_expired = true; __fwd_fail(504); return 0; }
|
|
@@ -57,35 +57,20 @@ function __fwd_remaining(): i32 {
|
|
|
57
57
|
return i32(Math.ceil(remaining)); // 1..30000; zero never reaches async.select
|
|
58
58
|
}
|
|
59
59
|
function __fwd_cleanup(): void {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (__fwd_response >= 0) { __pulse_fetch_close_response(__fwd_response); __fwd_response = -1; }
|
|
65
|
-
if (__fwd_output >= 0) { __pulse_fetch_close_response(__fwd_output); __fwd_output = -1; }
|
|
66
|
-
if (__fwd_downstream >= 0) { __fwd_abandon(__fwd_downstream); __fwd_downstream = -1; }
|
|
67
|
-
if (__fwd_pending >= 0) {
|
|
68
|
-
// No pending-request cancel ABI. Poll once without waiting and discard any
|
|
69
|
-
// late handles; an unresolved host operation ends at invocation teardown.
|
|
70
|
-
__pulse_send_error_detail_reset(__fwd_detail);
|
|
71
|
-
store<i32>(__fwd_done, 0);
|
|
72
|
-
const status = __pulse_fetch_poll(__fwd_pending, __fwd_detail, __fwd_done, __fwd_response_out, __fwd_body_out);
|
|
73
|
-
if (status == 0 && load<i32>(__fwd_done) == 1) {
|
|
74
|
-
__pulse_fetch_close_response(load<i32>(__fwd_response_out)); __pulse_fetch_close_body(load<i32>(__fwd_body_out));
|
|
75
|
-
}
|
|
76
|
-
__fwd_pending = -1;
|
|
77
|
-
}
|
|
60
|
+
__fra_cleanup();
|
|
61
|
+
__fwd_source = -1; __fwd_upload = -1; __fwd_request = -1;
|
|
62
|
+
__fwd_body = -1; __fwd_response = -1; __fwd_output = -1;
|
|
63
|
+
__fwd_downstream = -1; __fwd_pending = -1;
|
|
78
64
|
}
|
|
79
65
|
function __fwd_error_response(): bool {
|
|
80
|
-
if (
|
|
66
|
+
if (__fwd_ra === null) return false;
|
|
67
|
+
if (__fwd_failure == 0 && __pulse_fastly_last_error != 0) __fwd_fail(502);
|
|
68
|
+
if (!__pulse_response_owned()) { __fwd_cleanup(); return true; }
|
|
81
69
|
if (__fwd_failure == 0) return false;
|
|
82
70
|
const status = __fwd_failure;
|
|
83
|
-
__fwd_cleanup();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
value.text = status == 413 ? 'Payload Too Large' : status == 504 ? 'Gateway Timeout' : status == 400 ? 'Bad Request' : 'Bad Gateway';
|
|
87
|
-
__fwd_committed = true;
|
|
88
|
-
__pulse_fastly_send_result(__pulse_fastly_put(value)); return true;
|
|
71
|
+
__fwd_cleanup();
|
|
72
|
+
__fra_error(status);
|
|
73
|
+
return true;
|
|
89
74
|
}
|
|
90
75
|
function __fwd_lower(byte: u8): u8 { return byte >= 65 && byte <= 90 ? byte + 32 : byte; }
|
|
91
76
|
function __fwd_equal(ptr: usize, size: i32, text: string): bool {
|
|
@@ -136,9 +121,9 @@ function __fwd_begin(index: i32, payload: __PulseFastlyValue): void {
|
|
|
136
121
|
if (__pulse_fastly_last_error != 0 || !__fwd_remaining() || !__fwd_declared_request()) return;
|
|
137
122
|
const url = __pulse_fastly_utf8(__pulse_fastly_string(__pulse_fastly_payload_field(payload, 'url')));
|
|
138
123
|
const backend = __pulse_fastly_utf8(__pulse_fastly_backend(index));
|
|
139
|
-
if (
|
|
124
|
+
if (__fra_new(2,__fwd_io + 52) != 0) { __fwd_fail(502); return; }
|
|
140
125
|
__fwd_request = load<i32>(__fwd_io + 52);
|
|
141
|
-
if (
|
|
126
|
+
if (__fra_new(3,__fwd_io + 52) != 0) { __fwd_fail(502); return; }
|
|
142
127
|
__fwd_upload = load<i32>(__fwd_io + 52);
|
|
143
128
|
const method = memory.data<u8>([80,79,83,84]);
|
|
144
129
|
if (fastly_http_req_method_set(__fwd_request, method, 4) != 0 || fastly_http_req_uri_set(__fwd_request, changetype<usize>(url), url.byteLength) != 0) { __fwd_fail(502); return; }
|
|
@@ -152,9 +137,8 @@ function __fwd_begin(index: i32, payload: __PulseFastlyValue): void {
|
|
|
152
137
|
}
|
|
153
138
|
if (!__fwd_remaining()) return;
|
|
154
139
|
__pulse_transport_slots[index] = null;
|
|
155
|
-
const status =
|
|
156
|
-
// send
|
|
157
|
-
// close on that error path before any handle allocation/reuse can occur.
|
|
140
|
+
const status = __fra_dispatch(__fwd_request, __fwd_upload, changetype<usize>(backend), backend.byteLength, __fwd_io + 52);
|
|
141
|
+
// An ambiguous send retains its fenced request claim; cleanup never retries it.
|
|
158
142
|
if (status != 0) { __pulse_transport_capture(__fwd_index, status, 'send'); __fwd_fail(502); return; }
|
|
159
143
|
__fwd_request = -1; __fwd_pending = load<i32>(__fwd_io + 52);
|
|
160
144
|
__pulse_fastly_pending[index] = __fwd_pending; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_ASYNC;
|
|
@@ -165,7 +149,7 @@ function __fwd_poll(): bool {
|
|
|
165
149
|
if (__fwd_pending < 0 || !__fwd_remaining()) return __fwd_response >= 0;
|
|
166
150
|
__pulse_send_error_detail_reset(__fwd_detail);
|
|
167
151
|
store<i32>(__fwd_done, 0);
|
|
168
|
-
const status =
|
|
152
|
+
const status = __fra_poll();
|
|
169
153
|
const done = load<i32>(__fwd_done);
|
|
170
154
|
if (status != 0) { __pulse_transport_capture(__fwd_index, status, 'poll', __fwd_detail); __fwd_pending = -1; __fwd_fail(502); return false; }
|
|
171
155
|
if (done != 0 && done != 1) { __fwd_fail(502); return false; }
|
|
@@ -229,11 +213,11 @@ function __fwd_copy(source: i32, sink: i32, cap: i32, declared: i32, upload: boo
|
|
|
229
213
|
function __fwd_wait(): i32 {
|
|
230
214
|
if (__fwd_failure != 0 || __fwd_pending < 0) return 0;
|
|
231
215
|
const eof = __fwd_copy(__fwd_source, __fwd_upload, __FWD_REQUEST_MAX, __fwd_request_length, true);
|
|
232
|
-
|
|
216
|
+
__fra_dispose(1); __fwd_source = -1;
|
|
233
217
|
if (eof && __fwd_remaining()) {
|
|
234
218
|
const upload = __fwd_upload; __fwd_upload = -1;
|
|
235
|
-
if (
|
|
236
|
-
} else if (__fwd_upload >= 0) {
|
|
219
|
+
if (__fra_dispose(3) != 0) __fwd_fail(502);
|
|
220
|
+
} else if (__fwd_upload >= 0) { __fra_dispose(3,true); __fwd_upload = -1; }
|
|
237
221
|
while (__fwd_failure == 0 && __fwd_response < 0) {
|
|
238
222
|
if (__fwd_poll()) break;
|
|
239
223
|
if (__fwd_failure != 0 || !__fwd_wait_io(__fwd_pending, false)) break;
|
|
@@ -310,9 +294,9 @@ function __fwd_nominated(name: usize, length: i32): bool {
|
|
|
310
294
|
}
|
|
311
295
|
function __fwd_send(): i32 {
|
|
312
296
|
if (!__fwd_response_headers()) { __fwd_error_response(); return 0; }
|
|
313
|
-
if (
|
|
297
|
+
if (__fra_new(7,__fwd_io + 52) != 0) { __fwd_fail(502); __fwd_error_response(); return 0; }
|
|
314
298
|
__fwd_output = load<i32>(__fwd_io + 52);
|
|
315
|
-
if (fastly_http_resp_status_set(__fwd_output, __fwd_status) != 0 ||
|
|
299
|
+
if (fastly_http_resp_status_set(__fwd_output, __fwd_status) != 0 || __fra_new(8,__fwd_io + 52) != 0) { __fwd_fail(502); __fwd_error_response(); return 0; }
|
|
316
300
|
__fwd_downstream = load<i32>(__fwd_io + 52);
|
|
317
301
|
for (let i = 0; i < __fwd_header_count; i++) {
|
|
318
302
|
const pair = __fwd_pairs + i * 16, name = load<usize>(pair), length = load<i32>(pair + 4);
|
|
@@ -321,19 +305,19 @@ function __fwd_send(): i32 {
|
|
|
321
305
|
if (__fwd_hop(name, length) || nominated) continue;
|
|
322
306
|
if (fastly_http_resp_header_append(__fwd_output, name, length, load<usize>(pair + 8), load<i32>(pair + 12)) != 0) { __fwd_fail(502); break; }
|
|
323
307
|
}
|
|
324
|
-
|
|
308
|
+
__fra_dispose(5); __fwd_response = -1;
|
|
325
309
|
if (!__fwd_remaining()) { __fwd_error_response(); return 0; }
|
|
326
310
|
// Treat even an ambiguous send failure as committed: never emit a second
|
|
327
311
|
// response. The writable body remains ours until explicit EOF or abandon.
|
|
328
|
-
__fwd_committed = true;
|
|
329
312
|
const output = __fwd_output; __fwd_output = -1;
|
|
330
|
-
if (
|
|
313
|
+
if (__fra_send(output, __fwd_downstream, 1) != 0) { __fwd_fail(502); __fwd_cleanup(); return 0; }
|
|
331
314
|
const bodyless = __fwd_status == 204 || __fwd_status == 205 || __fwd_status == 304;
|
|
332
315
|
const eof = __fwd_copy(__fwd_body, __fwd_downstream, bodyless ? 0 : __FWD_RESPONSE_MAX, bodyless ? 0 : __fwd_response_length, false);
|
|
333
|
-
|
|
316
|
+
__fra_dispose(6); __fwd_body = -1;
|
|
334
317
|
if (eof && __fwd_remaining()) {
|
|
335
318
|
const downstream = __fwd_downstream; __fwd_downstream = -1;
|
|
336
|
-
if (
|
|
319
|
+
if (__fra_dispose(8) != 0) __fwd_fail(502);
|
|
320
|
+
else __pulse_response_complete();
|
|
337
321
|
// Success is the host-handoff boundary. The handle is gone; there is no
|
|
338
322
|
// final-writer wait, delivery receipt, or post-handoff timeout guarantee.
|
|
339
323
|
}
|
|
@@ -14,12 +14,13 @@ function validate(plan, options) {
|
|
|
14
14
|
const init = effect?.inputs?.find(input => input.name === 'init')?.value;
|
|
15
15
|
const body = field(init, 'body'), method = field(init, 'method'), headers = field(init, 'headers');
|
|
16
16
|
if (effects.length !== 1 || effect.kind !== 'fetch' || effect.grouped
|
|
17
|
+
|| effect.fetchErrors !== undefined || effect.fetchTiming !== undefined
|
|
17
18
|
|| effect.result?.mode !== 'return' || effect.result?.valueKind !== 'opaque-response'
|
|
18
19
|
|| effect.resource?.kind !== 'literal' || options.bindings?.dynamicBackends
|
|
19
20
|
|| body?.name !== 'request.body.forward-marker' || method?.kind !== 'literal' || method.value !== 'POST'
|
|
20
21
|
|| init.entries.some(entry => !['method', 'body', 'headers', 'timeoutMs'].includes(entry.key?.value))
|
|
21
22
|
|| (headers && (headers.kind !== 'object' || headers.entries.some(entry => entry.key?.kind !== 'literal' || entry.value?.kind !== 'literal' || typeof entry.value.value !== 'string')))) {
|
|
22
|
-
throw forwardingError('Fastly Native forwarding admits one terminal opaque POST fetch with inline req.body(), a static backend, and optional literal headers/timeoutMs; groups, decoding, reuse, and dynamic backends are unsupported.');
|
|
23
|
+
throw forwardingError('Fastly Native forwarding admits one terminal opaque POST fetch with inline req.body(), a static backend, and optional literal headers/timeoutMs; outcome/timing options, groups, decoding, reuse, and dynamic backends are unsupported.');
|
|
23
24
|
}
|
|
24
25
|
const entries = headers?.entries || [];
|
|
25
26
|
const hop = /^(connection|keep-alive|proxy-connection|transfer-encoding|te|trailer|upgrade|content-length|host|proxy-authenticate|proxy-authorization)$/i;
|
|
@@ -49,15 +50,6 @@ function instrument(input, plan, bindings) {
|
|
|
49
50
|
if (!line || !line.endsWith('}')) throw new Error('Fastly forwarding function boundary drift: ' + name);
|
|
50
51
|
replace(line, `function ${name}(${args}): ${result} { ${body} }`);
|
|
51
52
|
}
|
|
52
|
-
replace('function __pulse_fastly_send_result(handle: i32): i32 {',
|
|
53
|
-
'function __pulse_fastly_send_result(handle: i32): i32 { if (handle == __fwd_result && handle != 0) return __fwd_send();');
|
|
54
|
-
replace('function __pulse_fastly_jwt_send_error(): void {',
|
|
55
|
-
'function __pulse_fastly_jwt_send_error(): void { if (__fwd_error_response()) return;');
|
|
56
|
-
replace('function __request_send_timeout(): void {',
|
|
57
|
-
'function __request_send_timeout(): void { if (__fwd_committed) { __fwd_cleanup(); return; }');
|
|
58
|
-
replace(' __pulse_fastly_run_invocation()\n', ' __pulse_fastly_run_invocation()\n __fwd_cleanup()\n');
|
|
59
|
-
replace(' __pulse_fastly_request_body_handle = load<i32>(changetype<usize>(handles) + 4)',
|
|
60
|
-
' __pulse_fastly_request_body_handle = load<i32>(changetype<usize>(handles) + 4)\n __fwd_source = __pulse_fastly_request_body_handle');
|
|
61
53
|
// Generic request-budget code must not wait for headers before driving upload.
|
|
62
54
|
const begin = source.indexOf('function __pulse_fastly_resolve_effect(');
|
|
63
55
|
const end = source.indexOf('\n}', begin);
|
|
@@ -66,4 +58,16 @@ function instrument(input, plan, bindings) {
|
|
|
66
58
|
return source + '\n' + `const __FWD_REQUEST_MAX: i32 = ${bindings.bodyForwarding.requestMaxBytes}\nconst __FWD_RESPONSE_MAX: i32 = ${bindings.bodyForwarding.responseMaxBytes}\n`
|
|
67
59
|
+ fs.readFileSync(path.join(__dirname, 'native-body-forwarding.as.ts'), 'utf8');
|
|
68
60
|
}
|
|
69
|
-
|
|
61
|
+
function hooks(plan) {
|
|
62
|
+
if (!enabled(plan)) return [];
|
|
63
|
+
const owner = 'body-forwarding', order = 10;
|
|
64
|
+
return [
|
|
65
|
+
{ point: 'request.acquired', owner, order, body: ' __fwd_source = __pulse_fastly_request_body_handle' },
|
|
66
|
+
{ point: 'response.error', owner, order, body: ' return __fwd_error_response()' },
|
|
67
|
+
{ point: 'result.claim', owner, order, body: ` if (result != __fwd_result || result == 0) return false
|
|
68
|
+
if (__pulse_response_owned()) __fwd_send()
|
|
69
|
+
return true` },
|
|
70
|
+
{ point: 'invocation.end', owner, order, body: ' __fwd_cleanup(); __fra_cleanup(true)' }
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
module.exports = {IMPORTS, enabled, validate, instrument, hooks};
|