@xyo-network/event-kit-node 1.0.0 → 2.1.0
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/dist/node/filesystemCursorStore.d.ts.map +1 -1
- package/dist/node/filesystemPublisherEndpointChallengeStore.d.ts.map +1 -1
- package/dist/node/handshake.mjs +16 -7
- package/dist/node/handshake.mjs.map +2 -2
- package/dist/node/handshakeClients.d.ts +5 -1
- package/dist/node/handshakeClients.d.ts.map +1 -1
- package/dist/node/handshakeMutationIntent.d.ts +128 -66
- package/dist/node/handshakeMutationIntent.d.ts.map +1 -1
- package/dist/node/index.mjs +288 -116
- package/dist/node/index.mjs.map +4 -4
- package/dist/node/runtime/handshakeDiscovery.d.ts +4 -0
- package/dist/node/runtime/handshakeDiscovery.d.ts.map +1 -1
- package/dist/node/runtime/httpsTransport.d.ts.map +1 -1
- package/dist/node/runtime/publisherDeliveryState.d.ts +107 -0
- package/dist/node/runtime/publisherDeliveryState.d.ts.map +1 -0
- package/dist/node/runtime/publisherRuntime.d.ts +17 -1
- package/dist/node/runtime/publisherRuntime.d.ts.map +1 -1
- package/dist/node/runtime/sourceSigner.d.ts +1 -0
- package/dist/node/runtime/sourceSigner.d.ts.map +1 -1
- package/dist/node/runtime/sqliteHandshakeStores.d.ts +3 -1
- package/dist/node/runtime/sqliteHandshakeStores.d.ts.map +1 -1
- package/dist/node/runtime.mjs +271 -105
- package/dist/node/runtime.mjs.map +4 -4
- package/package.json +24 -24
package/dist/node/index.mjs
CHANGED
|
@@ -23,10 +23,7 @@ function createFilesystemCursorStore(options) {
|
|
|
23
23
|
try {
|
|
24
24
|
const raw = await readFile(filePath, "utf8");
|
|
25
25
|
const parsed = JSON.parse(raw);
|
|
26
|
-
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
return parsed.lastProcessedBlock;
|
|
26
|
+
return typeof parsed.lastProcessedBlock !== "number" || !Number.isFinite(parsed.lastProcessedBlock) ? void 0 : parsed.lastProcessedBlock;
|
|
30
27
|
} catch (error) {
|
|
31
28
|
if (isNotFound(error)) return void 0;
|
|
32
29
|
throw error;
|
|
@@ -481,10 +478,7 @@ function replaceJsonValue(_key, candidate) {
|
|
|
481
478
|
return typeof candidate === "bigint" ? { $eventKitBigInt: candidate.toString() } : candidate;
|
|
482
479
|
}
|
|
483
480
|
function reviveJson(_key, candidate) {
|
|
484
|
-
|
|
485
|
-
return BigInt(candidate.$eventKitBigInt);
|
|
486
|
-
}
|
|
487
|
-
return candidate;
|
|
481
|
+
return typeof candidate === "object" && candidate !== null && Object.keys(candidate).length === 1 && "$eventKitBigInt" in candidate && typeof candidate.$eventKitBigInt === "string" && /^(?:0|[1-9][0-9]*)$/u.test(candidate.$eventKitBigInt) ? BigInt(candidate.$eventKitBigInt) : candidate;
|
|
488
482
|
}
|
|
489
483
|
async function unlinkIfMissing3(path) {
|
|
490
484
|
try {
|
|
@@ -1076,7 +1070,7 @@ function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
|
1076
1070
|
if (await publish(attempted)) return { kind: "started", record: attempted };
|
|
1077
1071
|
const winner = await current(challengeHash);
|
|
1078
1072
|
if (winner === void 0) throw new Error("challenge attempt winner vanished");
|
|
1079
|
-
return winner.state === "verified" ?
|
|
1073
|
+
return { kind: winner.state === "verified" ? "verified" : "already-attempted", record: winner };
|
|
1080
1074
|
},
|
|
1081
1075
|
async completeVerified(input) {
|
|
1082
1076
|
const challengeHash = HashZod3.parse(input.challengeHash);
|
|
@@ -1085,7 +1079,7 @@ function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
|
1085
1079
|
const observed = await current(challengeHash);
|
|
1086
1080
|
if (observed === void 0) return { kind: "missing" };
|
|
1087
1081
|
if (observed.state === "verified") {
|
|
1088
|
-
return observed.responseHash === responseHash ?
|
|
1082
|
+
return { kind: observed.responseHash === responseHash ? "duplicate" : "collision", record: observed };
|
|
1089
1083
|
}
|
|
1090
1084
|
if (observed.state !== "attempted") return { kind: "not-attempted", record: observed };
|
|
1091
1085
|
if (!isLive(observed, input.nowSeconds)) return { kind: "expired", record: observed };
|
|
@@ -1097,7 +1091,7 @@ function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
|
1097
1091
|
if (await publish(verified)) return { kind: "verified", record: verified };
|
|
1098
1092
|
const winner = await readState(challengeHash, "verified");
|
|
1099
1093
|
if (winner === void 0) throw new Error("verified challenge winner vanished");
|
|
1100
|
-
return winner.responseHash === responseHash ?
|
|
1094
|
+
return { kind: winner.responseHash === responseHash ? "duplicate" : "collision", record: winner };
|
|
1101
1095
|
},
|
|
1102
1096
|
get: async (rawHash) => await current(HashZod3.parse(rawHash)),
|
|
1103
1097
|
async issue(input) {
|
|
@@ -1355,6 +1349,8 @@ async function persistMutationIntent(store, intent) {
|
|
|
1355
1349
|
}
|
|
1356
1350
|
async function publisherServiceRequestFingerprint(input) {
|
|
1357
1351
|
return await handshakeMutationRequestFingerprint({
|
|
1352
|
+
deliveryMode: input.deliveryMode,
|
|
1353
|
+
bestEffort: input.bestEffort ?? null,
|
|
1358
1354
|
expiresAtBlock: input.expiresAtBlock ?? null,
|
|
1359
1355
|
maxPositionsPerWake: input.maxPositionsPerWake,
|
|
1360
1356
|
sources: input.sources,
|
|
@@ -1374,6 +1370,8 @@ async function publisherRotationRequestFingerprint(input) {
|
|
|
1374
1370
|
}
|
|
1375
1371
|
async function subscriberSubscriptionRequestFingerprint(input) {
|
|
1376
1372
|
return await handshakeMutationRequestFingerprint({
|
|
1373
|
+
deliveryMode: input.deliveryMode,
|
|
1374
|
+
bestEffort: input.bestEffort ?? null,
|
|
1377
1375
|
audience: input.audience,
|
|
1378
1376
|
challengeSecret: input.challengeSecret,
|
|
1379
1377
|
deploymentId: input.deploymentId,
|
|
@@ -1760,6 +1758,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1760
1758
|
validFromBlock
|
|
1761
1759
|
};
|
|
1762
1760
|
const service = createPublisherService({
|
|
1761
|
+
deliveryMode: input.deliveryMode,
|
|
1762
|
+
...input.bestEffort === void 0 ? {} : { bestEffort: input.bestEffort },
|
|
1763
1763
|
...serviceInput,
|
|
1764
1764
|
authorizationChainId: options.reader.chainId,
|
|
1765
1765
|
sources: input.sources,
|
|
@@ -1879,6 +1879,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1879
1879
|
validFromBlock
|
|
1880
1880
|
};
|
|
1881
1881
|
const service = createPublisherService({
|
|
1882
|
+
deliveryMode: selected.service.body.deliveryMode,
|
|
1883
|
+
...selected.service.body.bestEffort === void 0 ? {} : { bestEffort: selected.service.body.bestEffort },
|
|
1882
1884
|
...serviceInput,
|
|
1883
1885
|
authorizationChainId: options.reader.chainId,
|
|
1884
1886
|
sources: selected.service.body.events.map((event) => event.source),
|
|
@@ -2172,6 +2174,8 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2172
2174
|
return Object.freeze(client);
|
|
2173
2175
|
}
|
|
2174
2176
|
function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
2177
|
+
if (input.deliveryMode !== service.deliveryMode || canonicalJson(input.bestEffort ?? null) !== canonicalJson(service.bestEffort ?? null))
|
|
2178
|
+
throw new Error("Requested delivery terms do not match exact selected service");
|
|
2175
2179
|
const plaintextInput = {
|
|
2176
2180
|
audience: input.audience,
|
|
2177
2181
|
challengeSecret: input.challengeSecret,
|
|
@@ -2184,6 +2188,8 @@ function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
|
2184
2188
|
throw new Error("Requested source is not advertised by the selected service");
|
|
2185
2189
|
}
|
|
2186
2190
|
return createWakeSubscriptionPlaintext({
|
|
2191
|
+
deliveryMode: input.deliveryMode,
|
|
2192
|
+
...input.bestEffort === void 0 ? {} : { bestEffort: input.bestEffort },
|
|
2187
2193
|
...plaintextInput,
|
|
2188
2194
|
source,
|
|
2189
2195
|
fromPosition: input.fromPosition,
|
|
@@ -2323,6 +2329,7 @@ function createBoundedPublisherEthersTransport(options = {}) {
|
|
|
2323
2329
|
}
|
|
2324
2330
|
|
|
2325
2331
|
// src/runtime/handshakeDiscovery.ts
|
|
2332
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
2326
2333
|
import { decryptWakeSubscription, sourceWakeFilterHash } from "@xyo-network/event-kit-protocol";
|
|
2327
2334
|
import { EndpointChallengeResponseZod as EndpointChallengeResponseZod2 } from "@xyo-network/event-kit-schemas";
|
|
2328
2335
|
function createPublisherHandshakeDiscovery(options) {
|
|
@@ -2338,6 +2345,9 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2338
2345
|
rejectedSubscriptions: 0,
|
|
2339
2346
|
challengeFailures: 0
|
|
2340
2347
|
};
|
|
2348
|
+
function selectedTuple(tuple) {
|
|
2349
|
+
return (options.serviceHash === void 0 || tuple.service.objectHash === options.serviceHash) && (options.deliveryMode === void 0 || tuple.service.body.deliveryMode === options.deliveryMode) && (options.deliveryMode === void 0 || tuple.subscription.body.deliveryMode === options.deliveryMode) && (options.deliveryMode === void 0 || isDeepStrictEqual2(tuple.service.body.bestEffort, options.bestEffort));
|
|
2350
|
+
}
|
|
2341
2351
|
async function activeView(signal) {
|
|
2342
2352
|
signal.throwIfAborted();
|
|
2343
2353
|
viewing ??= (async () => {
|
|
@@ -2355,7 +2365,7 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2355
2365
|
...status,
|
|
2356
2366
|
finalizedHead: view.finalizedHead,
|
|
2357
2367
|
indexedThrough: view.indexedThrough,
|
|
2358
|
-
pendingSubscriptions: view.subscriptions.filter((item) => item.state === "pending").length,
|
|
2368
|
+
pendingSubscriptions: view.subscriptions.filter((item) => item.state === "pending" && item.tuples.some(selectedTuple)).length,
|
|
2359
2369
|
rejectedSubscriptions: view.rejections.length
|
|
2360
2370
|
};
|
|
2361
2371
|
return view;
|
|
@@ -2420,7 +2430,7 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2420
2430
|
if (!found.ok)
|
|
2421
2431
|
throw new Error("Publisher acceptance candidates are unavailable");
|
|
2422
2432
|
const offset = options.state.get("challenge-rate", "offset") ?? 0;
|
|
2423
|
-
const candidates = found.candidates;
|
|
2433
|
+
const candidates = found.candidates.filter((candidate) => selectedTuple(candidate.tuple));
|
|
2424
2434
|
let attempted = 0;
|
|
2425
2435
|
for (let index = 0; index < Math.min(candidates.length, 128) && attempted < 4; index += 1) {
|
|
2426
2436
|
const candidate = candidates[(offset + index) % candidates.length];
|
|
@@ -2436,30 +2446,40 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2436
2446
|
await acceptCandidate(candidate.subscriber, candidate.tuple.subscription.objectHash, signal);
|
|
2437
2447
|
}
|
|
2438
2448
|
}
|
|
2449
|
+
async function loadScopedKeys(view, active) {
|
|
2450
|
+
const scopedKeys = active.flatMap((item) => item.tuples.filter((tuple) => tuple.subscription.objectHash === item.selectedSubscription).map((tuple) => ({
|
|
2451
|
+
encryptionKey: tuple.subscription.body.encryptionKey,
|
|
2452
|
+
keyId: tuple.subscription.body.keyId,
|
|
2453
|
+
publisher: options.publisher
|
|
2454
|
+
})));
|
|
2455
|
+
if (view.hasCurrentAdvertisement && (options.serviceHash === void 0 || view.service.objectHash === options.serviceHash) && (options.deliveryMode === void 0 || view.service.body.deliveryMode === options.deliveryMode)) {
|
|
2456
|
+
scopedKeys.push({
|
|
2457
|
+
encryptionKey: view.key.objectHash,
|
|
2458
|
+
keyId: view.key.body.keyId,
|
|
2459
|
+
publisher: options.publisher
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
const loadedKeys = await Promise.all(scopedKeys.map(async (key) => await options.keys.getPrivateKey(key)));
|
|
2463
|
+
status = {
|
|
2464
|
+
...status,
|
|
2465
|
+
keyLoaded: scopedKeys.length > 0 && loadedKeys.every((key) => key !== void 0),
|
|
2466
|
+
activeKeyIds: [...new Set(scopedKeys.map((key) => key.keyId))]
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2439
2469
|
return {
|
|
2440
2470
|
async discover(signal) {
|
|
2441
2471
|
const view = await activeView(signal);
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
encryptionKey: key.objectHash,
|
|
2445
|
-
keyId: key.body.keyId,
|
|
2446
|
-
publisher: options.publisher
|
|
2447
|
-
});
|
|
2448
|
-
status = {
|
|
2449
|
-
...status,
|
|
2450
|
-
keyLoaded: loaded !== void 0,
|
|
2451
|
-
activeKeyIds: key === void 0 ? [] : [key.body.keyId]
|
|
2452
|
-
};
|
|
2453
|
-
const subscriptions = [];
|
|
2454
|
-
const active = view.subscriptions.filter((item) => item.state === "active");
|
|
2472
|
+
const active = view.subscriptions.filter((item) => item.state === "active" && item.tuples.some((tuple) => tuple.subscription.objectHash === item.selectedSubscription && selectedTuple(tuple)));
|
|
2473
|
+
await loadScopedKeys(view, active);
|
|
2455
2474
|
if (active.length > (options.maxSubscriptions ?? 64))
|
|
2456
2475
|
throw new Error("Publisher active subscription limit exceeded");
|
|
2476
|
+
const subscriptions = [];
|
|
2457
2477
|
for (const lifecycle of active) {
|
|
2458
2478
|
if (lifecycle.state !== "active" || lifecycle.activeFromBlock === void 0)
|
|
2459
2479
|
continue;
|
|
2460
2480
|
try {
|
|
2461
2481
|
const tuple = lifecycle.tuples.find((item) => item.subscription.objectHash === lifecycle.selectedSubscription);
|
|
2462
|
-
if (tuple === void 0)
|
|
2482
|
+
if (tuple === void 0 || !selectedTuple(tuple))
|
|
2463
2483
|
throw new Error("Active publisher subscription has no selected tuple");
|
|
2464
2484
|
const encrypted = tuple.subscription.body;
|
|
2465
2485
|
if (encrypted.chainId !== options.authorizationChainId)
|
|
@@ -2479,6 +2499,9 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2479
2499
|
options.transport.assertAllowed(plaintext.endpoint);
|
|
2480
2500
|
subscriptions.push({
|
|
2481
2501
|
subscriptionHash: tuple.subscription.objectHash,
|
|
2502
|
+
serviceHash: tuple.service.objectHash,
|
|
2503
|
+
deliveryMode: plaintext.filter.deliveryMode,
|
|
2504
|
+
...plaintext.filter.bestEffort === void 0 ? {} : { bestEffort: plaintext.filter.bestEffort },
|
|
2482
2505
|
authorizationChainId: options.authorizationChainId,
|
|
2483
2506
|
controller: tuple.subscriber,
|
|
2484
2507
|
audience: plaintext.audience,
|
|
@@ -2498,7 +2521,7 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2498
2521
|
},
|
|
2499
2522
|
async isActive(subscription, signal) {
|
|
2500
2523
|
const view = await activeView(signal);
|
|
2501
|
-
return view.subscriptions.some((item) => item.state === "active" && item.selectedSubscription === subscription.subscriptionHash && item.tuples.some((tuple) => tuple.subscription.objectHash === subscription.subscriptionHash && tuple.subscriber === subscription.controller));
|
|
2524
|
+
return view.subscriptions.some((item) => item.state === "active" && item.selectedSubscription === subscription.subscriptionHash && item.tuples.some((tuple) => tuple.subscription.objectHash === subscription.subscriptionHash && tuple.subscriber === subscription.controller && selectedTuple(tuple)));
|
|
2502
2525
|
},
|
|
2503
2526
|
async acceptPending(signal) {
|
|
2504
2527
|
if (accepting === void 0) {
|
|
@@ -2717,11 +2740,7 @@ function loadPublisherAddressPolicy() {
|
|
|
2717
2740
|
}
|
|
2718
2741
|
function isPublicPublisherAddress(address) {
|
|
2719
2742
|
const policy = loadPublisherAddressPolicy();
|
|
2720
|
-
|
|
2721
|
-
return !policy.nonPublicIpv4.check(address, "ipv4");
|
|
2722
|
-
if (isIP(address) !== 6)
|
|
2723
|
-
return false;
|
|
2724
|
-
return policy.globalIpv6.check(address, "ipv6") && !policy.nonPublicIpv6.check(address, "ipv6");
|
|
2743
|
+
return isIP(address) === 4 ? !policy.nonPublicIpv4.check(address, "ipv4") : isIP(address) === 6 && policy.globalIpv6.check(address, "ipv6") && !policy.nonPublicIpv6.check(address, "ipv6");
|
|
2725
2744
|
}
|
|
2726
2745
|
function createPublisherHttpsTransport(input) {
|
|
2727
2746
|
const parsed = OptionsZod.safeParse(input);
|
|
@@ -2828,18 +2847,128 @@ function createPublisherHttpsTransport(input) {
|
|
|
2828
2847
|
}
|
|
2829
2848
|
|
|
2830
2849
|
// src/runtime/publisherRuntime.ts
|
|
2831
|
-
import {
|
|
2832
|
-
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
2850
|
+
import { isDeepStrictEqual as isDeepStrictEqual4 } from "node:util";
|
|
2833
2851
|
import { createMemoryLocator, PeriodicActor } from "@ariestools/actor";
|
|
2834
2852
|
import { creatable } from "@ariestools/sdk";
|
|
2835
|
-
import {
|
|
2853
|
+
import { BestEffortAdmissionAcknowledgmentZod } from "@xyo-network/event-kit-protocol";
|
|
2854
|
+
import { BestEffortDeliveryPolicyZod, WakeDeliveryModeZod } from "@xyo-network/event-kit-schemas";
|
|
2836
2855
|
import { z as z3 } from "zod";
|
|
2837
2856
|
|
|
2838
|
-
// src/runtime/
|
|
2857
|
+
// src/runtime/publisherDeliveryState.ts
|
|
2839
2858
|
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
2859
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
2860
|
+
import { WakeEnvelopeSchema, WakeEnvelopeZod } from "@xyo-network/event-kit-schemas";
|
|
2861
|
+
function assertDeliveryState(record, subscription) {
|
|
2862
|
+
if (!isDeepStrictEqual3(record.subscription, subscription))
|
|
2863
|
+
throw new Error("Subscription revision changed without a new identity");
|
|
2864
|
+
if (!Number.isSafeInteger(record.cursor) || record.cursor < subscription.fromPosition - 1 || [record.delivered, record.failures, record.nextAttemptMs].some((value) => !(Number.isSafeInteger(value) && value >= 0)) || [record.attempts, record.superseded, record.abandoned, record.totalAttempts, record.lastHintAt, record.lastNotifiedAt, record.lastHintTarget].some((value) => value !== void 0 && !(Number.isSafeInteger(value) && value >= 0)) || !["active", "blocked", "cancelled"].includes(record.state))
|
|
2865
|
+
throw new Error("Persisted publisher delivery state is invalid");
|
|
2866
|
+
if (record.pending === void 0) return;
|
|
2867
|
+
const pending = WakeEnvelopeZod.parse(record.pending);
|
|
2868
|
+
if (subscription.deliveryMode === "durable" && pending.fromPosition !== record.cursor + 1 || pending.deliveryMode !== subscription.deliveryMode || pending.toPosition - pending.fromPosition + 1 > subscription.maxPositionsPerWake || pending.authorizationChainId !== subscription.authorizationChainId || pending.deploymentId !== subscription.deploymentId || pending.queueId !== subscription.queueId || pending.filterHash !== subscription.filterHash || !isDeepStrictEqual3(pending.source, subscription.source))
|
|
2869
|
+
throw new Error("Persisted publisher outbox binding is invalid");
|
|
2870
|
+
}
|
|
2871
|
+
function makeEvent(subscription, fromPosition, toPosition, now, wakeId = randomBytes5(16).toString("hex")) {
|
|
2872
|
+
return WakeEnvelopeZod.parse({
|
|
2873
|
+
schema: WakeEnvelopeSchema,
|
|
2874
|
+
authorizationChainId: subscription.authorizationChainId,
|
|
2875
|
+
deliveryMode: subscription.deliveryMode,
|
|
2876
|
+
deploymentId: subscription.deploymentId,
|
|
2877
|
+
filterHash: subscription.filterHash,
|
|
2878
|
+
fromPosition,
|
|
2879
|
+
issuedAt: now,
|
|
2880
|
+
source: subscription.source,
|
|
2881
|
+
toPosition,
|
|
2882
|
+
wakeId,
|
|
2883
|
+
...subscription.queueId === void 0 ? {} : { queueId: subscription.queueId }
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
var DeliveryResponseError = class extends Error {
|
|
2887
|
+
retryAfterMs;
|
|
2888
|
+
status;
|
|
2889
|
+
constructor(status, retryAfterMs) {
|
|
2890
|
+
super("Publisher admission failed");
|
|
2891
|
+
this.status = status;
|
|
2892
|
+
this.retryAfterMs = retryAfterMs;
|
|
2893
|
+
}
|
|
2894
|
+
get retryable() {
|
|
2895
|
+
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
function prepareBestEffortDelivery(options) {
|
|
2899
|
+
const {
|
|
2900
|
+
bestEffort,
|
|
2901
|
+
first,
|
|
2902
|
+
heads,
|
|
2903
|
+
now,
|
|
2904
|
+
record,
|
|
2905
|
+
state
|
|
2906
|
+
} = options;
|
|
2907
|
+
const subscription = record.subscription;
|
|
2908
|
+
const head = heads.get(publisherSourceKey(subscription.source));
|
|
2909
|
+
const prior = record.pending;
|
|
2910
|
+
const expired = isHintExpired(record, now, bestEffort);
|
|
2911
|
+
if (head === void 0 || head instanceof Error || head.position < subscription.fromPosition) {
|
|
2912
|
+
if (prior !== void 0 && !expired) return { ...record, pending: prior };
|
|
2913
|
+
if (prior !== void 0) state.set("best-effort-deliveries", subscription.subscriptionHash, {
|
|
2914
|
+
...record,
|
|
2915
|
+
pending: void 0,
|
|
2916
|
+
abandoned: (record.abandoned ?? 0) + 1
|
|
2917
|
+
});
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
const refresh = refreshDue(record, now, bestEffort);
|
|
2921
|
+
if (freshHintRequired(record, head.position, first, refresh, expired)) {
|
|
2922
|
+
const pending = makeEvent(subscription, head.position, head.position, now);
|
|
2923
|
+
const prepared = {
|
|
2924
|
+
...record,
|
|
2925
|
+
pending,
|
|
2926
|
+
attempts: 0,
|
|
2927
|
+
lastHintAt: now,
|
|
2928
|
+
lastHintTarget: head.position,
|
|
2929
|
+
superseded: (record.superseded ?? 0) + (prior !== void 0 && !expired ? 1 : 0),
|
|
2930
|
+
abandoned: (record.abandoned ?? 0) + (expired ? 1 : 0),
|
|
2931
|
+
nextAttemptMs: 0,
|
|
2932
|
+
state: "active"
|
|
2933
|
+
};
|
|
2934
|
+
state.set("best-effort-deliveries", subscription.subscriptionHash, prepared);
|
|
2935
|
+
return prepared;
|
|
2936
|
+
}
|
|
2937
|
+
if (expired) state.set("best-effort-deliveries", subscription.subscriptionHash, {
|
|
2938
|
+
...record,
|
|
2939
|
+
pending: void 0,
|
|
2940
|
+
abandoned: (record.abandoned ?? 0) + 1
|
|
2941
|
+
});
|
|
2942
|
+
return prior === void 0 || expired ? void 0 : { ...record, pending: prior };
|
|
2943
|
+
}
|
|
2944
|
+
function isHintExpired(record, now, policy) {
|
|
2945
|
+
return record.pending !== void 0 && (now > record.pending.issuedAt + policy.maxHintAgeSeconds || (record.attempts ?? 0) >= policy.maxAttempts);
|
|
2946
|
+
}
|
|
2947
|
+
function refreshDue(record, now, policy) {
|
|
2948
|
+
return now - (record.lastHintAt ?? record.lastNotifiedAt ?? 0) >= policy.refreshIntervalSeconds;
|
|
2949
|
+
}
|
|
2950
|
+
function freshHintRequired(record, head, first, refresh, expired) {
|
|
2951
|
+
return first || head > (record.pending?.toPosition ?? record.lastHintTarget ?? record.cursor) || refresh && (record.pending === void 0 || expired);
|
|
2952
|
+
}
|
|
2953
|
+
function failedDelivery(record, current, mode, error, now) {
|
|
2954
|
+
const bestEffort = mode === "best-effort";
|
|
2955
|
+
const wait = error instanceof DeliveryResponseError && error.retryAfterMs !== void 0 ? error.retryAfterMs : Math.min(3e5, 1e3 * 2 ** Math.min(record.failures, 8));
|
|
2956
|
+
return {
|
|
2957
|
+
...record,
|
|
2958
|
+
failures: record.failures + 1,
|
|
2959
|
+
totalAttempts: bestEffort ? current?.totalAttempts ?? record.totalAttempts ?? 0 : record.totalAttempts,
|
|
2960
|
+
attempts: bestEffort ? current?.attempts ?? record.attempts ?? 0 : record.attempts,
|
|
2961
|
+
state: !bestEffort && error instanceof DeliveryResponseError && !error.retryable ? "blocked" : "active",
|
|
2962
|
+
nextAttemptMs: now * 1e3 + Math.min(36e5, wait),
|
|
2963
|
+
lastError: "delivery-failed"
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/runtime/sourceSigner.ts
|
|
2968
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
2840
2969
|
import {
|
|
2841
2970
|
NormalizedAddressZod as NormalizedAddressZod4,
|
|
2842
|
-
WakeEnvelopeZod,
|
|
2971
|
+
WakeEnvelopeZod as WakeEnvelopeZod2,
|
|
2843
2972
|
WakeJwtSchema
|
|
2844
2973
|
} from "@xyo-network/event-kit-schemas";
|
|
2845
2974
|
import { PayloadBuilder as PayloadBuilder2 } from "@xyo-network/sdk";
|
|
@@ -2849,14 +2978,14 @@ function createAccountSourceWakeSigner(options) {
|
|
|
2849
2978
|
throw new Error("Publisher signing account mismatch");
|
|
2850
2979
|
return {
|
|
2851
2980
|
async sign(input, audience) {
|
|
2852
|
-
const event =
|
|
2981
|
+
const event = WakeEnvelopeZod2.parse(input);
|
|
2853
2982
|
const eventHash = await PayloadBuilder2.dataHash(event);
|
|
2854
2983
|
const now = options.nowSeconds?.() ?? Math.floor(Date.now() / 1e3);
|
|
2855
2984
|
if (!Number.isSafeInteger(now) || now < 0)
|
|
2856
2985
|
throw new Error("Publisher signing clock is invalid");
|
|
2857
2986
|
const { token: jwt } = await options.account.signJwt({
|
|
2858
2987
|
audience,
|
|
2859
|
-
claims: { eventHash, jti:
|
|
2988
|
+
claims: { eventHash, jti: randomBytes6(16).toString("hex") },
|
|
2860
2989
|
now,
|
|
2861
2990
|
schema: WakeJwtSchema,
|
|
2862
2991
|
ttl: 60
|
|
@@ -2875,31 +3004,6 @@ function createAccountSourceWakeSigner(options) {
|
|
|
2875
3004
|
function activationReady(status) {
|
|
2876
3005
|
return (status?.keyLoaded ?? true) && (status?.pendingSubscriptions ?? 0) === 0;
|
|
2877
3006
|
}
|
|
2878
|
-
function assertDeliveryState(record, subscription) {
|
|
2879
|
-
if (!isDeepStrictEqual2(record.subscription, subscription))
|
|
2880
|
-
throw new Error("Subscription revision changed without a new identity");
|
|
2881
|
-
if (!Number.isSafeInteger(record.cursor) || record.cursor < subscription.fromPosition - 1 || [record.delivered, record.failures, record.nextAttemptMs].some((value) => !(Number.isSafeInteger(value) && value >= 0)) || !["active", "blocked", "cancelled"].includes(record.state))
|
|
2882
|
-
throw new Error("Persisted publisher delivery state is invalid");
|
|
2883
|
-
if (record.pending !== void 0) {
|
|
2884
|
-
const pending = WakeEnvelopeZod2.parse(record.pending);
|
|
2885
|
-
if (pending.fromPosition !== record.cursor + 1 || pending.toPosition - pending.fromPosition + 1 > subscription.maxPositionsPerWake || pending.authorizationChainId !== subscription.authorizationChainId || pending.deploymentId !== subscription.deploymentId || pending.queueId !== subscription.queueId || pending.filterHash !== subscription.filterHash || !isDeepStrictEqual2(pending.source, subscription.source))
|
|
2886
|
-
throw new Error("Persisted publisher outbox binding is invalid");
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
function makeEvent(subscription, fromPosition, toPosition, now, wakeId = randomBytes6(16).toString("hex")) {
|
|
2890
|
-
return WakeEnvelopeZod2.parse({
|
|
2891
|
-
schema: WakeEnvelopeSchema,
|
|
2892
|
-
authorizationChainId: subscription.authorizationChainId,
|
|
2893
|
-
deploymentId: subscription.deploymentId,
|
|
2894
|
-
filterHash: subscription.filterHash,
|
|
2895
|
-
fromPosition,
|
|
2896
|
-
issuedAt: now,
|
|
2897
|
-
source: subscription.source,
|
|
2898
|
-
toPosition,
|
|
2899
|
-
wakeId,
|
|
2900
|
-
...subscription.queueId === void 0 ? {} : { queueId: subscription.queueId }
|
|
2901
|
-
});
|
|
2902
|
-
}
|
|
2903
3007
|
var PublisherRuntimeActor = class extends PeriodicActor {
|
|
2904
3008
|
async runPass(context) {
|
|
2905
3009
|
await this.params.tick(context.signal);
|
|
@@ -2914,20 +3018,24 @@ var TimingZod = z3.object({
|
|
|
2914
3018
|
maxSubscriptions: z3.number().int().min(1).max(1024).default(64),
|
|
2915
3019
|
requestTimeoutMs: z3.number().int().min(100).max(6e4).default(1e4)
|
|
2916
3020
|
});
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
};
|
|
3021
|
+
function runtimeSettings(options) {
|
|
3022
|
+
const deliveryMode = WakeDeliveryModeZod.parse(options.deliveryMode);
|
|
3023
|
+
const bestEffort = options.bestEffort === void 0 ? void 0 : BestEffortDeliveryPolicyZod.parse(options.bestEffort);
|
|
3024
|
+
if (deliveryMode === "best-effort" !== (bestEffort !== void 0)) throw new TypeError("Explicit delivery mode requires its exact policy");
|
|
3025
|
+
return {
|
|
3026
|
+
bestEffort,
|
|
3027
|
+
deliveryMode,
|
|
3028
|
+
deliveryNamespace: deliveryMode === "best-effort" ? "best-effort-deliveries" : "deliveries",
|
|
3029
|
+
timing: TimingZod.parse(options)
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
2929
3032
|
async function createPersistentEventPublisherRuntime(options) {
|
|
2930
|
-
const
|
|
3033
|
+
const {
|
|
3034
|
+
bestEffort,
|
|
3035
|
+
deliveryMode,
|
|
3036
|
+
deliveryNamespace,
|
|
3037
|
+
timing
|
|
3038
|
+
} = runtimeSettings(options);
|
|
2931
3039
|
if (options.sources.length < 1 || options.sources.length > 16)
|
|
2932
3040
|
throw new Error("Publisher requires between one and sixteen bounded sources");
|
|
2933
3041
|
const clock = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
@@ -2939,7 +3047,12 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2939
3047
|
const sourceReaders = new Map(options.sources.map((reader) => [publisherSourceKey(reader.source), reader]));
|
|
2940
3048
|
if (sourceReaders.size !== options.sources.length)
|
|
2941
3049
|
throw new Error("Duplicate publisher source identity");
|
|
3050
|
+
const state = options.state;
|
|
3051
|
+
const existingMode = state.get("metadata", "runtime-delivery-mode");
|
|
3052
|
+
if (existingMode !== void 0 && existingMode !== deliveryMode) throw new Error("Publisher state delivery mode mismatch; preserve custody and use an explicitly qualified state binding");
|
|
3053
|
+
state.set("metadata", "runtime-delivery-mode", deliveryMode);
|
|
2942
3054
|
const abort = new AbortController();
|
|
3055
|
+
const seenThisProcess = /* @__PURE__ */ new Set();
|
|
2943
3056
|
let running = false;
|
|
2944
3057
|
let ready = false;
|
|
2945
3058
|
let clockHealthy = true;
|
|
@@ -2947,11 +3060,16 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2947
3060
|
let lastError;
|
|
2948
3061
|
let sourcesHealthy = false;
|
|
2949
3062
|
let stop;
|
|
2950
|
-
const state = options.state;
|
|
2951
3063
|
const timed = (promise, signal) => boundedPublisherOperation(promise, signal, timing.requestTimeoutMs);
|
|
2952
3064
|
function loadDelivery(subscription, now) {
|
|
3065
|
+
if (subscription.deliveryMode !== deliveryMode || !isDeepStrictEqual4(subscription.bestEffort, bestEffort))
|
|
3066
|
+
throw new Error("Subscription delivery mode or policy disagrees with runtime");
|
|
3067
|
+
if (deliveryMode === "best-effort" && (subscription.maxPositionsPerWake !== 1 || subscription.serviceHash === void 0)) throw new Error("Best Effort requires point range");
|
|
3068
|
+
if (options.serviceHash !== void 0 && subscription.serviceHash !== options.serviceHash)
|
|
3069
|
+
throw new Error("Subscription does not match the configured exact service descriptor");
|
|
2953
3070
|
const key = subscription.subscriptionHash;
|
|
2954
|
-
|
|
3071
|
+
if (deliveryMode === "best-effort" && state.get(deliveryNamespace, key) === void 0 && state.entries(deliveryNamespace).length >= timing.maxSubscriptions) throw new Error("Best Effort state capacity exceeded; live records are not evicted");
|
|
3072
|
+
let record = state.get(deliveryNamespace, key);
|
|
2955
3073
|
if (record === void 0) {
|
|
2956
3074
|
makeEvent(subscription, subscription.fromPosition, subscription.fromPosition, now, "0".repeat(32));
|
|
2957
3075
|
if (!Number.isSafeInteger(subscription.maxPositionsPerWake) || subscription.maxPositionsPerWake < 1 || subscription.maxPositionsPerWake > 4096) {
|
|
@@ -2965,12 +3083,26 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2965
3083
|
nextAttemptMs: 0,
|
|
2966
3084
|
state: "active"
|
|
2967
3085
|
};
|
|
2968
|
-
state.set(
|
|
3086
|
+
state.set(deliveryNamespace, key, record);
|
|
2969
3087
|
}
|
|
2970
3088
|
assertDeliveryState(record, subscription);
|
|
2971
3089
|
return record;
|
|
2972
3090
|
}
|
|
2973
3091
|
function prepareDelivery(record, now, heads) {
|
|
3092
|
+
if (deliveryMode === "best-effort" && bestEffort !== void 0) {
|
|
3093
|
+
const first = !seenThisProcess.has(record.subscription.subscriptionHash);
|
|
3094
|
+
const head2 = heads.get(publisherSourceKey(record.subscription.source));
|
|
3095
|
+
if (head2 !== void 0 && !(head2 instanceof Error) && head2.position >= record.subscription.fromPosition)
|
|
3096
|
+
seenThisProcess.add(record.subscription.subscriptionHash);
|
|
3097
|
+
return prepareBestEffortDelivery({
|
|
3098
|
+
bestEffort,
|
|
3099
|
+
first,
|
|
3100
|
+
heads,
|
|
3101
|
+
now,
|
|
3102
|
+
record,
|
|
3103
|
+
state
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
2974
3106
|
if (record.pending !== void 0) return { ...record, pending: record.pending };
|
|
2975
3107
|
const subscription = record.subscription;
|
|
2976
3108
|
const head = heads.get(publisherSourceKey(subscription.source));
|
|
@@ -2982,43 +3114,74 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2982
3114
|
state.transaction(() => {
|
|
2983
3115
|
const logicalKey = JSON.stringify([subscription.subscriptionHash, pending.fromPosition, pending.toPosition]);
|
|
2984
3116
|
if (!state.insert("delivery-events", logicalKey, pending)) throw new Error("Publisher logical range already exists outside its contiguous cursor");
|
|
2985
|
-
state.set(
|
|
3117
|
+
state.set(deliveryNamespace, subscription.subscriptionHash, prepared);
|
|
2986
3118
|
});
|
|
2987
3119
|
return prepared;
|
|
2988
3120
|
}
|
|
2989
3121
|
async function stillActive(record, signal) {
|
|
2990
3122
|
if (await timed(options.discovery.isActive(record.subscription, signal), signal)) return true;
|
|
2991
|
-
state.set(
|
|
3123
|
+
state.set(deliveryNamespace, record.subscription.subscriptionHash, {
|
|
2992
3124
|
...record,
|
|
2993
3125
|
pending: void 0,
|
|
2994
3126
|
state: "cancelled"
|
|
2995
3127
|
});
|
|
2996
3128
|
return false;
|
|
2997
3129
|
}
|
|
3130
|
+
function acknowledgeBestEffort(record, body, eventHash, signal, now) {
|
|
3131
|
+
const { pending, subscription } = record;
|
|
3132
|
+
const acknowledgment = BestEffortAdmissionAcknowledgmentZod.safeParse(body);
|
|
3133
|
+
if (!acknowledgment.success || acknowledgment.data.eventHash !== eventHash || acknowledgment.data.wakeId !== pending.wakeId || acknowledgment.data.expiresAt !== pending.issuedAt + (bestEffort?.maxHintAgeSeconds ?? 0) + 60 || acknowledgment.data.acceptedAt < pending.issuedAt - 60 || acknowledgment.data.acceptedAt > now + 60)
|
|
3134
|
+
throw new Error("Publisher did not receive a matching finite acknowledgment");
|
|
3135
|
+
signal.throwIfAborted();
|
|
3136
|
+
state.set(deliveryNamespace, subscription.subscriptionHash, {
|
|
3137
|
+
subscription,
|
|
3138
|
+
cursor: pending.toPosition,
|
|
3139
|
+
delivered: record.delivered + 1,
|
|
3140
|
+
failures: 0,
|
|
3141
|
+
attempts: 0,
|
|
3142
|
+
superseded: record.superseded ?? 0,
|
|
3143
|
+
abandoned: record.abandoned ?? 0,
|
|
3144
|
+
totalAttempts: (record.totalAttempts ?? 0) + 1,
|
|
3145
|
+
lastNotifiedAt: now,
|
|
3146
|
+
lastHintAt: pending.issuedAt,
|
|
3147
|
+
lastHintTarget: pending.toPosition,
|
|
3148
|
+
nextAttemptMs: 0,
|
|
3149
|
+
state: "active"
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
2998
3152
|
async function sendDelivery(record, signal, now) {
|
|
2999
3153
|
const { pending, subscription } = record;
|
|
3000
3154
|
signal.throwIfAborted();
|
|
3001
3155
|
const signed = await signer.sign(pending, subscription.audience);
|
|
3002
3156
|
signal.throwIfAborted();
|
|
3003
|
-
const attempt = state.get("delivery-attempts", pending.wakeId)?.count ?? 0;
|
|
3004
|
-
state.set(
|
|
3157
|
+
const attempt = deliveryMode === "best-effort" ? record.attempts ?? 0 : state.get("delivery-attempts", pending.wakeId)?.count ?? 0;
|
|
3158
|
+
if (deliveryMode === "best-effort") state.set(deliveryNamespace, subscription.subscriptionHash, {
|
|
3159
|
+
...record,
|
|
3160
|
+
attempts: attempt + 1,
|
|
3161
|
+
totalAttempts: (record.totalAttempts ?? 0) + 1
|
|
3162
|
+
});
|
|
3163
|
+
else state.set("delivery-attempts", pending.wakeId, {
|
|
3005
3164
|
count: attempt + 1,
|
|
3006
3165
|
startedAt: now,
|
|
3007
3166
|
status: "in-flight"
|
|
3008
3167
|
});
|
|
3009
3168
|
const response = await options.transport.post(subscription.endpoint, signed.event, signal, signed.jwt);
|
|
3010
|
-
state.set("delivery-attempts", pending.wakeId, {
|
|
3169
|
+
if (deliveryMode === "durable") state.set("delivery-attempts", pending.wakeId, {
|
|
3011
3170
|
count: attempt + 1,
|
|
3012
3171
|
startedAt: now,
|
|
3013
3172
|
status: response.status
|
|
3014
3173
|
});
|
|
3015
3174
|
if (response.status !== 200 && response.status !== 202) throw new DeliveryResponseError(response.status, response.retryAfterMs);
|
|
3175
|
+
if (deliveryMode === "best-effort") {
|
|
3176
|
+
acknowledgeBestEffort(record, response.body, signed.eventHash, signal, now);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3016
3179
|
const receipt = z3.object({ receiptId: z3.string().min(1), wakeId: z3.literal(pending.wakeId) }).safeParse(response.body);
|
|
3017
3180
|
if (!receipt.success) throw new Error("Publisher admission did not return a matching durable receipt");
|
|
3018
3181
|
signal.throwIfAborted();
|
|
3019
3182
|
state.transaction(() => {
|
|
3020
3183
|
state.set("delivery-receipts", pending.wakeId, { receiptId: receipt.data.receiptId, receivedAt: now });
|
|
3021
|
-
state.set(
|
|
3184
|
+
state.set(deliveryNamespace, subscription.subscriptionHash, {
|
|
3022
3185
|
subscription,
|
|
3023
3186
|
cursor: pending.toPosition,
|
|
3024
3187
|
delivered: record.delivered + 1,
|
|
@@ -3030,7 +3193,7 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3030
3193
|
}
|
|
3031
3194
|
async function deliver(subscription, signal, now, heads) {
|
|
3032
3195
|
let record = loadDelivery(subscription, now);
|
|
3033
|
-
if (record.state !== "active" || record.nextAttemptMs > now * 1e3) return;
|
|
3196
|
+
if (deliveryMode === "durable" && (record.state !== "active" || record.nextAttemptMs > now * 1e3)) return;
|
|
3034
3197
|
try {
|
|
3035
3198
|
if (!await stillActive(record, signal)) return;
|
|
3036
3199
|
options.transport.assertAllowed(subscription.endpoint);
|
|
@@ -3038,17 +3201,16 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3038
3201
|
const prepared = prepareDelivery(record, now, heads);
|
|
3039
3202
|
if (prepared === void 0) return;
|
|
3040
3203
|
record = prepared;
|
|
3204
|
+
if (record.nextAttemptMs > now * 1e3) return;
|
|
3041
3205
|
if (!await stillActive(record, signal)) return;
|
|
3042
3206
|
await sendDelivery(prepared, signal, now);
|
|
3043
3207
|
} catch (error) {
|
|
3044
3208
|
if (signal.aborted) return;
|
|
3045
|
-
state.set(
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
state
|
|
3049
|
-
|
|
3050
|
-
lastError: "delivery-failed"
|
|
3051
|
-
});
|
|
3209
|
+
state.set(
|
|
3210
|
+
deliveryNamespace,
|
|
3211
|
+
subscription.subscriptionHash,
|
|
3212
|
+
failedDelivery(record, state.get(deliveryNamespace, subscription.subscriptionHash), deliveryMode, error, now)
|
|
3213
|
+
);
|
|
3052
3214
|
}
|
|
3053
3215
|
}
|
|
3054
3216
|
async function readSources(signal) {
|
|
@@ -3078,13 +3240,13 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3078
3240
|
if (subscriptions.length > timing.maxSubscriptions) throw new Error("Publisher subscription limit exceeded");
|
|
3079
3241
|
const active = new Set(subscriptions.map((subscription) => subscription.subscriptionHash));
|
|
3080
3242
|
if (active.size !== subscriptions.length) throw new Error("Duplicate subscription identity");
|
|
3081
|
-
const unavailable = state.entries(
|
|
3243
|
+
const unavailable = state.entries(deliveryNamespace).filter((item) => !active.has(item.key) && item.value.state !== "cancelled");
|
|
3082
3244
|
let available = true;
|
|
3083
3245
|
for (let offset = 0; offset < unavailable.length; offset += timing.concurrency) {
|
|
3084
3246
|
signal.throwIfAborted();
|
|
3085
3247
|
const results = await Promise.allSettled(unavailable.slice(offset, offset + timing.concurrency).map(async ({ key, value }) => {
|
|
3086
3248
|
const stillAuthorized = await timed(options.discovery.isActive(value.subscription, signal), signal);
|
|
3087
|
-
if (!stillAuthorized) state.set(
|
|
3249
|
+
if (!stillAuthorized) state.set(deliveryNamespace, key, {
|
|
3088
3250
|
...value,
|
|
3089
3251
|
pending: void 0,
|
|
3090
3252
|
state: "cancelled"
|
|
@@ -3142,10 +3304,11 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3142
3304
|
async start() {
|
|
3143
3305
|
if (stop !== void 0)
|
|
3144
3306
|
throw new Error("Publisher is stopped");
|
|
3145
|
-
if (
|
|
3146
|
-
|
|
3147
|
-
running = true;
|
|
3307
|
+
if (running) {
|
|
3308
|
+
return;
|
|
3148
3309
|
}
|
|
3310
|
+
await actor.start();
|
|
3311
|
+
running = true;
|
|
3149
3312
|
},
|
|
3150
3313
|
async runOnce() {
|
|
3151
3314
|
if (stop !== void 0)
|
|
@@ -3177,9 +3340,14 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3177
3340
|
throw new AggregateError(failures, "Publisher runtime shutdown failed");
|
|
3178
3341
|
})(),
|
|
3179
3342
|
status() {
|
|
3180
|
-
const deliveries = state.entries(
|
|
3343
|
+
const deliveries = state.entries(deliveryNamespace).map((item) => item.value);
|
|
3181
3344
|
return {
|
|
3182
3345
|
running,
|
|
3346
|
+
deliveryMode,
|
|
3347
|
+
...bestEffort === void 0 ? {} : { bestEffort: Object.freeze({ ...bestEffort }) },
|
|
3348
|
+
superseded: deliveries.reduce((sum, item) => sum + (item.superseded ?? 0), 0),
|
|
3349
|
+
abandoned: deliveries.reduce((sum, item) => sum + (item.abandoned ?? 0), 0),
|
|
3350
|
+
totalAttempts: deliveries.reduce((sum, item) => sum + (item.totalAttempts ?? 0), 0),
|
|
3183
3351
|
ready: running && ready,
|
|
3184
3352
|
clockHealthy,
|
|
3185
3353
|
degraded: !ready || deliveries.some((item) => item.state !== "cancelled" && item.failures > 0),
|
|
@@ -3191,10 +3359,15 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3191
3359
|
passes,
|
|
3192
3360
|
...lastError === void 0 ? {} : { lastError },
|
|
3193
3361
|
...options.discovery.status === void 0 ? {} : { handshake: options.discovery.status() },
|
|
3194
|
-
sources: state.entries("source-heads").map((item) => ({ id: item.key, ...item.value })),
|
|
3195
|
-
subscriptions: state.entries(
|
|
3362
|
+
sources: state.entries("source-heads").filter((item) => sourceReaders.has(item.key)).map((item) => ({ id: item.key, ...item.value })),
|
|
3363
|
+
subscriptions: state.entries(deliveryNamespace).map((item) => ({
|
|
3196
3364
|
id: item.key,
|
|
3197
3365
|
cursor: item.value.cursor,
|
|
3366
|
+
...deliveryMode === "best-effort" ? {
|
|
3367
|
+
...item.value.cursor < item.value.subscription.fromPosition ? {} : { lastAdmissionTarget: item.value.cursor },
|
|
3368
|
+
attempts: item.value.attempts ?? 0,
|
|
3369
|
+
lastHintAt: item.value.lastHintAt
|
|
3370
|
+
} : {},
|
|
3198
3371
|
state: item.value.state,
|
|
3199
3372
|
pending: item.value.pending !== void 0,
|
|
3200
3373
|
failures: item.value.failures
|
|
@@ -3285,7 +3458,7 @@ async function openPublisherDataLake(options) {
|
|
|
3285
3458
|
}
|
|
3286
3459
|
|
|
3287
3460
|
// src/runtime/sqliteHandshakeStores.ts
|
|
3288
|
-
import { isDeepStrictEqual as
|
|
3461
|
+
import { isDeepStrictEqual as isDeepStrictEqual5 } from "node:util";
|
|
3289
3462
|
import { endpointChallengeHash as endpointChallengeHash2 } from "@xyo-network/event-kit-protocol";
|
|
3290
3463
|
import {
|
|
3291
3464
|
Base64Url32BytesZod as Base64Url32BytesZod3,
|
|
@@ -3306,7 +3479,7 @@ function requireTime2(value) {
|
|
|
3306
3479
|
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Publisher clock is invalid");
|
|
3307
3480
|
}
|
|
3308
3481
|
function samePublicationIntent(left, right) {
|
|
3309
|
-
return left.chainId === right.chainId && left.source === right.source && left.operationId === right.operationId &&
|
|
3482
|
+
return left.chainId === right.chainId && left.source === right.source && left.operationId === right.operationId && isDeepStrictEqual5(left.eventHashes, right.eventHashes) && isDeepStrictEqual5(left.attachmentHashes, right.attachmentHashes);
|
|
3310
3483
|
}
|
|
3311
3484
|
function publicationKey(intent) {
|
|
3312
3485
|
if (!/^[0-9a-f]{40}$/u.test(intent.chainId) || !/^[0-9a-f]{40}$/u.test(intent.source) || intent.operationId.length === 0 || intent.operationId.length > 256 || [...intent.eventHashes, ...intent.attachmentHashes].some((hash) => !HashZod5.safeParse(hash).success)) {
|
|
@@ -3314,11 +3487,12 @@ function publicationKey(intent) {
|
|
|
3314
3487
|
}
|
|
3315
3488
|
return JSON.stringify([intent.chainId, intent.source, intent.operationId]);
|
|
3316
3489
|
}
|
|
3317
|
-
function createSqlitePublisherHandshakeStores(state) {
|
|
3490
|
+
function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
3491
|
+
const replayKey = options.replayNamespace === void 0 ? "checkpoint" : `checkpoint:${z4.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u).parse(options.replayNamespace)}`;
|
|
3318
3492
|
const replay = {
|
|
3319
|
-
load: async () => state.get("replay",
|
|
3493
|
+
load: async () => state.get("replay", replayKey),
|
|
3320
3494
|
async save(checkpoint) {
|
|
3321
|
-
state.set("replay",
|
|
3495
|
+
state.set("replay", replayKey, checkpoint);
|
|
3322
3496
|
}
|
|
3323
3497
|
};
|
|
3324
3498
|
const intents = {
|
|
@@ -3356,7 +3530,7 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3356
3530
|
const key = `${record.publisher}:${record.keyId}`;
|
|
3357
3531
|
const prior = state.get("encryption-keys", key);
|
|
3358
3532
|
if (prior !== void 0) {
|
|
3359
|
-
if (!
|
|
3533
|
+
if (!isDeepStrictEqual5(prior, record)) throw new Error("Encryption key collision");
|
|
3360
3534
|
return "duplicate";
|
|
3361
3535
|
}
|
|
3362
3536
|
state.set("encryption-keys", key, record);
|
|
@@ -3376,7 +3550,7 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3376
3550
|
const priorNonce = state.get("challenge-nonces", challenge.nonce);
|
|
3377
3551
|
if (priorNonce !== void 0 && priorNonce !== hash) return { kind: "collision" };
|
|
3378
3552
|
const prior = current(hash);
|
|
3379
|
-
if (prior !== void 0) return
|
|
3553
|
+
if (prior !== void 0) return isDeepStrictEqual5(prior.challenge, challenge) ? { kind: "duplicate", record: prior } : { kind: "collision" };
|
|
3380
3554
|
const record = {
|
|
3381
3555
|
challenge,
|
|
3382
3556
|
challengeHash: hash,
|
|
@@ -3429,7 +3603,7 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3429
3603
|
for (const phase of phases) {
|
|
3430
3604
|
const record = state.get(`publication:${phase}`, key);
|
|
3431
3605
|
if (record === void 0) continue;
|
|
3432
|
-
if (record.phase !== phase || !samePublicationIntent(record, intent) || !
|
|
3606
|
+
if (record.phase !== phase || !samePublicationIntent(record, intent) || !isDeepStrictEqual5(record.evidence, signed.evidence)) {
|
|
3433
3607
|
throw new Error("Publication evidence collision");
|
|
3434
3608
|
}
|
|
3435
3609
|
latest = record;
|
|
@@ -3443,12 +3617,12 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3443
3617
|
state.transaction(() => {
|
|
3444
3618
|
const signed = state.get("publication:signed", key);
|
|
3445
3619
|
if (signed === void 0 && record.phase !== "signed") throw new Error("Publication must bind signed evidence first");
|
|
3446
|
-
if (signed !== void 0 && (!samePublicationIntent(signed, record) || !
|
|
3620
|
+
if (signed !== void 0 && (!samePublicationIntent(signed, record) || !isDeepStrictEqual5(signed.evidence, record.evidence))) {
|
|
3447
3621
|
throw new Error("Publication intent or evidence collision");
|
|
3448
3622
|
}
|
|
3449
3623
|
const namespace = `publication:${record.phase}`;
|
|
3450
3624
|
const prior = state.get(namespace, key);
|
|
3451
|
-
if (prior !== void 0 && !
|
|
3625
|
+
if (prior !== void 0 && !isDeepStrictEqual5(prior, record)) throw new Error("Publication phase collision");
|
|
3452
3626
|
if (prior === void 0) state.set(namespace, key, record);
|
|
3453
3627
|
});
|
|
3454
3628
|
}
|
|
@@ -3474,9 +3648,7 @@ import { hostname } from "node:os";
|
|
|
3474
3648
|
import PATH12 from "node:path";
|
|
3475
3649
|
function decode(value) {
|
|
3476
3650
|
return JSON.parse(value, (_key, candidate) => {
|
|
3477
|
-
|
|
3478
|
-
return BigInt(candidate.$eventKitBigInt);
|
|
3479
|
-
return candidate;
|
|
3651
|
+
return typeof candidate === "object" && candidate !== null && Object.keys(candidate).length === 1 && "$eventKitBigInt" in candidate && typeof candidate.$eventKitBigInt === "string" && /^(?:0|[1-9][0-9]*)$/u.test(candidate.$eventKitBigInt) ? BigInt(candidate.$eventKitBigInt) : candidate;
|
|
3480
3652
|
});
|
|
3481
3653
|
}
|
|
3482
3654
|
function processExists(pid) {
|