From 93871beb81802a2ed1a801bbd9e1de1947d589ec Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 10:02:05 -0600 Subject: [PATCH] fix(replication): propagate per-event expiresAt to record writes Two issues prevented replicated records from being evicted: 1. The `options` object passed to `_writeUpdate` for each replicated event did not include `expiresAt`. In a multi-record transaction batch every event after the first uses the first event as `context`, so `context.expiresAt` would silently apply the wrong expiration (or none) to subsequent records. 2. `_writeUpdate` read `expiresAt` only from `context`, so per-event options were ignored. Now: `options?.expiresAt ?? context?.expiresAt ?? fallback`. 3. `scheduleCleanup()` was only armed when `context.expiresAt` was truthy, missing replicated writes that carry expiration via options. Changed to `if (expiresAt >= 0)` to cover both paths. Co-Authored-By: Claude Sonnet 4.6 --- resources/Table.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/Table.ts b/resources/Table.ts index 940777be0..97ea83e2c 100644 --- a/resources/Table.ts +++ b/resources/Table.ts @@ -311,6 +311,8 @@ export function makeTable(options) { ensureLoaded: false, nodeId: event.nodeId, viaNodeId: event.viaNodeId, + // use per-event expiresAt: batched txn context only holds the first event's expiration + expiresAt: event.expiresAt, async: true, }; const id = event.id; @@ -1685,7 +1687,8 @@ export function makeTable(options) { const type = fullUpdate ? 'put' : 'patch'; let residencyId: number | undefined; if (options?.residencyId != undefined) residencyId = options.residencyId; - const expiresAt: number = context?.expiresAt ?? (expirationMs ? expirationMs + Date.now() : -1); + const expiresAt: number = + options?.expiresAt ?? context?.expiresAt ?? (expirationMs ? expirationMs + Date.now() : -1); const additionalAuditRefs: Array<{ version: number; nodeId: number }> = []; // track additional audit refs to store if (precedesExisting <= 0) { @@ -1902,7 +1905,7 @@ export function makeTable(options) { updateIndices(id, existingRecord, recordToStore, transaction && { transaction }); writeCommit(true); - if (context.expiresAt) scheduleCleanup(); + if (expiresAt >= 0) scheduleCleanup(); // arm for replicated writes too, not just local-context writes function writeCommit(storeRecord: boolean) { // we need to write the commit. if storeRecord then we need to store the record, otherwise we just need to store the audit record updateRecord(