@llblab/pi-telegram 0.52.0 → 0.52.2
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/AGENTS.md +6 -5
- package/BACKLOG.md +18 -1
- package/CHANGELOG.md +12 -0
- package/README.md +5 -2
- package/dist/lib/bindings.js +1 -0
- package/dist/lib/bus-api.js +4 -3
- package/dist/lib/extension.js +4 -0
- package/dist/lib/journal.d.ts +7 -1
- package/dist/lib/journal.js +72 -11
- package/dist/lib/locks.d.ts +5 -0
- package/dist/lib/locks.js +29 -11
- package/dist/lib/polling.d.ts +2 -0
- package/dist/lib/polling.js +12 -2
- package/dist/lib/queue.d.ts +2 -0
- package/dist/lib/queue.js +25 -3
- package/dist/lib/routing.d.ts +1 -0
- package/dist/lib/routing.js +118 -44
- package/dist/lib/sync.d.ts +5 -0
- package/dist/lib/sync.js +48 -5
- package/dist/lib/telegram-api.d.ts +2 -0
- package/dist/lib/telegram-api.js +22 -1
- package/dist/lib/threads.d.ts +6 -4
- package/dist/lib/threads.js +45 -3
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +34 -18
- package/dist/package.json +1 -1
- package/docs/architecture.md +14 -8
- package/docs/ui-style.md +1 -1
- package/lib/bindings.ts +1 -0
- package/lib/bus-api.ts +4 -2
- package/lib/extension.ts +5 -0
- package/lib/journal.ts +80 -11
- package/lib/locks.ts +28 -11
- package/lib/polling.ts +21 -2
- package/lib/queue.ts +26 -3
- package/lib/routing.ts +91 -36
- package/lib/sync.ts +47 -5
- package/lib/telegram-api.ts +26 -1
- package/lib/threads.ts +46 -7
- package/lib/updates.ts +43 -21
- package/package.json +1 -1
package/lib/threads.ts
CHANGED
|
@@ -332,7 +332,7 @@ export interface TelegramTemporaryThreadInput {
|
|
|
332
332
|
journalBindingKey: string;
|
|
333
333
|
updateIds: number[];
|
|
334
334
|
}
|
|
335
|
-
/**
|
|
335
|
+
/** Journal-owned donor discard evidence: retained manual cancellation or body-free chooser expiry, never recipient cancellation. */
|
|
336
336
|
export interface TelegramTemporaryThreadCancellationEvidence {
|
|
337
337
|
journalBindingKey: string;
|
|
338
338
|
updateId: number;
|
|
@@ -343,7 +343,7 @@ export interface TelegramTemporaryThreadEntry {
|
|
|
343
343
|
source: { journalBindingKey: string; updateId: number };
|
|
344
344
|
/** Append-only known source groups. Missing legacy metadata never proves that the creation source was alone. */
|
|
345
345
|
inputs?: TelegramTemporaryThreadInput[];
|
|
346
|
-
/** Whole known groups with positively observed cancellation; never
|
|
346
|
+
/** Whole known groups with positively observed donor cancellation or expiry; never recipient cancellation or deletion authority. */
|
|
347
347
|
cancelledInputs?: TelegramTemporaryThreadInput[];
|
|
348
348
|
/** Whole known groups whose Forward was positively completed by the journal owner; never deletion authority alone. */
|
|
349
349
|
completedInputs?: TelegramTemporaryThreadInput[];
|
|
@@ -415,6 +415,10 @@ export interface TelegramWorkspaceRestore {
|
|
|
415
415
|
recordTemporaryThreadInputCancellation(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput,
|
|
416
416
|
authority: TelegramWorkspaceRestoreAuthority,
|
|
417
417
|
inspect: (updateId: number) => TelegramTemporaryThreadCancellationEvidence | undefined): TelegramTemporaryThreadEntry | undefined;
|
|
418
|
+
/** Body-free chooser expiry may terminate an uncertain donor Forward/Restore, never accepted recipient work or a bound target. */
|
|
419
|
+
recordTemporaryThreadInputExpiry(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput,
|
|
420
|
+
authority: TelegramWorkspaceRestoreAuthority,
|
|
421
|
+
inspect: (updateId: number) => TelegramTemporaryThreadCancellationEvidence | undefined): TelegramTemporaryThreadEntry | undefined;
|
|
418
422
|
/** Caller holds profile admission and proves fresh source/protection clearance; publication grants one attempt, never retry. */
|
|
419
423
|
issueTemporaryThreadCleanup(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority):
|
|
420
424
|
{ issued: true; entry: TelegramTemporaryThreadEntry } | undefined;
|
|
@@ -423,8 +427,8 @@ export interface TelegramWorkspaceRestore {
|
|
|
423
427
|
retireTemporaryThread(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority,
|
|
424
428
|
completed?: TelegramTemporaryThreadInput): TelegramTemporaryThreadEntry | undefined;
|
|
425
429
|
/** New-world restart: atomically forgets this operator's Restore intents and temporary entries from previous runtime instances.
|
|
426
|
-
* Committed bindings stay; nothing is rolled back, replayed or deleted here. */
|
|
427
|
-
forgetPreviousWorld(authority: TelegramWorkspaceRestoreAuthority):
|
|
430
|
+
* Caller may preserve exact unbound temporary tokens for clock-bearing sources. Committed bindings stay; nothing is rolled back, replayed or deleted here. */
|
|
431
|
+
forgetPreviousWorld(authority: TelegramWorkspaceRestoreAuthority, preserveTemporaryTokens?: readonly string[]):
|
|
428
432
|
{ operations: TelegramWorkspaceRestoreIntent[]; temporaryThreads: TelegramTemporaryThreadEntry[] } | undefined;
|
|
429
433
|
}
|
|
430
434
|
export interface TelegramWorkspaceRestoreOptions {
|
|
@@ -3595,6 +3599,40 @@ export function createTelegramTopicTargetStore(
|
|
|
3595
3599
|
});
|
|
3596
3600
|
return recorded && current() ? recorded : undefined;
|
|
3597
3601
|
},
|
|
3602
|
+
recordTemporaryThreadInputExpiry(expected, inputValue, authority, inspect) {
|
|
3603
|
+
const input = structuredClone(inputValue);
|
|
3604
|
+
if (!isTemporaryThreadInput(input) || typeof inspect !== "function") return undefined;
|
|
3605
|
+
const operator = authority.operatorUserId, executor = structuredClone(authority.executor);
|
|
3606
|
+
const bound = classifyTemporaryTarget(read(), expected).kind === "bound";
|
|
3607
|
+
const current = () => authority.isCurrent() && authority.operatorUserId === operator &&
|
|
3608
|
+
isDeepStrictEqual(authority.executor, executor) && input.updateIds.every(updateId => {
|
|
3609
|
+
const evidence = inspect(updateId);
|
|
3610
|
+
return evidence?.journalBindingKey === input.journalBindingKey && evidence.updateId === updateId &&
|
|
3611
|
+
evidence.operatorAuthorityId === `telegram-owner:${operator}`;
|
|
3612
|
+
}) && authority.isCurrent();
|
|
3613
|
+
return advanceTemporary(expected, { ...authority, isCurrent: current }, (entry, file) => {
|
|
3614
|
+
if (entry.phase !== "created" || !getTelegramTemporaryThreadInputs(entry).some(value => isDeepStrictEqual(value, input)) ||
|
|
3615
|
+
entry.completedInputs?.some(value => isDeepStrictEqual(value, input))) return false;
|
|
3616
|
+
const cancelled = entry.cancelledInputs ?? [];
|
|
3617
|
+
if (cancelled.some(value => isDeepStrictEqual(value, input))) return "unchanged";
|
|
3618
|
+
const operations = file.operations.filter(({ request }) => request.source.journalBindingKey === input.journalBindingKey &&
|
|
3619
|
+
request.source.updateIds.some(id => input.updateIds.includes(id)));
|
|
3620
|
+
if (operations.some(({ request }) => !request.source.updateIds.every(id => input.updateIds.includes(id)))) return false;
|
|
3621
|
+
// Forget only expired donor intent; canonical binding, recipient queue and immutable settlement proofs are untouched.
|
|
3622
|
+
file.operations = file.operations.filter(value => !operations.includes(value));
|
|
3623
|
+
// Once bound, this is no longer a disposable tab. Forget its temporary frame, not the binding or other journal sources.
|
|
3624
|
+
if (bound) {
|
|
3625
|
+
file.temporaryThreads = file.temporaryThreads?.filter(value => value !== entry);
|
|
3626
|
+
if (!file.temporaryThreads?.length) delete file.temporaryThreads;
|
|
3627
|
+
return true;
|
|
3628
|
+
}
|
|
3629
|
+
const forwarded = entry.forwardedInputs?.filter(value => !isDeepStrictEqual(value, input));
|
|
3630
|
+
if (forwarded?.length) entry.forwardedInputs = forwarded;
|
|
3631
|
+
else delete entry.forwardedInputs;
|
|
3632
|
+
entry.cancelledInputs = [...cancelled, input];
|
|
3633
|
+
return true;
|
|
3634
|
+
});
|
|
3635
|
+
},
|
|
3598
3636
|
recordTemporaryThreadForwardIssued(expected, inputValue, authority) {
|
|
3599
3637
|
const input = structuredClone(inputValue);
|
|
3600
3638
|
if (!isTemporaryThreadInput(input)) return undefined;
|
|
@@ -3644,7 +3682,7 @@ export function createTelegramTopicTargetStore(
|
|
|
3644
3682
|
return entry;
|
|
3645
3683
|
});
|
|
3646
3684
|
},
|
|
3647
|
-
forgetPreviousWorld(authority) {
|
|
3685
|
+
forgetPreviousWorld(authority, preserveTemporaryTokens = []) {
|
|
3648
3686
|
if (!authority.isCurrent()) return undefined;
|
|
3649
3687
|
const executor = structuredClone(authority.executor), operator = authority.operatorUserId;
|
|
3650
3688
|
if (!isTelegramWorkspaceRestoreExecutor(executor) || !restoreInteger(operator)) return undefined;
|
|
@@ -3652,11 +3690,12 @@ export function createTelegramTopicTargetStore(
|
|
|
3652
3690
|
const previous = (value: { operatorUserId: number; executor: TelegramWorkspaceRestoreExecutor }) =>
|
|
3653
3691
|
value.operatorUserId === operator && value.executor.instanceId !== executor.instanceId;
|
|
3654
3692
|
const before = storage.read(), file = structuredClone(before);
|
|
3655
|
-
const
|
|
3693
|
+
const forgetTemporary = (value: TelegramTemporaryThreadEntry) => previous(value) && !preserveTemporaryTokens.includes(value.token);
|
|
3694
|
+
const operations = file.operations.filter(previous), temporaryThreads = (file.temporaryThreads ?? []).filter(forgetTemporary);
|
|
3656
3695
|
if (!current()) return undefined;
|
|
3657
3696
|
if (!operations.length && !temporaryThreads.length) return { operations, temporaryThreads };
|
|
3658
3697
|
file.operations = file.operations.filter(value => !previous(value));
|
|
3659
|
-
const remaining = (file.temporaryThreads ?? []).filter(value => !
|
|
3698
|
+
const remaining = (file.temporaryThreads ?? []).filter(value => !forgetTemporary(value));
|
|
3660
3699
|
if (remaining.length) file.temporaryThreads = remaining;
|
|
3661
3700
|
else delete file.temporaryThreads;
|
|
3662
3701
|
if (file.revision === Number.MAX_SAFE_INTEGER) throw new Error("Workspace Restore revision exhausted.");
|
package/lib/updates.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
type TelegramUpdateJournalEntry,
|
|
57
57
|
type TelegramUpdateJournalRoutingInput,
|
|
58
58
|
type TelegramRoutingInputJournal,
|
|
59
|
+
type TelegramRoutingInputExpiryResult,
|
|
59
60
|
type TelegramUpdateJournalQueueDiscardResult,
|
|
60
61
|
type TelegramUpdateJournalQueueHandoffAcceptResult,
|
|
61
62
|
type TelegramUpdateJournalQueueHandoffCancelResult,
|
|
@@ -495,6 +496,8 @@ export interface TelegramHeldSourcePreparation<TContext> {
|
|
|
495
496
|
/** Exact prepared worker owner, context, key and authority; the consumer adds its own domain fences. */
|
|
496
497
|
isCurrent: () => boolean;
|
|
497
498
|
signal: AbortSignal;
|
|
499
|
+
/** Chooser-clock sources that must survive cold spending until their fixed expiry. */
|
|
500
|
+
routingSourceIds?: readonly number[];
|
|
498
501
|
}
|
|
499
502
|
|
|
500
503
|
export interface TelegramDeferredAbandonmentRecoveryPage {
|
|
@@ -1922,7 +1925,7 @@ export interface TelegramRoutingInputExpirySource {
|
|
|
1922
1925
|
original: TelegramUpdateJournalEntry;
|
|
1923
1926
|
journalBindingKey: string;
|
|
1924
1927
|
isCurrent(): boolean;
|
|
1925
|
-
expire():
|
|
1928
|
+
expire(): TelegramRoutingInputExpiryResult | undefined;
|
|
1926
1929
|
}
|
|
1927
1930
|
|
|
1928
1931
|
export interface TelegramUpdateWorkerJournalPort {
|
|
@@ -2035,7 +2038,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
|
|
|
2035
2038
|
getRecipientBindingKey?: () => string | undefined;
|
|
2036
2039
|
/** True selects legacy historical review/spending; retain protects unsupported originals without disposition authority. */
|
|
2037
2040
|
shouldReviewHistoricalInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | "retain" | Promise<boolean | "retain">;
|
|
2038
|
-
/** New-world restart: spend
|
|
2041
|
+
/** New-world restart: spend unclocked classified routing input without delivery, copy or completion. Chooser clocks wait for expiry; interrupted private abandonment keeps exact recovery. */
|
|
2039
2042
|
spendHistoricalInput?: boolean;
|
|
2040
2043
|
/** Holds protected live or retry sources before execution; never cancels or disposes them. */
|
|
2041
2044
|
shouldHoldPendingInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | Promise<boolean>;
|
|
@@ -2090,6 +2093,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
2090
2093
|
}) => void;
|
|
2091
2094
|
armRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => TelegramUpdateJournalRoutingInput | undefined;
|
|
2092
2095
|
selectRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => boolean;
|
|
2096
|
+
isRoutingInputCurrent?: (input: { updateId: number; signal: AbortSignal }) => boolean;
|
|
2093
2097
|
supportsDeferredAbandonment?: (input: { updateId: number; signal: AbortSignal; journalBindingKey: string }) => boolean;
|
|
2094
2098
|
inspectAbandoning?: (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal }) =>
|
|
2095
2099
|
TelegramDeferredAbandonmentRecoveryPage | undefined;
|
|
@@ -3215,18 +3219,29 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3215
3219
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !expireRouting || !expireRoutingObserver) return false;
|
|
3216
3220
|
let nextAtMs: number | undefined, changed = false;
|
|
3217
3221
|
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
|
|
3218
|
-
// Lost
|
|
3222
|
+
// Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
|
|
3219
3223
|
const candidates = [...snapshot.entries, ...[...deferredSources.values()]
|
|
3220
3224
|
.filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
|
|
3221
3225
|
.map(source => source.entry)];
|
|
3222
3226
|
for (const entry of candidates) {
|
|
3223
3227
|
const lifetime = entry.routingInput;
|
|
3224
|
-
if (
|
|
3225
|
-
|
|
3226
|
-
|
|
3228
|
+
if (!lifetime || entry.state !== "pending" || !binding || claims.get(entry.updateId) === "queued" ||
|
|
3229
|
+
unsettledExecutionsByUpdateId.get(entry.updateId)?.size) continue;
|
|
3230
|
+
// Classify the first startup snapshot before constructing any historical expiry carrier.
|
|
3231
|
+
if (expectedOwner.startupUpdateIds?.has(entry.updateId) && !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput) {
|
|
3232
|
+
nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3227
3235
|
if (getNowMs() < lifetime.expiresAtMs) { nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs); continue; }
|
|
3228
3236
|
const existing = deferredSources.get(entry.updateId);
|
|
3229
|
-
if (existing && !isDeepStrictEqual(existing.entry, entry))
|
|
3237
|
+
if (existing && !isDeepStrictEqual(existing.entry, entry)) {
|
|
3238
|
+
// Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
|
|
3239
|
+
const previous = existing.entry.routingInput;
|
|
3240
|
+
if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
|
|
3241
|
+
(previous && (!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
|
|
3242
|
+
(previous.phase === "selected" && lifetime.phase !== "selected")))) continue;
|
|
3243
|
+
existing.entry = structuredClone(entry);
|
|
3244
|
+
}
|
|
3230
3245
|
const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true as const };
|
|
3231
3246
|
deferredSources.set(entry.updateId, source);
|
|
3232
3247
|
if (!claims.has(entry.updateId)) claims.set(entry.updateId, "historical");
|
|
@@ -3234,7 +3249,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3234
3249
|
deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
|
|
3235
3250
|
deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
|
|
3236
3251
|
isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
|
|
3237
|
-
let committed:
|
|
3252
|
+
let committed: TelegramRoutingInputExpiryResult | undefined;
|
|
3238
3253
|
try {
|
|
3239
3254
|
if (current()) await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
|
|
3240
3255
|
expire() {
|
|
@@ -3242,9 +3257,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3242
3257
|
if (committed) return { ...committed, duplicate: true };
|
|
3243
3258
|
claims.set(entry.updateId, "abandoning");
|
|
3244
3259
|
committed = expireRouting!({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
|
|
3260
|
+
// Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
|
|
3245
3261
|
claims.delete(entry.updateId); deferredSources.delete(entry.updateId);
|
|
3246
3262
|
state.journalEntryCount = committed.entryCount; state.journalSerializedBytes = committed.serializedBytes;
|
|
3247
|
-
updateClaimCounts(); notifyStateChange(); changed
|
|
3263
|
+
updateClaimCounts(); notifyStateChange(); changed ||= !committed.duplicate;
|
|
3248
3264
|
return committed;
|
|
3249
3265
|
} }, expectedOwner.ctx, expectedOwner.controller.signal);
|
|
3250
3266
|
} catch (error) { recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId }); }
|
|
@@ -3445,7 +3461,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3445
3461
|
transition("deferred", entry.updateId);
|
|
3446
3462
|
continue;
|
|
3447
3463
|
}
|
|
3448
|
-
if (review && historical && spendHistorical && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
3464
|
+
if (review && historical && spendHistorical && !entry.routingInput && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
3449
3465
|
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
3450
3466
|
continue;
|
|
3451
3467
|
}
|
|
@@ -3456,10 +3472,6 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3456
3472
|
}
|
|
3457
3473
|
}
|
|
3458
3474
|
if (entry.state === "pending" && entry.routingInput) {
|
|
3459
|
-
if (historical && spendHistorical) {
|
|
3460
|
-
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
3461
|
-
continue;
|
|
3462
|
-
}
|
|
3463
3475
|
claims.set(entry.updateId, "historical");
|
|
3464
3476
|
transition("deferred", entry.updateId);
|
|
3465
3477
|
continue;
|
|
@@ -3628,7 +3640,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3628
3640
|
expectedOwner.heldSourcesPreparedIssued = true;
|
|
3629
3641
|
// Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
|
|
3630
3642
|
void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
|
|
3631
|
-
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent
|
|
3643
|
+
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent,
|
|
3644
|
+
routingSourceIds: [...deferredSources].filter(([, source]) => source.journalBindingKey === key && !!source.entry.routingInput)
|
|
3645
|
+
.map(([id]) => id) }) : undefined).catch(error => {
|
|
3632
3646
|
try { deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" }); } catch { /* Diagnostic only. */ }
|
|
3633
3647
|
});
|
|
3634
3648
|
} catch (error) {
|
|
@@ -3784,6 +3798,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3784
3798
|
},
|
|
3785
3799
|
armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
|
|
3786
3800
|
selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
|
|
3801
|
+
isRoutingInputCurrent(input) {
|
|
3802
|
+
return owner?.controller.signal === input.signal && !input.signal.aborted &&
|
|
3803
|
+
deferredSources.has(input.updateId) && claims.get(input.updateId) !== "abandoning";
|
|
3804
|
+
},
|
|
3787
3805
|
inspectDeferredSource(input) {
|
|
3788
3806
|
const expectedOwner = owner;
|
|
3789
3807
|
const source = deferredSources.get(input.updateId);
|
|
@@ -3843,7 +3861,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3843
3861
|
return;
|
|
3844
3862
|
}
|
|
3845
3863
|
const claim = claims.get(input.updateId);
|
|
3846
|
-
if (claim === "abandoning" || claim === "historical" || claim === "retained"
|
|
3864
|
+
if (claim === "abandoning" || claim === "historical" || claim === "retained" ||
|
|
3865
|
+
(!claim && routingJournal?.inspectExpiry(input.updateId))) return;
|
|
3847
3866
|
if (input.outcome.kind === "complete") {
|
|
3848
3867
|
// Expiry may retire only a still-deferred source, never accepted queue work.
|
|
3849
3868
|
if (claim !== "deferred") return;
|
|
@@ -4453,6 +4472,7 @@ export interface TelegramUpdateAdmissionHandleDeps<
|
|
|
4453
4472
|
abandonDeferred?: TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"];
|
|
4454
4473
|
armRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["armRoutingInput"];
|
|
4455
4474
|
selectRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["selectRoutingInput"];
|
|
4475
|
+
isRoutingInputCurrent?: TelegramUpdateWorkerRuntime<TContext>["isRoutingInputCurrent"];
|
|
4456
4476
|
supportsDeferredAbandonment?: TelegramUpdateWorkerRuntime<TContext>["supportsDeferredAbandonment"];
|
|
4457
4477
|
inspectAbandoning?: TelegramUpdateWorkerRuntime<TContext>["inspectAbandoning"];
|
|
4458
4478
|
inspectHistorical?: TelegramUpdateWorkerRuntime<TContext>["inspectHistorical"];
|
|
@@ -5483,9 +5503,10 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
5483
5503
|
generation,
|
|
5484
5504
|
updateId: update.update_id,
|
|
5485
5505
|
signal,
|
|
5486
|
-
isCurrent: () => !signal.aborted && !suspended
|
|
5506
|
+
isCurrent: () => !signal.aborted && !suspended && ((!routingLifetime && !routingClockUnknown) || immediate || settlementReported ||
|
|
5507
|
+
deps.isRoutingInputCurrent?.({ updateId: update.update_id, signal }) !== false),
|
|
5487
5508
|
assertCurrent() {
|
|
5488
|
-
if (
|
|
5509
|
+
if (!execution.isCurrent()) {
|
|
5489
5510
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
5490
5511
|
}
|
|
5491
5512
|
},
|
|
@@ -5499,7 +5520,7 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
5499
5520
|
let outcome: TelegramUpdateAdmissionOutcome | undefined;
|
|
5500
5521
|
const boundUpdate = bindTelegramUpdateExecutionFence(
|
|
5501
5522
|
bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
5502
|
-
if (
|
|
5523
|
+
if (!execution.isCurrent()) return;
|
|
5503
5524
|
if (next.kind === "deferred" && next.routingReview) {
|
|
5504
5525
|
if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
|
|
5505
5526
|
throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
|
|
@@ -5522,7 +5543,7 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
5522
5543
|
}
|
|
5523
5544
|
void Promise.resolve()
|
|
5524
5545
|
.then(() => {
|
|
5525
|
-
if (
|
|
5546
|
+
if (!execution.isCurrent()) return;
|
|
5526
5547
|
return deps.onLateOutcome!(next, {
|
|
5527
5548
|
updateId: update.update_id,
|
|
5528
5549
|
ctx,
|
|
@@ -5605,7 +5626,7 @@ export function createTelegramCustodiedUpdateAdmissionHandle<
|
|
|
5605
5626
|
TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
|
|
5606
5627
|
TContext,
|
|
5607
5628
|
>(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>,
|
|
5608
|
-
"onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
|
|
5629
|
+
"onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "isRoutingInputCurrent" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
|
|
5609
5630
|
journal: TelegramCustodyExecutionJournal;
|
|
5610
5631
|
recipientBindingKey: string;
|
|
5611
5632
|
onLateOutcomeError(error: unknown, updateId: number): void;
|
|
@@ -6983,6 +7004,7 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
6983
7004
|
abandonDeferred: input => worker?.abandonDeferred?.(input),
|
|
6984
7005
|
armRoutingInput: input => worker?.armRoutingInput?.(input),
|
|
6985
7006
|
selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
|
|
7007
|
+
isRoutingInputCurrent: input => worker?.isRoutingInputCurrent?.(input) !== false,
|
|
6986
7008
|
supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
|
|
6987
7009
|
inspectAbandoning: input => worker?.inspectAbandoning?.(input),
|
|
6988
7010
|
inspectHistorical: input => worker?.inspectHistorical?.(input),
|