@pulse-compute/wasm-host-runtime 1.0.0-beta.3 → 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,50 @@ 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
|
+
},
|
|
662
|
+
encodeJson(value, schemaId) {
|
|
663
|
+
const key = requireSchemaId(schemaCodecs, schemaId, 'application-value');
|
|
664
|
+
const event = {
|
|
665
|
+
boundary: 'application-value', schemaId: key, responseCaseId: null,
|
|
666
|
+
operationId: `application-value:${key}`, status: null,
|
|
667
|
+
contentType: 'application/json; charset=utf-8', headers: [],
|
|
668
|
+
bodyOwnership: 'application-owned', target: options.target || null,
|
|
669
|
+
provider: options.provider || null, effectId: null, groupId: null
|
|
670
|
+
};
|
|
671
|
+
try {
|
|
672
|
+
const text = schemaCodecs.encodeJsonText(key, value, 'application-value');
|
|
673
|
+
assertSchemaBodySize(schemaCodecs, text, key, 'application-value');
|
|
674
|
+
const normalized = schemaCodecs.decodeJsonText(key, text, 'application-value-trace');
|
|
675
|
+
jsonTrace(trace, { ...event, kind: 'json.encode.value' }, normalized);
|
|
676
|
+
return text;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
jsonTrace(trace, { ...event, kind: 'json.encode.error', ...jsonTraceErrorDetail(error) });
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
},
|
|
637
682
|
req: requestSurface.surface,
|
|
638
683
|
log: Object.freeze(log),
|
|
639
684
|
fetch() {
|
|
@@ -891,6 +936,7 @@ function normalizeProviderEffect(effect, schemaCodecs, options = {}) {
|
|
|
891
936
|
if (effect.kind === 'fetch') return normalizeFetchEffect(effect, schemaCodecs, options);
|
|
892
937
|
const id = String(effect.id || '');
|
|
893
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) }) });
|
|
894
940
|
if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
|
|
895
941
|
return Object.freeze({ id, kind: effect.kind, name: String(effect.name), ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
|
|
896
942
|
}
|
|
@@ -1304,6 +1350,16 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1304
1350
|
const registry = options.continuationRegistry || createContinuationRegistry({ ttlMs: options.continuationTtlMs, clock: options.clock });
|
|
1305
1351
|
|
|
1306
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) {
|
|
1307
1363
|
if (!programModule || typeof programModule.createHandler !== 'function') throw new TypeError('Canonical runtime requires a compiled canonical module.');
|
|
1308
1364
|
const metadata = assertCanonicalProgramCompatibility(programModule);
|
|
1309
1365
|
executionSequence += 1;
|
|
@@ -1365,6 +1421,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1365
1421
|
}
|
|
1366
1422
|
|
|
1367
1423
|
async function dispatchOne(effect) {
|
|
1424
|
+
executionOptions.requestBudget.check();
|
|
1368
1425
|
const normalized = normalizeProviderEffect(effect, schemaCodecs, runtimeOptions);
|
|
1369
1426
|
const conditional = portableKv.isConditionalKv(normalized.kind);
|
|
1370
1427
|
if (conditional) portableKv.registerKvRedactions(normalized, (value) => sensitiveValues.add(value));
|
|
@@ -1374,7 +1431,8 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1374
1431
|
const effectExecution = { ...options, ...executionOptions, metadata, executionId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
|
|
1375
1432
|
let snapshot = conditional
|
|
1376
1433
|
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)), effectExecution, runtimeOptions)
|
|
1377
|
-
: await adapter.dispatchEffect(normalized, effectExecution);
|
|
1434
|
+
: await executionOptions.requestBudget.race(adapter.dispatchEffect(normalized, effectExecution));
|
|
1435
|
+
executionOptions.requestBudget.check();
|
|
1378
1436
|
if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') {
|
|
1379
1437
|
if (snapshot !== undefined && typeof snapshot !== 'string') {
|
|
1380
1438
|
throw new CanonicalRuntimeError(
|
|
@@ -1395,6 +1453,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1395
1453
|
{ receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
|
|
1396
1454
|
);
|
|
1397
1455
|
}
|
|
1456
|
+
if (normalized.kind === 'time.now') snapshot = require('@pulse-compute/runtime/host').normalizeTimeResult(snapshot);
|
|
1398
1457
|
if (normalized.kind === 'event.emit' && snapshot !== undefined) {
|
|
1399
1458
|
throw new CanonicalRuntimeError(
|
|
1400
1459
|
'EventAcceptanceError',
|
|
@@ -1467,7 +1526,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1467
1526
|
const generator = programModule.createHandler()(context.ctx, pulse);
|
|
1468
1527
|
if (!generator || typeof generator.next !== 'function') throw new CanonicalRuntimeError('CanonicalHandlerProtocolError', 'PULSE_CANONICAL_HANDLER_PROTOCOL', 'Compiled canonical handler did not return a generator.');
|
|
1469
1528
|
let step;
|
|
1470
|
-
try { step = generator.next(); }
|
|
1529
|
+
try { executionOptions.requestBudget.check(); step = generator.next(); }
|
|
1471
1530
|
catch (error) { error.execution = { executionId, trace: [...trace], continuations: executionContinuations() }; throw error; }
|
|
1472
1531
|
|
|
1473
1532
|
while (!step.done) {
|
|
@@ -1481,6 +1540,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1481
1540
|
let value;
|
|
1482
1541
|
try {
|
|
1483
1542
|
value = marker.kind === 'group' ? await dispatchGroup(effects) : await dispatchOne(effects[0]);
|
|
1543
|
+
executionOptions.requestBudget.check();
|
|
1484
1544
|
registry.resume(continuationId);
|
|
1485
1545
|
recordTrace(Object.freeze({ type: 'continuation-resumed', provider: adapter.id, executionId, continuationId, branchPoint: marker.continuationId }));
|
|
1486
1546
|
step = generator.next(value);
|
|
@@ -1497,7 +1557,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1497
1557
|
}
|
|
1498
1558
|
|
|
1499
1559
|
let response;
|
|
1500
|
-
try { response = finalResponse(step.value, context.request.method); }
|
|
1560
|
+
try { executionOptions.requestBudget.check(); response = finalResponse(step.value, context.request.method); }
|
|
1501
1561
|
catch (error) {
|
|
1502
1562
|
error.execution = Object.freeze({ executionId, trace: Object.freeze([...trace]), continuations: Object.freeze(executionContinuations()), resolutionOrder: Object.freeze([...resolutionOrder]) });
|
|
1503
1563
|
throw error;
|
|
@@ -1550,6 +1610,7 @@ module.exports = {
|
|
|
1550
1610
|
CanonicalRuntimeError,
|
|
1551
1611
|
assertCanonicalProgramCompatibility,
|
|
1552
1612
|
createCanonicalHostRuntime,
|
|
1613
|
+
createRequestBudget: portableKv.createRequestBudget,
|
|
1553
1614
|
executeCanonicalProgram,
|
|
1554
1615
|
createCanonicalContext,
|
|
1555
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');
|
|
@@ -655,6 +666,8 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
655
666
|
router_match(pathHandle, patternHandle) { return put(routerMatch(value(pathHandle), value(patternHandle))); },
|
|
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))); },
|
|
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))); },
|
|
658
671
|
response_text(valueHandle, optionsHandle) { requireHttpSurface('ctx.text'); return put(context.ctx.text(value(valueHandle), value(optionsHandle))); },
|
|
659
672
|
response_custom(valueHandle) { requireHttpSurface('ctx.response'); return put(context.ctx.response(value(valueHandle))); },
|
|
660
673
|
grip_is_websocket() {
|
|
@@ -747,6 +760,24 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
747
760
|
else env[item.name] = () => { throw new CanonicalNativeHostError(`Unsupported AssemblyScript env import ${item.name}.`, 'PULSE_CANONICAL_NATIVE_ENV_IMPORT_UNSUPPORTED', { name: item.name }); };
|
|
748
761
|
}
|
|
749
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; };
|
|
750
781
|
instance = new WebAssembly.Instance(module, { pulse_host: pulseHost, env });
|
|
751
782
|
if (schemaIndexes.size > 0) {
|
|
752
783
|
const available = new Set(moduleShape.exports.map((entry) => entry.name));
|
|
@@ -789,6 +820,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
789
820
|
}
|
|
790
821
|
|
|
791
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);
|
|
792
824
|
if (effect.kind === 'event.emit' && result !== undefined) {
|
|
793
825
|
throw new canonicalRuntime.CanonicalRuntimeError(
|
|
794
826
|
'EventAcceptanceError',
|
|
@@ -896,6 +928,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
896
928
|
resume() { return instance.exports.pulse_resume(); },
|
|
897
929
|
pendingEffects: takePending,
|
|
898
930
|
prepareEffectResult,
|
|
931
|
+
captureApplicationError,
|
|
899
932
|
setEffectResult,
|
|
900
933
|
resultValue,
|
|
901
934
|
response() { requireHttpSurface('HTTP completion'); return canonicalRuntime.finalResponse(resultValue(), context.ctx.req.method); },
|
|
@@ -914,9 +947,18 @@ function nativeEventCancelled(message) {
|
|
|
914
947
|
return new CanonicalNativeHostError(message, 'PULSE_CANONICAL_NATIVE_EVENT_CANCELLED');
|
|
915
948
|
}
|
|
916
949
|
|
|
917
|
-
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) {
|
|
918
958
|
if (!signal) return Promise.resolve(value);
|
|
919
|
-
|
|
959
|
+
const cancelled = () => eventMode ? nativeEventCancelled('Native event execution was cancelled during effect dispatch.')
|
|
960
|
+
: nativeRequestCancelled(signal);
|
|
961
|
+
if (signal.aborted) return Promise.reject(cancelled());
|
|
920
962
|
return new Promise((resolve, reject) => {
|
|
921
963
|
let settled = false;
|
|
922
964
|
const finish = (callback, result) => {
|
|
@@ -925,7 +967,7 @@ function raceNativeEventSignal(value, signal) {
|
|
|
925
967
|
signal.removeEventListener('abort', onAbort);
|
|
926
968
|
callback(result);
|
|
927
969
|
};
|
|
928
|
-
const onAbort = () => finish(reject,
|
|
970
|
+
const onAbort = () => finish(reject, cancelled());
|
|
929
971
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
930
972
|
Promise.resolve(value).then(
|
|
931
973
|
(result) => finish(resolve, result),
|
|
@@ -954,14 +996,17 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
954
996
|
let executionFailure;
|
|
955
997
|
let activeContinuation;
|
|
956
998
|
try {
|
|
999
|
+
if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
957
1000
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
958
1001
|
throw nativeEventCancelled('Native event execution was cancelled before handler entry.');
|
|
959
1002
|
}
|
|
1003
|
+
options.requestBudget?.check();
|
|
960
1004
|
status = eventMode ? controller.startEvent(invocation.frame) : controller.start();
|
|
961
1005
|
while (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.SUSPENDED) {
|
|
962
1006
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
963
1007
|
throw nativeEventCancelled('Native event execution was cancelled while suspended.');
|
|
964
1008
|
}
|
|
1009
|
+
options.requestBudget?.check();
|
|
965
1010
|
const pending = controller.pendingEffects();
|
|
966
1011
|
if (pending.length !== controller.exports.pulse_pending_count()) {
|
|
967
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 });
|
|
@@ -994,7 +1039,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
994
1039
|
}
|
|
995
1040
|
effectCount += pending.length;
|
|
996
1041
|
|
|
997
|
-
const settled = await Promise.allSettled(pending.map(async (entry) => {
|
|
1042
|
+
const settled = await raceNativeSignal(Promise.allSettled(pending.map(async (entry) => {
|
|
1043
|
+
options.requestBudget?.check();
|
|
998
1044
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
999
1045
|
throw nativeEventCancelled('Native event execution was cancelled before effect dispatch.');
|
|
1000
1046
|
}
|
|
@@ -1032,19 +1078,27 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1032
1078
|
const rawResult = conditional
|
|
1033
1079
|
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)),
|
|
1034
1080
|
{ ...effectExecution, onKvObservation: (event) => controller.trace.push(event) }, options)
|
|
1035
|
-
: await
|
|
1081
|
+
: await raceNativeSignal(adapter.dispatchEffect(normalized, effectExecution), options.signal, eventMode);
|
|
1082
|
+
options.requestBudget?.check();
|
|
1036
1083
|
const result = controller.prepareEffectResult(entry.index, rawResult);
|
|
1037
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)));
|
|
1038
1085
|
resolutionOrder.push(entry.effect.id);
|
|
1039
1086
|
return { entry, result };
|
|
1040
|
-
}));
|
|
1087
|
+
})), options.signal, eventMode);
|
|
1041
1088
|
|
|
1042
1089
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
1043
1090
|
throw nativeEventCancelled('Native event execution was cancelled while suspended.');
|
|
1044
1091
|
}
|
|
1045
|
-
|
|
1046
|
-
if (
|
|
1047
|
-
|
|
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));
|
|
1048
1102
|
const previousPc = controller.programCounter();
|
|
1049
1103
|
activeContinuation.state = 'resumed';
|
|
1050
1104
|
activeContinuation.states.push('resumed');
|
|
@@ -1052,8 +1106,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1052
1106
|
if (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.INVALID_RESUME) {
|
|
1053
1107
|
throw new CanonicalNativeHostError('Native module rejected a complete effect-result set.', 'PULSE_CANONICAL_NATIVE_INVALID_RESUME', { programCounter: previousPc, errorCode: controller.lastErrorCode() });
|
|
1054
1108
|
}
|
|
1055
|
-
activeContinuation.state = 'completed';
|
|
1056
|
-
activeContinuation.states.push(
|
|
1109
|
+
activeContinuation.state = failed ? 'failed' : 'completed';
|
|
1110
|
+
activeContinuation.states.push(activeContinuation.state);
|
|
1057
1111
|
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, continuationState }, controller.sensitiveValues)));
|
|
1058
1112
|
activeContinuation = undefined;
|
|
1059
1113
|
}
|
|
@@ -1085,6 +1139,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1085
1139
|
valueHandleCount: controller.heap.size()
|
|
1086
1140
|
});
|
|
1087
1141
|
}
|
|
1142
|
+
if (options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
1143
|
+
options.requestBudget?.check();
|
|
1088
1144
|
const response = controller.response();
|
|
1089
1145
|
controller.trace.push(Object.freeze(redactValue({ type: 'native-execution-completed', executionId, provider: adapter.id, status: response.status, bodyClass: response.bodyClass }, controller.sensitiveValues)));
|
|
1090
1146
|
return Object.freeze({
|
|
@@ -1125,7 +1181,11 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1125
1181
|
}
|
|
1126
1182
|
|
|
1127
1183
|
async function executeCanonicalNativeModule(compiled, options = {}) {
|
|
1128
|
-
|
|
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(); }
|
|
1129
1189
|
}
|
|
1130
1190
|
|
|
1131
1191
|
function nativeEventFailure(error) {
|