@pulse-compute/wasm-host-runtime 1.0.0-beta.4 → 1.0.0-beta.5
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
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
> **Supported entry points:** `None for application authors.`<br>
|
|
8
8
|
> **Stability:** Internal compiler/runtime interface synchronized with this release set.<br>
|
|
9
9
|
> **npm:** [`@pulse-compute/wasm-host-runtime`](https://www.npmjs.com/package/@pulse-compute/wasm-host-runtime)<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.5/packages/implementation-packages/)
|
|
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.5` package policy.
|
|
13
13
|
<!-- pulse-package-status:end -->
|
|
14
14
|
|
|
15
15
|
Package-owned PulseWasm host-runtime build-time builders and local runtime helpers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/wasm-host-runtime",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"./runtime/canonical-native-host": "./src/runtime/canonical-native-host.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@pulse-compute/crypto": "1.0.0-beta.
|
|
32
|
-
"@pulse-compute/runtime": "1.0.0-beta.
|
|
33
|
-
"@pulse-compute/wasm-build-support": "1.0.0-beta.
|
|
34
|
-
"@pulse-compute/wasm-contracts": "1.0.0-beta.
|
|
35
|
-
"@pulse-compute/wasm-schema-json": "1.0.0-beta.
|
|
31
|
+
"@pulse-compute/crypto": "1.0.0-beta.5",
|
|
32
|
+
"@pulse-compute/runtime": "1.0.0-beta.5",
|
|
33
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.5",
|
|
34
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.5",
|
|
35
|
+
"@pulse-compute/wasm-schema-json": "1.0.0-beta.5"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
49
49
|
"directory": "wasm/packages/host-runtime"
|
|
50
50
|
},
|
|
51
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
51
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/",
|
|
52
52
|
"bugs": {
|
|
53
53
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
54
54
|
}
|
|
@@ -390,6 +390,7 @@ function assertSchemaBodySize(schemaCodecs, text, schemaId, source) {
|
|
|
390
390
|
const runtimeValueErrorCodes = Object.freeze({
|
|
391
391
|
BodyTooLargeError: 'PULSE_BODY_TOO_LARGE',
|
|
392
392
|
BodyDecodeError: 'PULSE_BODY_DECODE',
|
|
393
|
+
RequestBodyInvalidUtf8Error: 'PULSE_REQUEST_BODY_INVALID_UTF8',
|
|
393
394
|
BodyUnavailableError: 'PULSE_BODY_UNAVAILABLE',
|
|
394
395
|
OpaqueBodyInspectionError: 'PULSE_OPAQUE_BODY_INSPECTION',
|
|
395
396
|
ResponseEncodeError: 'PULSE_RESPONSE_ENCODE',
|
|
@@ -431,7 +432,7 @@ function normalizeRequest(options = {}) {
|
|
|
431
432
|
function createRequestSurface(request, options = {}, schemaCodecs, trace) {
|
|
432
433
|
const body = runtimeApi.body.createValue(
|
|
433
434
|
{ body: request.body, headers: request.headers },
|
|
434
|
-
{ maxBytes: options.maxRequestBodyBytes || options.maxBodyBytes || options.maxStructuredBodyBytes }
|
|
435
|
+
{ maxBytes: options.maxRequestBodyBytes || options.maxBodyBytes || options.maxStructuredBodyBytes, strictUtf8: true }
|
|
435
436
|
);
|
|
436
437
|
const schemaMemo = new Map();
|
|
437
438
|
return Object.freeze({
|
|
@@ -634,6 +635,30 @@ function createCanonicalContext(options, trace) {
|
|
|
634
635
|
}
|
|
635
636
|
|
|
636
637
|
const ctx = Object.freeze({
|
|
638
|
+
decodeJson(text, schemaId) {
|
|
639
|
+
const key = requireSchemaId(schemaCodecs, schemaId, 'application-text');
|
|
640
|
+
const event = {
|
|
641
|
+
boundary: 'application-text', schemaId: key, responseCaseId: null,
|
|
642
|
+
operationId: `application-text:${key}`, status: null,
|
|
643
|
+
contentType: null, headers: [], bodyOwnership: 'application-owned',
|
|
644
|
+
target: options.target || null, provider: options.provider || null,
|
|
645
|
+
effectId: null, groupId: null
|
|
646
|
+
};
|
|
647
|
+
try {
|
|
648
|
+
if (typeof text !== 'string') {
|
|
649
|
+
throw new CanonicalRuntimeError('SchemaDecodeError', 'PULSE_SCHEMA_DECODE',
|
|
650
|
+
'Application JSON text must be a string.',
|
|
651
|
+
{ schemaId: key, source: 'application-text', expected: 'string', actualKind: typeof text });
|
|
652
|
+
}
|
|
653
|
+
assertSchemaBodySize(schemaCodecs, text, key, 'application-text');
|
|
654
|
+
const value = schemaCodecs.decodeJsonText(key, text, 'application-text');
|
|
655
|
+
jsonTrace(trace, { ...event, kind: 'json.decode.text' }, value);
|
|
656
|
+
return value;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
jsonTrace(trace, { ...event, kind: 'json.decode.error', ...jsonTraceErrorDetail(error) });
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
},
|
|
637
662
|
encodeJson(value, schemaId) {
|
|
638
663
|
const key = requireSchemaId(schemaCodecs, schemaId, 'application-value');
|
|
639
664
|
const event = {
|
|
@@ -911,6 +936,7 @@ function normalizeProviderEffect(effect, schemaCodecs, options = {}) {
|
|
|
911
936
|
if (effect.kind === 'fetch') return normalizeFetchEffect(effect, schemaCodecs, options);
|
|
912
937
|
const id = String(effect.id || '');
|
|
913
938
|
if (!id) throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical provider effect requires an id.', { effect });
|
|
939
|
+
if (effect.kind === 'time.now') return Object.freeze({ id, kind: 'time.now', providerKind: 'time', operation: 'now', capability: 'time.wall-clock', source: effect.source, ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }) });
|
|
914
940
|
if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
|
|
915
941
|
return Object.freeze({ id, kind: effect.kind, name: String(effect.name), ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
|
|
916
942
|
}
|
|
@@ -1324,6 +1350,16 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1324
1350
|
const registry = options.continuationRegistry || createContinuationRegistry({ ttlMs: options.continuationTtlMs, clock: options.clock });
|
|
1325
1351
|
|
|
1326
1352
|
async function execute(programModule, executionOptions = {}) {
|
|
1353
|
+
const input = { ...options, ...executionOptions };
|
|
1354
|
+
const budget = portableKv.createRequestBudget(input);
|
|
1355
|
+
try {
|
|
1356
|
+
budget.check();
|
|
1357
|
+
return await executeInvocation(programModule, { ...executionOptions, requestBudget: budget, signal: budget.signal,
|
|
1358
|
+
deadlineMonotonicMs: budget.deadlineMonotonicMs ?? input.deadlineMonotonicMs, kvClock: budget.deadlineMonotonicMs === undefined ? input.kvClock : budget.clock });
|
|
1359
|
+
} finally { if (!input.requestBudget) budget.close(); }
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
async function executeInvocation(programModule, executionOptions) {
|
|
1327
1363
|
if (!programModule || typeof programModule.createHandler !== 'function') throw new TypeError('Canonical runtime requires a compiled canonical module.');
|
|
1328
1364
|
const metadata = assertCanonicalProgramCompatibility(programModule);
|
|
1329
1365
|
executionSequence += 1;
|
|
@@ -1385,6 +1421,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1385
1421
|
}
|
|
1386
1422
|
|
|
1387
1423
|
async function dispatchOne(effect) {
|
|
1424
|
+
executionOptions.requestBudget.check();
|
|
1388
1425
|
const normalized = normalizeProviderEffect(effect, schemaCodecs, runtimeOptions);
|
|
1389
1426
|
const conditional = portableKv.isConditionalKv(normalized.kind);
|
|
1390
1427
|
if (conditional) portableKv.registerKvRedactions(normalized, (value) => sensitiveValues.add(value));
|
|
@@ -1394,7 +1431,8 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1394
1431
|
const effectExecution = { ...options, ...executionOptions, metadata, executionId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
|
|
1395
1432
|
let snapshot = conditional
|
|
1396
1433
|
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)), effectExecution, runtimeOptions)
|
|
1397
|
-
: await adapter.dispatchEffect(normalized, effectExecution);
|
|
1434
|
+
: await executionOptions.requestBudget.race(adapter.dispatchEffect(normalized, effectExecution));
|
|
1435
|
+
executionOptions.requestBudget.check();
|
|
1398
1436
|
if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') {
|
|
1399
1437
|
if (snapshot !== undefined && typeof snapshot !== 'string') {
|
|
1400
1438
|
throw new CanonicalRuntimeError(
|
|
@@ -1415,6 +1453,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1415
1453
|
{ receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
|
|
1416
1454
|
);
|
|
1417
1455
|
}
|
|
1456
|
+
if (normalized.kind === 'time.now') snapshot = require('@pulse-compute/runtime/host').normalizeTimeResult(snapshot);
|
|
1418
1457
|
if (normalized.kind === 'event.emit' && snapshot !== undefined) {
|
|
1419
1458
|
throw new CanonicalRuntimeError(
|
|
1420
1459
|
'EventAcceptanceError',
|
|
@@ -1487,7 +1526,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1487
1526
|
const generator = programModule.createHandler()(context.ctx, pulse);
|
|
1488
1527
|
if (!generator || typeof generator.next !== 'function') throw new CanonicalRuntimeError('CanonicalHandlerProtocolError', 'PULSE_CANONICAL_HANDLER_PROTOCOL', 'Compiled canonical handler did not return a generator.');
|
|
1489
1528
|
let step;
|
|
1490
|
-
try { step = generator.next(); }
|
|
1529
|
+
try { executionOptions.requestBudget.check(); step = generator.next(); }
|
|
1491
1530
|
catch (error) { error.execution = { executionId, trace: [...trace], continuations: executionContinuations() }; throw error; }
|
|
1492
1531
|
|
|
1493
1532
|
while (!step.done) {
|
|
@@ -1501,6 +1540,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1501
1540
|
let value;
|
|
1502
1541
|
try {
|
|
1503
1542
|
value = marker.kind === 'group' ? await dispatchGroup(effects) : await dispatchOne(effects[0]);
|
|
1543
|
+
executionOptions.requestBudget.check();
|
|
1504
1544
|
registry.resume(continuationId);
|
|
1505
1545
|
recordTrace(Object.freeze({ type: 'continuation-resumed', provider: adapter.id, executionId, continuationId, branchPoint: marker.continuationId }));
|
|
1506
1546
|
step = generator.next(value);
|
|
@@ -1517,7 +1557,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1517
1557
|
}
|
|
1518
1558
|
|
|
1519
1559
|
let response;
|
|
1520
|
-
try { response = finalResponse(step.value, context.request.method); }
|
|
1560
|
+
try { executionOptions.requestBudget.check(); response = finalResponse(step.value, context.request.method); }
|
|
1521
1561
|
catch (error) {
|
|
1522
1562
|
error.execution = Object.freeze({ executionId, trace: Object.freeze([...trace]), continuations: Object.freeze(executionContinuations()), resolutionOrder: Object.freeze([...resolutionOrder]) });
|
|
1523
1563
|
throw error;
|
|
@@ -1570,6 +1610,7 @@ module.exports = {
|
|
|
1570
1610
|
CanonicalRuntimeError,
|
|
1571
1611
|
assertCanonicalProgramCompatibility,
|
|
1572
1612
|
createCanonicalHostRuntime,
|
|
1613
|
+
createRequestBudget: portableKv.createRequestBudget,
|
|
1573
1614
|
executeCanonicalProgram,
|
|
1574
1615
|
createCanonicalContext,
|
|
1575
1616
|
normalizeFetchResponse,
|
|
@@ -244,6 +244,7 @@ function nativeEffect(planEffect, payload) {
|
|
|
244
244
|
resource: planEffect.resource,
|
|
245
245
|
source: planEffect.source
|
|
246
246
|
};
|
|
247
|
+
if (planEffect.kind === 'time.now') return { ...base, providerKind: 'time', operation: 'now', capability: 'time.wall-clock' };
|
|
247
248
|
if (planEffect.kind === 'fetch') {
|
|
248
249
|
const response = nativeFetchResponseContract(planEffect, payload);
|
|
249
250
|
return { ...base, url: payload.url, init: payload.init, ...response };
|
|
@@ -631,6 +632,11 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
631
632
|
return put(unary(operator, value(valueHandle)));
|
|
632
633
|
},
|
|
633
634
|
value_truthy(valueHandle) { return value(valueHandle) ? 1 : 0; },
|
|
635
|
+
value_string_trim(valueHandle) {
|
|
636
|
+
const input = value(valueHandle);
|
|
637
|
+
if (typeof input !== 'string') throw new CanonicalNativeHostError('String trim requires a string value.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
638
|
+
return put(input.trim());
|
|
639
|
+
},
|
|
634
640
|
value_nullish(valueHandle) { return value(valueHandle) === null || value(valueHandle) === undefined ? 1 : 0; },
|
|
635
641
|
log(level, messageHandle) {
|
|
636
642
|
const methods = Object.freeze({ 1: 'error', 2: 'warn', 3: 'info', 4: 'debug' });
|
|
@@ -645,7 +651,12 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
645
651
|
request_url() { requireHttpSurface('ctx.req.url'); return put(context.ctx.req.url); },
|
|
646
652
|
request_path() { requireHttpSurface('ctx.req.path'); return put(context.ctx.req.path); },
|
|
647
653
|
request_headers() { requireHttpSurface('ctx.req.headers'); return put(context.ctx.req.headers); },
|
|
648
|
-
request_header(nameHandle) {
|
|
654
|
+
request_header(nameHandle) {
|
|
655
|
+
requireHttpSurface('ctx.req.header');
|
|
656
|
+
const name = String(value(nameHandle)).toLowerCase();
|
|
657
|
+
const header = context.ctx.req.headers.find(([key]) => String(key).toLowerCase() === name);
|
|
658
|
+
return put(header ? header[1] : undefined);
|
|
659
|
+
},
|
|
649
660
|
request_text() { requireHttpSurface('ctx.req.text'); return put(context.ctx.req.text()); },
|
|
650
661
|
request_json(schemaHandle) {
|
|
651
662
|
requireHttpSurface('ctx.req.json');
|
|
@@ -656,6 +667,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
656
667
|
router_param(pathHandle, patternHandle, nameHandle) { return put(routerParam(value(pathHandle), value(patternHandle), value(nameHandle))); },
|
|
657
668
|
response_json(valueHandle, optionsHandle) { requireHttpSurface('ctx.json'); return put(context.ctx.json(value(valueHandle), value(optionsHandle))); },
|
|
658
669
|
schema_encode(valueHandle, schemaHandle) { return put(context.ctx.encodeJson(value(valueHandle), value(schemaHandle))); },
|
|
670
|
+
schema_decode(textHandle, schemaHandle) { return put(context.ctx.decodeJson(value(textHandle), value(schemaHandle))); },
|
|
659
671
|
response_text(valueHandle, optionsHandle) { requireHttpSurface('ctx.text'); return put(context.ctx.text(value(valueHandle), value(optionsHandle))); },
|
|
660
672
|
response_custom(valueHandle) { requireHttpSurface('ctx.response'); return put(context.ctx.response(value(valueHandle))); },
|
|
661
673
|
grip_is_websocket() {
|
|
@@ -748,6 +760,24 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
748
760
|
else env[item.name] = () => { throw new CanonicalNativeHostError(`Unsupported AssemblyScript env import ${item.name}.`, 'PULSE_CANONICAL_NATIVE_ENV_IMPORT_UNSUPPORTED', { name: item.name }); };
|
|
749
761
|
}
|
|
750
762
|
|
|
763
|
+
let applicationError = 0;
|
|
764
|
+
const applicationErrors = executionPlane === 'http' && (plan.routing?.entries || []).some(entry => entry.kind === 'error');
|
|
765
|
+
function captureApplicationError(error) {
|
|
766
|
+
if (!applicationErrors || options.signal?.aborted || !portableKv.isApplicationError(error)) return false;
|
|
767
|
+
if (!applicationError) {
|
|
768
|
+
const safe = canonicalRuntime.redactRuntimeError(error, sensitiveValues);
|
|
769
|
+
applicationError = put(Object.freeze({ name: safe.name, code: safe.code, message: safe.message }));
|
|
770
|
+
}
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
if (applicationErrors) for (const [name, implementation] of Object.entries(pulseHost)) {
|
|
774
|
+
pulseHost[name] = (...args) => {
|
|
775
|
+
if (applicationError) return 0;
|
|
776
|
+
try { return implementation(...args); }
|
|
777
|
+
catch (error) { if (!captureApplicationError(error)) throw error; return 0; }
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
pulseHost.router_error_take = () => { const error = applicationError; applicationError = 0; return error; };
|
|
751
781
|
instance = new WebAssembly.Instance(module, { pulse_host: pulseHost, env });
|
|
752
782
|
if (schemaIndexes.size > 0) {
|
|
753
783
|
const available = new Set(moduleShape.exports.map((entry) => entry.name));
|
|
@@ -790,6 +820,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
790
820
|
}
|
|
791
821
|
|
|
792
822
|
let result = portableKv.isConditionalKv(effect.kind) ? portableKv.normalizeConditionalKvResult(effect, rawResult, options) : rawResult;
|
|
823
|
+
if (effect.kind === 'time.now') result = require('@pulse-compute/runtime/host').normalizeTimeResult(rawResult);
|
|
793
824
|
if (effect.kind === 'event.emit' && result !== undefined) {
|
|
794
825
|
throw new canonicalRuntime.CanonicalRuntimeError(
|
|
795
826
|
'EventAcceptanceError',
|
|
@@ -897,6 +928,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
897
928
|
resume() { return instance.exports.pulse_resume(); },
|
|
898
929
|
pendingEffects: takePending,
|
|
899
930
|
prepareEffectResult,
|
|
931
|
+
captureApplicationError,
|
|
900
932
|
setEffectResult,
|
|
901
933
|
resultValue,
|
|
902
934
|
response() { requireHttpSurface('HTTP completion'); return canonicalRuntime.finalResponse(resultValue(), context.ctx.req.method); },
|
|
@@ -915,9 +947,18 @@ function nativeEventCancelled(message) {
|
|
|
915
947
|
return new CanonicalNativeHostError(message, 'PULSE_CANONICAL_NATIVE_EVENT_CANCELLED');
|
|
916
948
|
}
|
|
917
949
|
|
|
918
|
-
function
|
|
950
|
+
function nativeRequestCancelled(signal) {
|
|
951
|
+
if (['PULSE_REQUEST_DEADLINE_EXCEEDED', 'PULSE_REQUEST_CLOCK_INVALID'].includes(signal.reason?.code)) return signal.reason;
|
|
952
|
+
return new portableKv.PulseRuntimeContractError('PULSE_RUNTIME_EFFECT_ABORTED',
|
|
953
|
+
signal.reason instanceof Error ? signal.reason.message : 'Native execution was cancelled.',
|
|
954
|
+
{ cause: signal.reason });
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function raceNativeSignal(value, signal, eventMode = true) {
|
|
919
958
|
if (!signal) return Promise.resolve(value);
|
|
920
|
-
|
|
959
|
+
const cancelled = () => eventMode ? nativeEventCancelled('Native event execution was cancelled during effect dispatch.')
|
|
960
|
+
: nativeRequestCancelled(signal);
|
|
961
|
+
if (signal.aborted) return Promise.reject(cancelled());
|
|
921
962
|
return new Promise((resolve, reject) => {
|
|
922
963
|
let settled = false;
|
|
923
964
|
const finish = (callback, result) => {
|
|
@@ -926,7 +967,7 @@ function raceNativeEventSignal(value, signal) {
|
|
|
926
967
|
signal.removeEventListener('abort', onAbort);
|
|
927
968
|
callback(result);
|
|
928
969
|
};
|
|
929
|
-
const onAbort = () => finish(reject,
|
|
970
|
+
const onAbort = () => finish(reject, cancelled());
|
|
930
971
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
931
972
|
Promise.resolve(value).then(
|
|
932
973
|
(result) => finish(resolve, result),
|
|
@@ -955,14 +996,17 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
955
996
|
let executionFailure;
|
|
956
997
|
let activeContinuation;
|
|
957
998
|
try {
|
|
999
|
+
if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
958
1000
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
959
1001
|
throw nativeEventCancelled('Native event execution was cancelled before handler entry.');
|
|
960
1002
|
}
|
|
1003
|
+
options.requestBudget?.check();
|
|
961
1004
|
status = eventMode ? controller.startEvent(invocation.frame) : controller.start();
|
|
962
1005
|
while (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.SUSPENDED) {
|
|
963
1006
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
964
1007
|
throw nativeEventCancelled('Native event execution was cancelled while suspended.');
|
|
965
1008
|
}
|
|
1009
|
+
options.requestBudget?.check();
|
|
966
1010
|
const pending = controller.pendingEffects();
|
|
967
1011
|
if (pending.length !== controller.exports.pulse_pending_count()) {
|
|
968
1012
|
throw new CanonicalNativeHostError('Native pending-effect count differs from host queue.', 'PULSE_CANONICAL_NATIVE_PENDING_MISMATCH', { moduleCount: controller.exports.pulse_pending_count(), hostCount: pending.length });
|
|
@@ -995,7 +1039,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
995
1039
|
}
|
|
996
1040
|
effectCount += pending.length;
|
|
997
1041
|
|
|
998
|
-
const settled = await Promise.allSettled(pending.map(async (entry) => {
|
|
1042
|
+
const settled = await raceNativeSignal(Promise.allSettled(pending.map(async (entry) => {
|
|
1043
|
+
options.requestBudget?.check();
|
|
999
1044
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
1000
1045
|
throw nativeEventCancelled('Native event execution was cancelled before effect dispatch.');
|
|
1001
1046
|
}
|
|
@@ -1033,19 +1078,27 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1033
1078
|
const rawResult = conditional
|
|
1034
1079
|
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)),
|
|
1035
1080
|
{ ...effectExecution, onKvObservation: (event) => controller.trace.push(event) }, options)
|
|
1036
|
-
: await
|
|
1081
|
+
: await raceNativeSignal(adapter.dispatchEffect(normalized, effectExecution), options.signal, eventMode);
|
|
1082
|
+
options.requestBudget?.check();
|
|
1037
1083
|
const result = controller.prepareEffectResult(entry.index, rawResult);
|
|
1038
1084
|
controller.trace.push(Object.freeze(redactValue({ type: 'native-effect-resolved', executionId, provider: adapter.id, effectId: normalized.id, kind: normalized.kind, result: privateEffect || entry.effect.kind === 'secret.get' ? '<redacted>' : result && typeof result.toJSON === 'function' ? result.toJSON() : result }, controller.sensitiveValues)));
|
|
1039
1085
|
resolutionOrder.push(entry.effect.id);
|
|
1040
1086
|
return { entry, result };
|
|
1041
|
-
}));
|
|
1087
|
+
})), options.signal, eventMode);
|
|
1042
1088
|
|
|
1043
1089
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
1044
1090
|
throw nativeEventCancelled('Native event execution was cancelled while suspended.');
|
|
1045
1091
|
}
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1092
|
+
options.requestBudget?.check();
|
|
1093
|
+
if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
1094
|
+
const failures = settled.filter(entry => entry.status === 'rejected');
|
|
1095
|
+
const failed = failures.find(entry => !portableKv.isApplicationError(entry.reason)) || failures[0];
|
|
1096
|
+
if (failed && (!failures.every(entry => portableKv.isApplicationError(entry.reason))
|
|
1097
|
+
|| !controller.captureApplicationError(failed.reason))) {
|
|
1098
|
+
throw canonicalRuntime.redactRuntimeError(failed.reason, controller.sensitiveValues);
|
|
1099
|
+
}
|
|
1100
|
+
settled.forEach((item, index) => controller.setEffectResult(pending[index].index,
|
|
1101
|
+
item.status === 'fulfilled' ? item.value.result : undefined));
|
|
1049
1102
|
const previousPc = controller.programCounter();
|
|
1050
1103
|
activeContinuation.state = 'resumed';
|
|
1051
1104
|
activeContinuation.states.push('resumed');
|
|
@@ -1053,8 +1106,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1053
1106
|
if (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.INVALID_RESUME) {
|
|
1054
1107
|
throw new CanonicalNativeHostError('Native module rejected a complete effect-result set.', 'PULSE_CANONICAL_NATIVE_INVALID_RESUME', { programCounter: previousPc, errorCode: controller.lastErrorCode() });
|
|
1055
1108
|
}
|
|
1056
|
-
activeContinuation.state = 'completed';
|
|
1057
|
-
activeContinuation.states.push(
|
|
1109
|
+
activeContinuation.state = failed ? 'failed' : 'completed';
|
|
1110
|
+
activeContinuation.states.push(activeContinuation.state);
|
|
1058
1111
|
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, continuationState }, controller.sensitiveValues)));
|
|
1059
1112
|
activeContinuation = undefined;
|
|
1060
1113
|
}
|
|
@@ -1086,6 +1139,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1086
1139
|
valueHandleCount: controller.heap.size()
|
|
1087
1140
|
});
|
|
1088
1141
|
}
|
|
1142
|
+
if (options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
1143
|
+
options.requestBudget?.check();
|
|
1089
1144
|
const response = controller.response();
|
|
1090
1145
|
controller.trace.push(Object.freeze(redactValue({ type: 'native-execution-completed', executionId, provider: adapter.id, status: response.status, bodyClass: response.bodyClass }, controller.sensitiveValues)));
|
|
1091
1146
|
return Object.freeze({
|
|
@@ -1126,7 +1181,11 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1126
1181
|
}
|
|
1127
1182
|
|
|
1128
1183
|
async function executeCanonicalNativeModule(compiled, options = {}) {
|
|
1129
|
-
|
|
1184
|
+
const budget = portableKv.createRequestBudget(options);
|
|
1185
|
+
try {
|
|
1186
|
+
budget.check();
|
|
1187
|
+
return await executeCanonicalNativeInvocation(compiled, { ...options, requestBudget: budget, signal: budget.signal, deadlineMonotonicMs: budget.deadlineMonotonicMs ?? options.deadlineMonotonicMs, kvClock: budget.deadlineMonotonicMs === undefined ? options.kvClock : budget.clock }, Object.freeze({ plane: 'http' }));
|
|
1188
|
+
} finally { if (!options.requestBudget) budget.close(); }
|
|
1130
1189
|
}
|
|
1131
1190
|
|
|
1132
1191
|
function nativeEventFailure(error) {
|