@pulse-compute/provider-fastly 1.0.0-beta.1 → 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.
Files changed (83) hide show
  1. package/README.md +7 -6
  2. package/dist/config-schema.json +30 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -10
  8. package/src/build/canonical-target.js +41 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +51 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +6 -11
  13. package/src/build/host-extensions.js +254 -0
  14. package/src/build/kv-native.as.ts +331 -0
  15. package/src/build/kv-native.js +27 -0
  16. package/src/build/lifecycle.js +50 -0
  17. package/src/build/native-application-errors.js +159 -0
  18. package/src/build/native-binary-resources.js +30 -0
  19. package/src/build/native-body-forwarding.as.ts +325 -0
  20. package/src/build/native-body-forwarding.js +73 -0
  21. package/src/build/native-dynamic-backends.as.ts +115 -0
  22. package/src/build/native-dynamic-backends.js +52 -0
  23. package/src/build/native-fetch-accounting.as.ts +133 -0
  24. package/src/build/native-fetch-accounting.js +84 -0
  25. package/src/build/native-fetch-bounds.js +47 -0
  26. package/src/build/native-fetch-cohorts.as.ts +126 -0
  27. package/src/build/native-fetch-cohorts.js +65 -0
  28. package/src/build/native-fetch-deadlines.as.ts +169 -0
  29. package/src/build/native-fetch-deadlines.js +36 -0
  30. package/src/build/native-fetch-errors.js +38 -0
  31. package/src/build/native-fetch-headers.js +52 -0
  32. package/src/build/native-fetch-outcomes.js +87 -0
  33. package/src/build/native-fetch-timing.as.ts +74 -0
  34. package/src/build/native-fetch-timing.js +50 -0
  35. package/src/build/native-forwarding-accounting.as.ts +153 -0
  36. package/src/build/native-forwarding-accounting.js +47 -0
  37. package/src/build/native-handle-ownership.js +127 -0
  38. package/src/build/native-header-lookup.js +50 -0
  39. package/src/build/native-http-effects.js +164 -35
  40. package/src/build/native-platform-capabilities.js +815 -220
  41. package/src/build/native-request-body.js +45 -0
  42. package/src/build/native-request-headers.js +93 -0
  43. package/src/build/native-send-error-detail.as.ts +12 -0
  44. package/src/build/native-stdio.js +45 -0
  45. package/src/build/native-string-values.js +143 -0
  46. package/src/build/native-transport-details.as.ts +84 -0
  47. package/src/build/native-value-budget.js +90 -0
  48. package/src/build/package-native-application.js +114 -0
  49. package/src/build/request-budget-source.js +58 -0
  50. package/src/build/request-budget.js +46 -0
  51. package/src/build/response-authority.js +56 -0
  52. package/src/build/s3-body.as.ts +97 -0
  53. package/src/build/s3-native.as.ts +228 -0
  54. package/src/build/s3-native.js +30 -0
  55. package/src/build/schema-admission.js +83 -0
  56. package/src/build/schema-nested-json.js +93 -0
  57. package/src/build/schema-scalar-record.js +41 -0
  58. package/src/build/time-native.js +49 -0
  59. package/src/build/wasm-budget.js +28 -0
  60. package/src/config-api.js +6 -0
  61. package/src/config-schema.json +30 -0
  62. package/src/index.ts +25 -0
  63. package/src/javascript/fetch-adapter.js +3 -0
  64. package/src/javascript/jwt-verifier.js +14 -4
  65. package/src/javascript/logging.js +11 -2
  66. package/src/javascript/package-effects.js +5 -2
  67. package/src/javascript/runtime-host.js +8 -3
  68. package/src/javascript/source-package.js +4 -2
  69. package/src/javascript/target-support-policy.js +20 -3
  70. package/src/javascript/target.js +17 -1
  71. package/src/javascript/test-runtime.js +1 -0
  72. package/src/provider-contract.js +23 -6
  73. package/src/runtime/canonical-api-runtime.js +6 -0
  74. package/src/testing/conditional-kv-host.js +155 -0
  75. package/src/testing/native-dynamic-backends-host.js +39 -0
  76. package/src/testing/native-http-effects-host.js +18 -1
  77. package/src/testing/native-platform-capabilities-host.js +359 -24
  78. package/src/toolchain/body-forwarding.js +25 -0
  79. package/src/toolchain/config.js +16 -0
  80. package/src/toolchain/index.js +49 -5
  81. package/src/toolchain/logging.js +18 -0
  82. package/src/toolchain/native-execution.js +83 -0
  83. package/src/toolchain/s3.js +26 -0
@@ -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,331 @@
1
+ // Fastly Native realization of the runtime-owned K1/K2 conditional KV contract.
2
+ // Limits are generated from runtime/host. No SDK or unconditional CAS fallback.
3
+ @external('fastly_async_io', 'select') declare function __kv_select(handles: usize, count: i32, timeout: i32, ready: usize): i32
4
+ @external('fastly_http_body', 'close') declare function __kv_close(body: i32): i32
5
+
6
+ @lazy const __kv_deadline = new StaticArray<i64>(PULSE_FASTLY_EFFECT_COUNT)
7
+ @lazy const __kv_dispatched = new StaticArray<bool>(PULSE_FASTLY_EFFECT_COUNT)
8
+ @lazy const __kv_list_limits = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
9
+ @lazy const __kv_list_prefixes = new Array<string>(PULSE_FASTLY_EFFECT_COUNT).fill('')
10
+ let __kv_request_deadline: i64 = -1;
11
+ // Host embedding seam, in monotonic nanoseconds; never exposed through ctx.
12
+ export function pulse_fastly_kv_request_deadline(deadline: i64): void { __kv_request_deadline = deadline; }
13
+ function __kv_clock(): i64 {
14
+ const out = new StaticArray<i64>(1);
15
+ return wasi_snapshot_preview1_clock_time_get(1, 1, changetype<usize>(out)) == 0 ? out[0] : -1;
16
+ }
17
+ function __kv_remaining(index: i32): i32 {
18
+ const now = __kv_clock();
19
+ if (now < 0) return -1;
20
+ const remaining = __kv_deadline[index] - now;
21
+ return remaining <= 0 ? 0 : i32((remaining + 999999) / 1000000);
22
+ }
23
+ function __kv_set(result: i32, key: string, value: i32): void { host_value_object_set(result, __pulse_fastly_string_value(key), value); }
24
+ function __kv_result(status: string, reason: string = ''): i32 {
25
+ const result = host_value_object(); __kv_set(result, 'status', __pulse_fastly_string_value(status));
26
+ if (reason.length) __kv_set(result, 'reason', __pulse_fastly_string_value(reason));
27
+ return result;
28
+ }
29
+ function __kv_failure(index: i32, reason: string): i32 {
30
+ const kind = __pulse_fastly_effect_kind(index);
31
+ return __kv_result(kind == 15 || kind == 23 ? 'failed' : __kv_dispatched[index] ? 'unknown' : kind == 22 ? 'not-deleted' : 'not-stored', reason);
32
+ }
33
+ function __kv_fail(index: i32, reason: string): void { __pulse_fastly_ready_effect(index, __kv_failure(index, reason)); }
34
+ function __kv_expired(index: i32): bool {
35
+ const remaining = __kv_remaining(index);
36
+ if (remaining > 0) return false;
37
+ __kv_fail(index, remaining == 0 ? 'timeout' : 'unavailable'); return true;
38
+ }
39
+ // select readiness guarantees the corresponding I/O action will not block.
40
+ // Zero timeout means infinity in this ABI, so it must never be passed.
41
+ function __kv_ready(index: i32, handle: i32): i32 {
42
+ const remaining = __kv_remaining(index); if (remaining <= 0) return remaining;
43
+ const handles = new StaticArray<i32>(1), out = new StaticArray<i32>(1);
44
+ handles[0] = handle; out[0] = -1;
45
+ if (__kv_select(changetype<usize>(handles), 1, remaining, changetype<usize>(out)) != 0) return -1;
46
+ const after = __kv_remaining(index); if (after <= 0) return after;
47
+ return out[0] == 0 ? 1 : out[0] == -1 ? 0 : -2;
48
+ }
49
+ function __kv_ready_failure(index: i32, ready: i32): i32 { return __kv_failure(index, ready == 0 ? 'timeout' : ready == -2 ? 'protocol' : 'transport'); }
50
+ function __kv_key(text: string): bool {
51
+ if (text == ' ' || text == '\ufffe' || text == '\uffff') return false;
52
+ if (!text.length || text.length > __KV_keyBytes || String.UTF8.byteLength(text) > __KV_keyBytes) return false;
53
+ for (let i = 0; i < text.length; i++) {
54
+ const c = text.charCodeAt(i);
55
+ if (c < 32 || (c >= 127 && c <= 159)) return false;
56
+ if (c >= 0xd800 && c <= 0xdbff) { if (++i >= text.length) return false; const next = text.charCodeAt(i); if (next < 0xdc00 || next > 0xdfff) return false; }
57
+ else if (c >= 0xdc00 && c <= 0xdfff) return false;
58
+ }
59
+ // Fastly's exact-key restrictions; never normalize an application key.
60
+ return text != '.' && text != '..' && !text.startsWith('.well-known/acme-challenge')
61
+ && text.indexOf('./') < 0 && !text.endsWith('/.') && !text.endsWith('/..')
62
+ && text.indexOf('#') < 0 && text.indexOf('?') < 0 && text.indexOf('^') < 0
63
+ && text.indexOf('|') < 0 && text.indexOf(';') < 0 && text.indexOf('\n') < 0 && text.indexOf('\r') < 0;
64
+ }
65
+ function __kv_list_text(text: string, max: i32, empty: bool = false): bool {
66
+ if ((!empty && !text.length) || text.length > max || String.UTF8.byteLength(text) > max) return false;
67
+ for (let i = 0; i < text.length; i++) {
68
+ const c = text.charCodeAt(i);
69
+ if (c < 32 || (c >= 127 && c <= 159)) return false;
70
+ if (c >= 0xd800 && c <= 0xdbff) { if (++i >= text.length) return false; const next = text.charCodeAt(i); if (next < 0xdc00 || next > 0xdfff) return false; }
71
+ else if (c >= 0xdc00 && c <= 0xdfff) return false;
72
+ }
73
+ return true;
74
+ }
75
+ // ABI config: mode:u32, cursor:ptr/len, limit:u32, prefix:ptr/len.
76
+ // Strong is the explicit provider mode; it does not establish a page snapshot.
77
+ function __kv_list_config(index: i32, payload: __PulseFastlyValue, config: StaticArray<i32>): i32 {
78
+ const options = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'options'));
79
+ if (options.kind != PULSE_VALUE_OBJECT) return -1;
80
+ for (let i = 0; i < options.keys.length; i++) if (options.keys[i] != 'limit' && options.keys[i] != 'prefix' && options.keys[i] != 'cursor') return -1;
81
+ const limit = __pulse_fastly_value(__pulse_fastly_payload_field(options, 'limit'));
82
+ if (limit.kind != PULSE_VALUE_NUMBER || !isFinite(limit.number) || limit.number < 1 || limit.number > __KV_listLimit || Math.floor(limit.number) != limit.number) return -1;
83
+ config[3] = i32(limit.number); __kv_list_limits[index] = config[3]; __kv_list_prefixes[index] = '';
84
+ let mask = 4;
85
+ const prefix = __pulse_fastly_value(__pulse_fastly_payload_field(options, 'prefix'));
86
+ if (prefix.kind != PULSE_VALUE_UNDEFINED) {
87
+ if (prefix.kind != PULSE_VALUE_STRING || !__kv_list_text(prefix.text, __KV_keyBytes, true)) return -1;
88
+ __kv_private(prefix.text); __kv_list_prefixes[index] = prefix.text;
89
+ if (prefix.text.length) mask |= 8;
90
+ }
91
+ const cursor = __pulse_fastly_value(__pulse_fastly_payload_field(options, 'cursor'));
92
+ if (cursor.kind != PULSE_VALUE_UNDEFINED) {
93
+ if (cursor.kind != PULSE_VALUE_STRING || !__kv_list_text(cursor.text, __KV_cursorBytes)) return -2;
94
+ __kv_private(cursor.text);
95
+ mask |= 2;
96
+ }
97
+ return mask;
98
+ }
99
+ function __kv_token_valid(token: string): bool {
100
+ if (token.length != 29 || !token.startsWith('fastly-kv-v1:')) return false;
101
+ for (let i = 13; i < 29; i++) { const c = token.charCodeAt(i); if (!(c >= 48 && c <= 57) && !(c >= 97 && c <= 102)) return false; }
102
+ return true;
103
+ }
104
+ function __kv_generation(token: string): u64 {
105
+ let value: u64 = 0;
106
+ for (let i = 13; i < 29; i++) { const c = token.charCodeAt(i); value = (value << 4) | u64(c <= 57 ? c - 48 : c - 87); }
107
+ return value;
108
+ }
109
+ function __kv_token(value: u64): string {
110
+ let text = 'fastly-kv-v1:';
111
+ for (let i = 15; i >= 0; i--) text += '0123456789abcdef'.charAt(i32((value >> (i * 4)) & 15));
112
+ return text;
113
+ }
114
+ function __kv_number(value: f64): string {
115
+ const text = __pulse_fastly_number_string(value);
116
+ // AS prints a trailing .0 for large integral f64s; JSON.stringify does not.
117
+ // Count the portable serialized value budget, including this boundary case.
118
+ return text.endsWith('.0') ? text.substring(0, text.length - 2) : text;
119
+ }
120
+ // Object field names are complete quoted tokens, not substring secrets: a field
121
+ // named "id" must not corrupt "considered", "invalidAcks", or numeric metrics.
122
+ // Values, lookup keys and generations retain raw and JSON-escaped protection.
123
+ function __kv_private(text: string, field: bool = false): string {
124
+ const encoded = __pulse_fastly_quote(text);
125
+ if (field) __pulse_fastly_remember_secret(encoded, true);
126
+ else {
127
+ __pulse_fastly_remember_secret(text);
128
+ __pulse_fastly_remember_secret(encoded.substring(1, encoded.length - 1));
129
+ }
130
+ return encoded;
131
+ }
132
+ class __KvEncoder {
133
+ reason: string = ''; entries: i32 = 0; bytes: i32 = 0;
134
+ seen: Set<i32> = new Set<i32>(); parts: Array<string> | null;
135
+ // Reads need the same validation and redaction walk, but no serialized output.
136
+ constructor(materialize: bool = true) { this.parts = materialize ? new Array<string>() : null; }
137
+ add(text: string): void { this.bytes += String.UTF8.byteLength(text); if (this.bytes > __KV_valueBytes) this.reason = 'too-large'; else if (this.parts !== null) this.parts!.push(text); }
138
+ visit(handle: i32, depth: i32 = 0): void {
139
+ if (this.reason.length) return;
140
+ if (++this.entries > __KV_entries || depth > __KV_depth) { this.reason = 'too-large'; return; }
141
+ const value = __pulse_fastly_value(handle);
142
+ if (value.kind == PULSE_VALUE_NULL) this.add('null');
143
+ else if (value.kind == PULSE_VALUE_BOOLEAN) this.add(value.boolean ? 'true' : 'false');
144
+ else if (value.kind == PULSE_VALUE_STRING) this.add(__kv_private(value.text));
145
+ else if (value.kind == PULSE_VALUE_NUMBER) { if (!isFinite(value.number)) this.reason = 'invalid-value'; else this.add(value.number == 0 ? '0' : __kv_number(value.number)); }
146
+ else if (value.kind == PULSE_VALUE_ARRAY || value.kind == PULSE_VALUE_OBJECT) {
147
+ if (this.seen.has(handle)) { this.reason = 'invalid-value'; return; } this.seen.add(handle);
148
+ const object = value.kind == PULSE_VALUE_OBJECT; this.add(object ? '{' : '[');
149
+ for (let i = 0; i < value.values.length && !this.reason.length; i++) {
150
+ if (i) this.add(',');
151
+ if (object) this.add(__kv_private(value.keys[i], true) + ':');
152
+ this.visit(value.values[i], depth + 1);
153
+ }
154
+ this.add(object ? '}' : ']');
155
+ } else this.reason = 'invalid-value';
156
+ }
157
+ encode(handle: i32): string { this.visit(handle); return this.reason.length ? '' : this.parts!.join(''); }
158
+ }
159
+ function __pulse_fastly_kv_conditional_begin(index: i32, payload: __PulseFastlyValue): void {
160
+ __kv_dispatched[index] = false;
161
+ const start = __kv_clock(); if (start < 0) { __kv_fail(index, 'unavailable'); return; }
162
+ __kv_deadline[index] = start + i64(__KV_timeoutMs) * 1000000;
163
+ if (__kv_request_deadline >= 0 && __kv_request_deadline < __kv_deadline[index]) __kv_deadline[index] = __kv_request_deadline;
164
+ const kind = __pulse_fastly_effect_kind(index), key = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'key'));
165
+ let listConfig: StaticArray<i32> | null = null;
166
+ let listMask = 0;
167
+ if (__KV_HAS_LIST && kind == 23) {
168
+ listConfig = new StaticArray<i32>(6);
169
+ listMask = __kv_list_config(index, payload, listConfig!);
170
+ if (listMask < 0) { __kv_fail(index, listMask == -2 ? 'invalid-cursor' : 'invalid-options'); return; }
171
+ } else {
172
+ if (key.kind != PULSE_VALUE_STRING || !__kv_key(key.text)) { __kv_fail(index, 'invalid-key'); return; }
173
+ __kv_private(key.text);
174
+ }
175
+ let generation: u64 = 0;
176
+ if (kind == 17) {
177
+ const token = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'generation'));
178
+ if (token.kind != PULSE_VALUE_STRING || !__kv_token_valid(token.text)) { __kv_fail(index, 'invalid-generation'); return; }
179
+ __kv_private(token.text); generation = __kv_generation(token.text);
180
+ }
181
+ let wire = '';
182
+ if (__KV_HAS_WRITE && (kind == 16 || kind == 17)) {
183
+ const encoder = new __KvEncoder(); const json = encoder.encode(__pulse_fastly_payload_field(payload, 'value'));
184
+ if (encoder.reason.length) { __kv_fail(index, encoder.reason); return; }
185
+ // Immutable string snapshot exists before any binding/body/dispatch hostcall.
186
+ wire = '{"__pulseKv":1,"value":' + json + '}';
187
+ if (String.UTF8.byteLength(wire) > __KV_wireBytes) { __kv_fail(index, 'too-large'); return; }
188
+ }
189
+ if (__kv_expired(index)) return;
190
+ const name = __pulse_fastly_utf8(__pulse_fastly_kv_store(index)), storeHandle = new StaticArray<i32>(1);
191
+ if (fastly_kv_store_open(changetype<usize>(name), name.byteLength, changetype<usize>(storeHandle)) != 0) { __kv_fail(index, 'configuration'); return; }
192
+ if (__kv_expired(index)) return;
193
+ const keyBytes = __pulse_fastly_utf8(key.text), pending = new StaticArray<i32>(1); let status: i32;
194
+ if (__KV_HAS_LIST && kind == 23) {
195
+ const options = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'options'));
196
+ const prefixBytes = __pulse_fastly_utf8(__kv_list_prefixes[index]);
197
+ const cursor = __pulse_fastly_value(__pulse_fastly_payload_field(options, 'cursor'));
198
+ const cursorBytes = __pulse_fastly_utf8(cursor.kind == PULSE_VALUE_STRING ? cursor.text : '');
199
+ listConfig![1] = i32(changetype<usize>(cursorBytes)); listConfig![2] = cursorBytes.byteLength;
200
+ listConfig![4] = i32(changetype<usize>(prefixBytes)); listConfig![5] = prefixBytes.byteLength;
201
+ status = fastly_kv_store_list(storeHandle[0], listMask, changetype<usize>(listConfig!), changetype<usize>(pending));
202
+ } else if (__KV_HAS_DELETE && kind == 22) {
203
+ const config = new StaticArray<i32>(1);
204
+ __kv_dispatched[index] = true;
205
+ status = fastly_kv_store_delete(storeHandle[0], changetype<usize>(keyBytes), keyBytes.byteLength, 0, changetype<usize>(config), changetype<usize>(pending));
206
+ } else if (__KV_HAS_READ && kind == 15) {
207
+ const config = new StaticArray<i32>(1);
208
+ status = fastly_kv_store_lookup(storeHandle[0], changetype<usize>(keyBytes), keyBytes.byteLength, 0, changetype<usize>(config), changetype<usize>(pending));
209
+ } else if (__KV_HAS_WRITE) {
210
+ const body = new StaticArray<i32>(1), written = new StaticArray<i32>(1);
211
+ if (fastly_http_body_new(changetype<usize>(body)) != 0) { __kv_fail(index, 'transport'); return; }
212
+ if (__kv_expired(index)) { __kv_close(body[0]); return; }
213
+ const bytes = __pulse_fastly_utf8(wire);
214
+ status = fastly_http_body_write(body[0], changetype<usize>(bytes), bytes.byteLength, 0, changetype<usize>(written));
215
+ if (status != 0 || written[0] != bytes.byteLength) { __kv_close(body[0]); __kv_fail(index, status != 0 ? 'transport' : 'protocol'); return; }
216
+ if (__kv_expired(index)) { __kv_close(body[0]); return; }
217
+ const config = new StaticArray<u64>(4); // 32 zeroed bytes, including padding.
218
+ if (kind == 16) store<u32>(changetype<usize>(config), 1);
219
+ else store<u64>(changetype<usize>(config) + 24, generation);
220
+ __kv_dispatched[index] = true; // Entry is the conservative mutation boundary.
221
+ status = fastly_kv_store_insert(storeHandle[0], changetype<usize>(keyBytes), keyBytes.byteLength, body[0], kind == 17 ? 32 : 0, changetype<usize>(config), changetype<usize>(pending));
222
+ // Insert takes ownership on success. On error the body may already be consumed;
223
+ // best-effort close cannot turn uncertain acceptance into rejection.
224
+ if (status != 0) __kv_close(body[0]);
225
+ } else { __kv_fail(index, 'protocol'); return; }
226
+ if (status != 0) { __kv_fail(index, kind == 23 && status == 2 && (listMask & 2) != 0 ? 'invalid-cursor' : 'transport'); return; }
227
+ __pulse_fastly_pending[index] = pending[0]; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_ASYNC;
228
+ }
229
+ function __kv_utf8(data: Uint8Array, length: i32): bool {
230
+ for (let i = 0; i < length;) {
231
+ const c = data[i++]; if (c <= 127) continue;
232
+ const count = c >= 194 && c <= 223 ? 1 : c >= 224 && c <= 239 ? 2 : c >= 240 && c <= 244 ? 3 : -1;
233
+ if (count < 0 || i + count > length) return false;
234
+ const second = data[i];
235
+ if ((c == 224 && second < 160) || (c == 237 && second > 159) || (c == 240 && second < 144) || (c == 244 && second > 143)) return false;
236
+ for (let j = 0; j < count; j++) { const next = data[i++]; if (next < 128 || next > 191) return false; }
237
+ }
238
+ return true;
239
+ }
240
+ // Each read is synchronous through owned-string decoding and parsing. Parallel
241
+ // effects enter their waits serially; no result or pending hostcall owns this
242
+ // scratch buffer. Only the written prefix is inspected, including after errors.
243
+ @lazy let __kv_read_buffer: Uint8Array | null = null;
244
+ function __kv_read(index: i32, body: i32, generation: u64): i32 {
245
+ const isList = __KV_HAS_LIST && __pulse_fastly_effect_kind(index) == 23, max = isList ? __KV_envelopeBytes : __KV_wireBytes;
246
+ if (__kv_read_buffer === null || __kv_read_buffer!.length < max + 1) __kv_read_buffer = new Uint8Array(max + 1);
247
+ const buffer = __kv_read_buffer!, written = new StaticArray<i32>(1); let count = 0;
248
+ while (true) {
249
+ const ready = __kv_ready(index, body); if (ready <= 0) return __kv_ready_failure(index, ready);
250
+ written[0] = 0;
251
+ if (fastly_http_body_read(body, buffer.dataStart + count, max + 1 - count, changetype<usize>(written)) != 0) return __kv_failure(index, 'transport');
252
+ if (written[0] < 0 || written[0] > max + 1 - count) return __kv_failure(index, 'protocol');
253
+ if (!written[0]) break;
254
+ count += written[0]; if (count > max) return __kv_failure(index, 'too-large');
255
+ }
256
+ if (!__kv_utf8(buffer, count)) return __kv_failure(index, 'protocol');
257
+ const parser = new __PulseJsonParser(String.UTF8.decodeUnsafe(buffer.dataStart, count, false), true), parsed = parser.parse();
258
+ if (parser.failed || parsed <= 0) return __kv_failure(index, parser.tooLarge ? 'too-large' : 'protocol');
259
+ const outer = __pulse_fastly_value(parsed);
260
+ if (isList) return __kv_list_result(index, outer);
261
+ if (outer.kind != PULSE_VALUE_OBJECT || outer.keys.length != 2 || __pulse_fastly_find(outer, '__pulseKv') < 0 || __pulse_fastly_find(outer, 'value') < 0) return __kv_failure(index, 'protocol');
262
+ const marker = __pulse_fastly_value(__pulse_fastly_payload_field(outer, '__pulseKv'));
263
+ if (marker.kind != PULSE_VALUE_NUMBER || marker.number != 1) return __kv_failure(index, 'protocol');
264
+ const value = __pulse_fastly_payload_field(outer, 'value'), encoder = new __KvEncoder(false); encoder.visit(value);
265
+ if (encoder.reason.length) return __kv_failure(index, encoder.reason);
266
+ const remaining = __kv_remaining(index); if (remaining <= 0) return __kv_failure(index, remaining == 0 ? 'timeout' : 'unavailable');
267
+ const result = __kv_result('found'), token = __kv_token(generation); __kv_private(token);
268
+ __kv_set(result, 'generation', __pulse_fastly_string_value(token)); __kv_set(result, 'value', value);
269
+ __pulse_fastly_deep_freeze(result); return result;
270
+ }
271
+ function __kv_list_result(index: i32, outer: __PulseFastlyValue): i32 {
272
+ if (outer.kind != PULSE_VALUE_OBJECT || outer.keys.length != 2 || __pulse_fastly_find(outer, 'data') < 0 || __pulse_fastly_find(outer, 'meta') < 0) return __kv_failure(index, 'protocol');
273
+ const data = __pulse_fastly_value(__pulse_fastly_payload_field(outer, 'data')), meta = __pulse_fastly_value(__pulse_fastly_payload_field(outer, 'meta'));
274
+ if (data.kind != PULSE_VALUE_ARRAY || data.values.length > __kv_list_limits[index] || meta.kind != PULSE_VALUE_OBJECT) return __kv_failure(index, 'protocol');
275
+ for (let i = 0; i < meta.keys.length; i++) if (meta.keys[i] != 'limit' && meta.keys[i] != 'prefix' && meta.keys[i] != 'next_cursor' && meta.keys[i] != 'consistency') return __kv_failure(index, 'protocol');
276
+ const limit = __pulse_fastly_value(__pulse_fastly_payload_field(meta, 'limit'));
277
+ if (limit.kind != PULSE_VALUE_NUMBER || limit.number != __kv_list_limits[index]) return __kv_failure(index, 'protocol');
278
+ const prefix = __pulse_fastly_value(__pulse_fastly_payload_field(meta, 'prefix'));
279
+ if (prefix.kind != PULSE_VALUE_UNDEFINED && prefix.kind != PULSE_VALUE_NULL && (prefix.kind != PULSE_VALUE_STRING || prefix.text != __kv_list_prefixes[index])) return __kv_failure(index, 'protocol');
280
+ const consistency = __pulse_fastly_value(__pulse_fastly_payload_field(meta, 'consistency'));
281
+ if (consistency.kind != PULSE_VALUE_UNDEFINED && consistency.kind != PULSE_VALUE_NULL && (consistency.kind != PULSE_VALUE_STRING || consistency.text != 'strong')) return __kv_failure(index, 'protocol');
282
+ const keys = host_value_array(), seen = new Set<string>();
283
+ for (let i = 0; i < data.values.length; i++) {
284
+ const key = __pulse_fastly_value(data.values[i]);
285
+ if (key.kind != PULSE_VALUE_STRING || !__kv_list_text(key.text, __KV_keyBytes) || !key.text.startsWith(__kv_list_prefixes[index]) || seen.has(key.text)) return __kv_failure(index, 'protocol');
286
+ seen.add(key.text); __kv_private(key.text); host_value_array_push(keys, data.values[i]);
287
+ }
288
+ const cursor = __pulse_fastly_value(__pulse_fastly_payload_field(meta, 'next_cursor'));
289
+ if (cursor.kind != PULSE_VALUE_UNDEFINED && cursor.kind != PULSE_VALUE_NULL && (cursor.kind != PULSE_VALUE_STRING || !__kv_list_text(cursor.text, __KV_cursorBytes))) return __kv_failure(index, 'protocol');
290
+ if (cursor.kind == PULSE_VALUE_STRING) __kv_private(cursor.text);
291
+ const remaining = __kv_remaining(index); if (remaining <= 0) return __kv_failure(index, remaining == 0 ? 'timeout' : 'unavailable');
292
+ const result = __kv_result('listed'); __kv_set(result, 'keys', keys);
293
+ __kv_set(result, 'cursor', cursor.kind == PULSE_VALUE_STRING ? __pulse_fastly_string_value(cursor.text) : host_value_null());
294
+ return result;
295
+ }
296
+ function __kv_wait(index: i32): i32 {
297
+ if (__pulse_fastly_pending_mode[index] == PULSE_FASTLY_PENDING_READY) return __pulse_fastly_wait_ready(index);
298
+ const pending = __pulse_fastly_pending[index];
299
+ __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_NONE; __pulse_fastly_pending[index] = 0; __pulse_fastly_payloads[index] = 0;
300
+ // No KV cancel hostcall exists. An expired pending handle is left to invocation
301
+ // teardown; never call an unready wait just to drain it, and never retry.
302
+ const ready = __kv_ready(index, pending); if (ready <= 0) return __kv_ready_failure(index, ready);
303
+ const error = new StaticArray<i32>(1), kind = __pulse_fastly_effect_kind(index);
304
+ if ((__KV_HAS_WRITE || __KV_HAS_DELETE) && kind != 15 && kind != 23) {
305
+ const status = __KV_HAS_DELETE && kind == 22 ? fastly_kv_store_delete_wait(pending, changetype<usize>(error)) : __KV_HAS_WRITE ? fastly_kv_store_insert_wait(pending, changetype<usize>(error)) : 1;
306
+ if (status != 0) return __kv_failure(index, 'transport');
307
+ const remaining = __kv_remaining(index); if (remaining <= 0) return __kv_failure(index, remaining == 0 ? 'timeout' : 'unavailable');
308
+ if (kind == 22 && (error[0] == 1 || error[0] == 3)) return __kv_result(error[0] == 1 ? 'deleted' : 'not-found');
309
+ if (error[0] == 1) return __kv_result('stored');
310
+ if (kind != 22 && (error[0] == 4 || (error[0] == 3 && kind == 17))) return __kv_result('conflict');
311
+ if (error[0] == 2 || error[0] == 5 || error[0] == 7) return __kv_result(kind == 22 ? 'not-deleted' : 'not-stored', error[0] == 2 ? 'rejected' : error[0] == 5 ? 'too-large' : 'throttled');
312
+ return __kv_failure(index, error[0] == 6 ? 'unavailable' : 'protocol');
313
+ }
314
+ const body = new StaticArray<i32>(1), metadata = new Uint8Array(2000), written = new StaticArray<i32>(1), generation = new StaticArray<u64>(1);
315
+ const status = __KV_HAS_LIST && kind == 23 ? fastly_kv_store_list_wait(pending, changetype<usize>(body), changetype<usize>(error)) : __KV_HAS_READ ? fastly_kv_store_lookup_wait_v2(pending, changetype<usize>(body), metadata.dataStart, metadata.length, changetype<usize>(written), changetype<usize>(generation), changetype<usize>(error)) : 1;
316
+ if (status != 0) return __kv_failure(index, 'transport');
317
+ if (error[0] != 1) {
318
+ const remaining = __kv_remaining(index); if (remaining <= 0) return __kv_failure(index, remaining == 0 ? 'timeout' : 'unavailable');
319
+ if (error[0] == 3 && kind == 15) return __kv_result('not-found');
320
+ if (error[0] == 2 && kind == 23) return __kv_failure(index, 'invalid-cursor');
321
+ return __kv_failure(index, error[0] == 5 ? 'too-large' : error[0] == 6 ? 'unavailable' : error[0] == 7 ? 'throttled' : 'protocol');
322
+ }
323
+ const result = written[0] < 0 || written[0] > metadata.length ? __kv_failure(index, 'protocol') : __kv_read(index, body[0], generation[0]);
324
+ __kv_close(body[0]); return result;
325
+ }
326
+
327
+ function __pulse_fastly_kv_conditional_wait(index: i32): i32 {
328
+ const result = __kv_wait(index);
329
+ if (__KV_HAS_LIST) { __kv_list_prefixes[index] = ''; __kv_list_limits[index] = 0; }
330
+ __pulse_fastly_deep_freeze(result); return result;
331
+ }
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { KV_CONDITIONAL_KINDS, KV_CONDITIONAL_LIMITS } = require('@pulse-compute/runtime/host');
5
+
6
+ const KV_IMPORTS = Object.freeze([
7
+ 'fastly_async_io:select', 'fastly_http_body:close', 'wasi_snapshot_preview1:clock_time_get',
8
+ 'fastly_kv_store:open'
9
+ ]);
10
+ function kvImports(kind) {
11
+ if (kind === 'kv.list') return [...KV_IMPORTS, 'fastly_kv_store:list', 'fastly_kv_store:list_wait', 'fastly_http_body:read'];
12
+ if (kind === 'kv.delete') return [...KV_IMPORTS.filter(key => key !== 'fastly_http_body:close'), 'fastly_kv_store:delete', 'fastly_kv_store:delete_wait'];
13
+ return [...KV_IMPORTS, ...(kind === 'kv.getVersioned'
14
+ ? ['fastly_kv_store:lookup', 'fastly_kv_store:lookup_wait_v2', 'fastly_http_body:read']
15
+ : ['fastly_kv_store:insert', 'fastly_kv_store:insert_wait', 'fastly_http_body:new', 'fastly_http_body:write'])];
16
+ }
17
+ function kvNativeSource(plan) {
18
+ if (!plan.effects.some(effect => KV_CONDITIONAL_KINDS.includes(effect.kind))) return '';
19
+ const limits = Object.entries(KV_CONDITIONAL_LIMITS).map(([key, value]) => `const __KV_${key}: i32 = ${value};`).join('\n');
20
+ const kinds = new Set(plan.effects.map(effect => effect.kind));
21
+ const selection = `const __KV_HAS_READ: bool = ${kinds.has('kv.getVersioned')};
22
+ const __KV_HAS_WRITE: bool = ${kinds.has('kv.insertIfAbsent') || kinds.has('kv.compareAndSwap')};
23
+ const __KV_HAS_LIST: bool = ${kinds.has('kv.list')};
24
+ const __KV_HAS_DELETE: bool = ${kinds.has('kv.delete')};`;
25
+ return limits + '\n' + selection + '\n' + fs.readFileSync(path.join(__dirname, 'kv-native.as.ts'), 'utf8');
26
+ }
27
+ module.exports = { KV_CONDITIONAL_KINDS, KV_CONDITIONAL_LIMITS, KV_IMPORTS, kvImports, kvNativeSource };