improve: reduce idle delivery and notification database work (#168643)

* perf(sqlite): reuse idle delivery and notification facts

Keep queue and Web Push presence in the existing physical-database receipt owner. Preserve separate audit durability points while omitting readback for current newly inserted events. Audit collection, retention, and schema are unchanged.

* fix(tooling): include delivery cache in extracted PR wrappers
This commit is contained in:
Peter Steinberger 2026-10-10 15:49:27 -07:00 • committed by GitHub
parent 53b8118647
commit 566005dc13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 245 additions and 79 deletions

View file

@ -36,6 +36,14 @@ are not part of the correctness contract. SQLite statements in autocommit see
committed rows without a freshness query. Keep explicit read transactions only
where multiple statements need one consistent snapshot.
Idle outbound recovery and the Web Push subscription-presence check reuse
physical-database facts on the Gateway host, avoiding worker requests as well as
SQL. Queue insertions and replacements invalidate the empty recovery fact;
subscription mutations publish or invalidate presence through the existing
commit receipts. These facts do not authorize sends: delivery custody and current
subscription policy remain with their existing owners. Mention Inbox head checks
use the same physical-database admission lifecycle.
Outside writers, including CLI commands, Doctor, cron processes, native companion
processes, and plugin children, must route through the Gateway or acquire exclusive
ownership while it is stopped. Existing direct writers that have not adopted that

View file

@ -824,6 +824,7 @@ src/infra/dedupe.ts
src/infra/deep-merge.ts
src/infra/deferred-plugin-migrations.contract.ts
src/infra/deferred-plugin-migrations.ts
src/infra/delivery-queue-cache.ts
src/infra/delivery-queue-sqlite-bound.ts
src/infra/delivery-queue-sqlite.types.ts
src/infra/diagnostic-event-listener-presence.ts

View file

@ -637,8 +637,14 @@ export function recordAuditEventInDatabase(
if (!Number.isSafeInteger(insertedSequence) || insertedSequence < 1) {
throw new Error("audit event sequence is outside the supported integer range");
}
pruneAuditEventsAfterInsert(db, Date.now());
const row = queries.read(insertedSequence);
const now = Date.now();
pruneAuditEventsAfterInsert(db, now);
// Current events cannot be removed by retention: sequence allocation puts
// them after every existing row. An expired replay may be pruned immediately.
const row =
values.occurred_at < now - AUDIT_EVENT_RETENTION_MS
? queries.read(insertedSequence)
: { ...values, sequence: insertedSequence };
recordConfirmedTerminalMessageExecutionBinding(db, {
eventId: row?.event_id,
token: executionToken,

View file

@ -167,7 +167,7 @@ describe("audit event persistence", () => {
...database,
database: openOpenClawStateDatabase(database),
});
recordAuditEventInDatabase(
const middle = recordAuditEventInDatabase(
auditInput({
occurredAt: now + 1,
sourceSequence: 2,
@ -179,7 +179,7 @@ describe("audit event persistence", () => {
}),
{ ...database, database: openOpenClawStateDatabase(database) },
);
recordAuditEventInDatabase(
const newest = recordAuditEventInDatabase(
auditInput({
occurredAt: now + 2,
sourceSequence: 3,
@ -195,12 +195,14 @@ describe("audit event persistence", () => {
);
const first = await listAuditEvents({ database, limit: 2 });
expect(first.events).toEqual([newest, middle]);
expect(first.events.map((event) => event.sourceSequence)).toEqual([3, 2]);
expect(first.nextCursor).toBe(first.events[1]?.sequence);
await closeOpenClawStateDatabaseAsync();
closeOpenClawStateDatabaseForTest();
const second = await listAuditEvents({ database, limit: 2, cursor: first.nextCursor });
expect(second.events).toEqual([oldest]);
expect(second.events.map((event) => event.sourceSequence)).toEqual([1]);
expect(second.events[0]?.eventId).toBe(oldest?.eventId);
expect(second.nextCursor).toBeUndefined();
@ -328,6 +330,42 @@ describe("audit event persistence", () => {
expect(db.prepare("SELECT COUNT(*) AS count FROM audit_events").get()).toEqual({ count: 0 });
});
it("returns an expired append only while bounded pruning still retains it", () => {
const database = createDatabaseOptions();
const owner = openOpenClawStateDatabase(database);
const now = Date.now();
const occurredAt = now - AUDIT_EVENT_RETENTION_MS_CONTRACT - 1_000;
owner.db
.prepare(
`WITH RECURSIVE numbers(n) AS (
SELECT 1 UNION ALL SELECT n + 1 FROM numbers WHERE n < ?
)
INSERT INTO audit_events (
event_id, source_id, source_sequence, occurred_at, kind, action, status,
actor_type, actor_id, agent_id, run_id
)
SELECT 'old-event-' || n, 'old-source-' || n, n, ?, 'agent_run',
'agent.run.started', 'started', 'agent', 'main', 'main', 'old-run'
FROM numbers`,
)
.run(AUDIT_EVENT_PRUNE_BATCH_ROWS_CONTRACT, occurredAt);
const retained = recordAuditEventInDatabase(auditInput({ occurredAt: occurredAt + 1 }), {
...database,
database: owner,
});
expect(retained?.occurredAt).toBe(occurredAt + 1);
expect(
recordAuditEventInDatabase(auditInput({ occurredAt: occurredAt + 2 }), {
...database,
database: owner,
}),
).toBeUndefined();
expect(owner.db.prepare("SELECT COUNT(*) AS count FROM audit_events").get()).toEqual({
count: 0,
});
});
it("keeps reused run ids distinct across actual event timestamps", async () => {
const database = createDatabaseOptions();
const occurredAt = Date.now();

View file

@ -0,0 +1,6 @@
import type { SqliteDatabaseAdmissionKey } from "./sqlite-database-admission.js";
export const emptyOutboundDeliveryQueueAdmission: SqliteDatabaseAdmissionKey<true | undefined> = {
name: "state.empty-outbound-delivery-queue",
read: (value) => (value === true ? true : undefined),
};

View file

@ -3,6 +3,7 @@ import type { DatabaseSync } from "node:sqlite";
import type { RawBuilder, Selectable } from "kysely";
import type { OpenClawStateDatabase } from "../state/openclaw-state-db-contract.js";
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
import { emptyOutboundDeliveryQueueAdmission } from "./delivery-queue-cache.js";
import type { DeliveryQueueEntryState } from "./delivery-queue-sqlite.types.js";
import {
createSqliteQueryCache,
@ -11,6 +12,7 @@ import {
prepareSqliteQueryTakeFirstSync,
getNodeSqliteKysely,
} from "./kysely-sync.js";
import { publishSqliteDatabaseAdmission } from "./sqlite-database-admission.js";
import { coerceRequiredSqliteNumber as sqliteNumber } from "./sqlite-number.js";
type QueueStatus = "pending" | "failed" | "completed";
@ -302,7 +304,12 @@ export function upsertBoundDeliveryQueueEntryInDatabase(
): boolean {
const queries = deliveryQueueUpserts(database.db);
const query = (queries[bound.mode] ??= createDeliveryQueueUpsert(database.db, bound.mode));
return query(bound.row).numAffectedRows === 1n;
const changed = query(bound.row).numAffectedRows === 1n;
if (changed) {
// Every queue insertion/replacement passes here; deletion cannot make an empty queue nonempty.
publishSqliteDatabaseAdmission(database.db, emptyOutboundDeliveryQueueAdmission, undefined);
}
return changed;
}
/** Recovery and media custody share the same inventory of unfinished work. */

View file

@ -1,4 +1,5 @@
import type { OpenClawStateDatabase } from "../../state/openclaw-state-db-contract.js";
import { emptyOutboundDeliveryQueueAdmission } from "../delivery-queue-cache.js";
import {
deliveryQueueEntriesQuery,
inflateDeliveryQueueRow,
@ -7,11 +8,12 @@ import {
} from "../delivery-queue-sqlite-bound.js";
import { transitionOwnedDeliveryQueueEntryInDatabase } from "../delivery-queue-sqlite-claim.kernel.js";
import { upsertDeliveryQueueEntryInDatabase } from "../delivery-queue-sqlite.kernel.js";
import { createSqliteQueryCache, executeSqliteQuerySync } from "../kysely-sync.js";
import { executeSqliteQuerySync } from "../kysely-sync.js";
import {
getSqliteReadOperationRevision,
type SqliteReadOperationRevision,
} from "../sqlite-schema-facts.js";
getSqliteDatabaseAdmission,
publishSqliteDatabaseAdmission,
readSqliteDatabaseWriteRevision,
} from "../sqlite-database-admission.js";
import {
OUTBOUND_EXECUTABLE_QUEUE_NAMES,
outboundDeliveryQueueName,
@ -21,10 +23,6 @@ import { projectOutboundDelivery } from "./delivery-queue-projection.js";
import type { OutboundDeliveryStorageEntry } from "./delivery-queue-storage.types.js";
import type { QueuedDelivery } from "./delivery-queue-types.js";
const unfinishedDeliveryFacts = createSqliteQueryCache<{
empty?: SqliteReadOperationRevision;
}>(() => ({}));
/** Restore the exact pre-attempt row while its original owner still holds custody. */
export function restoreDeliveryAttemptBeforeDispatchInDatabase(
database: OpenClawStateDatabase,
@ -85,14 +83,11 @@ export function readOutboundDeliveriesInDatabase(
database: Pick<OpenClawStateDatabase, "db">,
input: { id?: string; mode: "pending" | "unfinished" },
): OutboundDeliveryStorageEntry[] {
const idle =
input.id === undefined && input.mode === "unfinished"
? unfinishedDeliveryFacts(database.db)
: undefined;
const revision = idle && getSqliteReadOperationRevision(database.db);
if (revision && idle?.empty === revision) {
const idle = input.id === undefined && input.mode === "unfinished";
if (idle && getSqliteDatabaseAdmission(database.db, emptyOutboundDeliveryQueueAdmission)) {
return [];
}
const revision = idle && readSqliteDatabaseWriteRevision(database.db);
let query = deliveryQueueEntriesQuery(database, OUTBOUND_EXECUTABLE_QUEUE_NAMES, input.mode)
.select("queue_name")
.orderBy("enqueued_at", "asc")
@ -112,12 +107,17 @@ export function readOutboundDeliveriesInDatabase(
seen.add(entry.id);
return [{ queueName: row.queue_name, entry: projectOutboundDelivery(row.queue_name, entry) }];
});
if (idle) {
// The enclosing read admission probes foreign commits on every new poll.
idle.empty =
entries.length === 0 && getSqliteReadOperationRevision(database.db) === revision
? revision
: undefined;
if (
idle &&
!database.db.isTransaction &&
revision !== undefined &&
revision === readSqliteDatabaseWriteRevision(database.db)
) {
publishSqliteDatabaseAdmission(
database.db,
emptyOutboundDeliveryQueueAdmission,
entries.length === 0 ? true : undefined,
);
}
return entries;
}

View file

@ -5,6 +5,7 @@ import {
hydrateOpenClawStateWorkerError,
retainOpenClawStateWorkerErrorPayload,
} from "../../state/openclaw-state-worker-error.js";
import { emptyOutboundDeliveryQueueAdmission } from "../delivery-queue-cache.js";
import type { InitialDeliveryProducerClaim } from "../delivery-queue-sqlite-claim.kernel.js";
import {
captureDeliveryQueueStateContext,
@ -14,6 +15,7 @@ import {
import { executeDeliveryQueueOperation } from "../delivery-queue-worker-store.js";
import type { DeliveryQueueWorkerOperations } from "../delivery-queue.worker-contract.js";
import { generateSecureUuid } from "../secure-random.js";
import { getOrLoadSqliteDatabaseAdmissionForPath } from "../sqlite-database-admission.js";
import { createSqliteWorkerOperationAdmission } from "../sqlite-worker-operation-admission.js";
import type { SqliteWorkerOperationSettlement } from "../sqlite-worker-operation-settlement.js";
import { OutboundDeliveryError, PlatformMessageNotDispatchedError } from "./deliver-types.js";
@ -362,6 +364,18 @@ async function readOutboundDeliveries(
context?: DeliveryQueueStateContext,
): Promise<QueuedDelivery[]> {
const captured = context ?? captureDeliveryQueueStateContext(stateDir);
if (
input.id === undefined &&
input.mode === "unfinished" &&
getOrLoadSqliteDatabaseAdmissionForPath(
captured.workerContext.admission.databasePath,
emptyOutboundDeliveryQueueAdmission,
() => undefined,
)
) {
captured.workerContext.admission.assertCurrent();
return [];
}
const reply = await executeExistingOpenClawStateRead(
{
path: captured.workerContext.admission.databasePath,

View file

@ -8,8 +8,10 @@ import {
openOpenClawStateDatabase,
runOpenClawStateWriteTransaction,
} from "../../state/openclaw-state-db.js";
import { updateDeliveryQueueEntryInDatabase } from "../delivery-queue-sqlite.kernel.js";
import { openNodeSqliteDatabase } from "../node-sqlite.js";
import {
updateDeliveryQueueEntryInDatabase,
upsertDeliveryQueueEntryInDatabase,
} from "../delivery-queue-sqlite.kernel.js";
import { runSqliteReadOperationSync } from "../sqlite-schema-facts.js";
import { failPendingDelivery } from "./delivery-queue-ack.js";
import { ackDeliveryInDatabase } from "./delivery-queue-ack.kernel.js";
@ -25,6 +27,7 @@ import {
failDeliveryAfterPlatformSend,
failDeliveryBeforePlatformSend,
loadPendingDelivery,
loadUnfinishedDeliveries,
markDeliveryPlatformOutcomeUnknown,
markDeliveryPlatformSendDispatched,
markDeliveryPlatformSendAttemptStarted,
@ -41,56 +44,52 @@ describe("delivery-queue storage", () => {
const enqueueTextDelivery = (params: Parameters<typeof enqueueDelivery>[0], rootDir = tmpDir()) =>
enqueueDelivery(params, rootDir);
it("skips unchanged empty recovery scans and observes foreign, local, and rolled-back writes", async () => {
const options = { env: { ...process.env, OPENCLAW_STATE_DIR: tmpDir() } };
it("refreshes empty recovery facts after worker and native writes without publishing rollback", async () => {
const stateDir = tmpDir();
const options = { env: { ...process.env, OPENCLAW_STATE_DIR: stateDir } };
const database = openOpenClawStateDatabase(options);
const reads = trackSqliteStatementExecutions(database.db, ["queue"], (sql) =>
/^select .+ from "delivery_queue_entries"/i.test(sql) ? "queue" : null,
);
const read = () =>
runSqliteReadOperationSync(database.db, () =>
readOutboundDeliveriesInDatabase(database, { mode: "unfinished" }),
);
const peer = openNodeSqliteDatabase(database.path);
try {
expect(read()).toEqual([]);
expect(read()).toEqual([]);
expect(reads.counts.queue).toBe(1);
const read = () => loadUnfinishedDeliveries(stateDir);
expect(await read()).toEqual([]);
expect(await read()).toEqual([]);
const id = await enqueueTextDelivery({
channel: "directchat",
to: "+1555",
payloads: [{ text: "Queued after an idle poll" }],
});
expect(read().map(({ entry }) => entry.id)).toEqual([id]);
database.db
.prepare("UPDATE delivery_queue_entries SET status = 'completed' WHERE id = ?")
.run(id);
expect(read()).toEqual([]);
expect(read()).toEqual([]);
expect(reads.counts.queue).toBe(3);
peer.prepare("UPDATE delivery_queue_entries SET status = 'pending' WHERE id = ?").run(id);
expect(read().map(({ entry }) => entry.id)).toEqual([id]);
expect(reads.counts.queue).toBe(4);
expect(() =>
runOpenClawStateWriteTransaction(
() => {
database.db
.prepare("UPDATE delivery_queue_entries SET status = 'completed' WHERE id = ?")
.run(id);
expect(read()).toEqual([]);
throw new Error("roll back recovery fixture");
},
{ ...options, database },
),
).toThrow("roll back recovery fixture");
expect(read().map(({ entry }) => entry.id)).toEqual([id]);
} finally {
peer.close();
reads.restore();
const id = await enqueueTextDelivery({
channel: "directchat",
to: "+1555",
payloads: [{ text: "Queued after an idle poll" }],
});
expect((await read()).map((entry) => entry.id)).toEqual([id]);
const entry = await loadPendingDelivery(id, stateDir);
if (!entry) {
throw new Error("Expected pending delivery");
}
await ackDelivery(id, stateDir);
expect(await read()).toEqual([]);
expect(await read()).toEqual([]);
expect(() =>
runOpenClawStateWriteTransaction(
() => {
upsertDeliveryQueueEntryInDatabase(
{ queueName: OUTBOUND_DELIVERY_QUEUE_NAME, entry },
database,
);
expect(
runSqliteReadOperationSync(database.db, () =>
readOutboundDeliveriesInDatabase(database, { mode: "unfinished" }),
).map(({ entry: pending }) => pending.id),
).toEqual([id]);
throw new Error("roll back recovery fixture");
},
{ ...options, database },
),
).toThrow("roll back recovery fixture");
expect(await read()).toEqual([]);
upsertDeliveryQueueEntryInDatabase(
{ queueName: OUTBOUND_DELIVERY_QUEUE_NAME, entry },
database,
);
expect((await read()).map((pending) => pending.id)).toEqual([id]);
});
function readStatus(id: string): string | undefined {

View file

@ -0,0 +1,6 @@
import type { SqliteDatabaseAdmissionKey } from "./sqlite-database-admission.js";
export const boundWebPushSubscriptionsAdmission: SqliteDatabaseAdmissionKey<boolean> = {
name: "state.web-push-bound-subscriptions",
read: (value) => (typeof value === "boolean" ? value : undefined),
};

View file

@ -18,6 +18,7 @@ import {
getNodeSqliteKysely,
} from "./kysely-sync.js";
import { normalizeWebPushDevicePreferences } from "./push-web-preferences.js";
import { boundWebPushSubscriptionsAdmission } from "./push-web-store.cache.js";
import {
WEB_PUSH_VAPID_STATE_KEY,
WebPushSubscriptionBindingError,
@ -32,6 +33,11 @@ import {
type WebPushMutationProfiles,
type WebPushMutationProfileFacts,
} from "./push-web-store.records.js";
import {
getSqliteDatabaseAdmission,
publishSqliteDatabaseAdmission,
readSqliteDatabaseWriteRevision,
} from "./sqlite-database-admission.js";
import { requestSqliteWorkerOperationAdmission } from "./sqlite-worker-operation-admission.js";
import { getSqliteWorkerStateContext } from "./sqlite-worker-state-context.js";
@ -188,7 +194,12 @@ export function listWebPushSubscriptionsInDatabase(
export function hasBoundWebPushSubscriptionsInDatabase(database: OpenClawStateDatabase): boolean {
ensureWebPushSubscriptionBindingSchema(database);
const { db } = database;
return Boolean(
const admitted = getSqliteDatabaseAdmission(db, boundWebPushSubscriptionsAdmission);
if (admitted !== undefined) {
return admitted;
}
const revision = readSqliteDatabaseWriteRevision(db);
const hasBound = Boolean(
executeSqliteQueryTakeFirstSync(
db,
getNodeSqliteKysely<WebPushDatabase>(db)
@ -199,6 +210,10 @@ export function hasBoundWebPushSubscriptionsInDatabase(database: OpenClawStateDa
.limit(1),
),
);
if (revision !== undefined && revision === readSqliteDatabaseWriteRevision(db)) {
publishSqliteDatabaseAdmission(db, boundWebPushSubscriptionsAdmission, hasBound);
}
return hasBound;
}
/** Lists only subscriptions reconciled by an authenticated browser device. */
@ -501,6 +516,11 @@ export function upsertWebPushSubscriptionInDatabase(
}),
),
);
publishSqliteDatabaseAdmission(
db,
boundWebPushSubscriptionsAdmission,
row.device_id ? true : undefined,
);
return subscription;
});
}
@ -527,7 +547,11 @@ export function deleteBoundWebPushSubscriptionInDatabase(
params.expectedUserProfileId,
),
);
return Number(result.numAffectedRows ?? 0) > 0;
const deleted = Number(result.numAffectedRows ?? 0) > 0;
if (deleted) {
publishSqliteDatabaseAdmission(db, boundWebPushSubscriptionsAdmission, undefined);
}
return deleted;
});
}
@ -551,7 +575,11 @@ export function deleteWebPushSubscriptionIfCurrentInDatabase(params: {
.where("auth", "=", subscription.keys.auth)
.where("updated_at_ms", "=", subscription.updatedAtMs),
);
return Number(result.numAffectedRows ?? 0) > 0;
const deleted = Number(result.numAffectedRows ?? 0) > 0;
if (deleted) {
publishSqliteDatabaseAdmission(db, boundWebPushSubscriptionsAdmission, undefined);
}
return deleted;
}, webPushDatabaseOptions(params.database));
}

View file

@ -7,6 +7,7 @@ import {
executeOpenClawStateWorker,
runOpenClawStateWorkerOperation,
} from "../state/openclaw-state-worker-store.js";
import { boundWebPushSubscriptionsAdmission } from "./push-web-store.cache.js";
import {
WebPushSubscriptionBindingError,
type WebPushMutationGuard,
@ -17,6 +18,7 @@ import {
type WebPushSnapshotAction,
} from "./push-web-store.scope.js";
import type { WebPushWorkerOperations } from "./push-web-store.worker-contract.js";
import { getOrLoadSqliteDatabaseAdmissionForPath } from "./sqlite-database-admission.js";
import { createSqliteWorkerOperationAdmission } from "./sqlite-worker-operation-admission.js";
export {
WebPushSubscriptionBindingError,
@ -177,7 +179,16 @@ export function listWebPushSubscriptions(stateDir?: string) {
}
export function hasBoundWebPushSubscriptions(stateDir?: string) {
return executeWebPushCommand("webPush.hasBoundWebPushSubscriptions", undefined, stateDir);
const captured = context(stateDir);
captured.admission.assertCurrent();
const admitted = getOrLoadSqliteDatabaseAdmissionForPath(
captured.admission.databasePath,
boundWebPushSubscriptionsAdmission,
() => undefined,
);
return admitted === undefined
? executeWebPushCommand("webPush.hasBoundWebPushSubscriptions", undefined, captured)
: Promise.resolve(admitted);
}
export function prepareWebPushApprovalDeliveries(

View file

@ -21,6 +21,7 @@ import {
closeOpenClawStateDatabaseAsync,
openOpenClawStateDatabase,
} from "../state/openclaw-state-db.js";
import * as stateWorker from "../state/openclaw-state-worker-store.js";
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
import {
deleteWebPushApprovalDeliveryTargets,
@ -136,6 +137,7 @@ beforeEach(async () => {
});
afterEach(async () => {
vi.restoreAllMocks();
await closeOpenClawStateDatabaseAsync();
await fs.rm(tmpDir, { recursive: true, force: true });
});
@ -332,8 +334,13 @@ describe("subscription CRUD", () => {
]);
});
it("keeps legacy unbound rows test-only until browser reconciliation", async () => {
it("retains bound-subscription presence until registration or removal changes it", async () => {
const execute = vi.spyOn(stateWorker, "executeOpenClawStateWorker");
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
execute.mockClear();
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
expect(execute).not.toHaveBeenCalled();
await registerSubscription(endpoint);
expect(await readBoundSubscriptions(tmpDir)).toEqual([]);
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
@ -348,6 +355,9 @@ describe("subscription CRUD", () => {
binding: { deviceId: "browser-device", userProfileId: null },
});
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(true);
execute.mockClear();
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(true);
expect(execute).not.toHaveBeenCalled();
expect(await readBoundSubscriptions(tmpDir)).toEqual([
{
...rebound,
@ -356,6 +366,24 @@ describe("subscription CRUD", () => {
devicePreferences: defaultDevicePreferences,
},
]);
await registerSubscription(endpoint);
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
await registerSubscription(endpoint, {
binding: { deviceId: "browser-device", userProfileId: null },
});
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(true);
expect(
await clearBoundWebPushSubscription({
endpoint,
expectedDeviceId: "browser-device",
expectedUserProfileId: null,
baseDir: tmpDir,
}),
).toBe(true);
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
execute.mockClear();
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
expect(execute).not.toHaveBeenCalled();
});
it("preserves bindings when an older writer updates only the original columns", async () => {
@ -887,6 +915,20 @@ describe("sending", () => {
expect(await listWebPushSubscriptions(tmpDir)).toEqual([replacement]);
});
it("refreshes bound-subscription presence after expired-delivery cleanup", async () => {
await registerSubscription("https://push.example.com/expired-bound", {
binding: { deviceId: "browser-device", userProfileId: null },
});
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(true);
vi.mocked(webPush.sendNotification).mockRejectedValueOnce(
Object.assign(new Error("gone"), { statusCode: 410 }),
);
expect(await broadcastWebPush({ title: "Expired" }, tmpDir)).toEqual([
expect.objectContaining({ ok: false, statusCode: 410 }),
]);
expect(await hasBoundWebPushSubscriptions(tmpDir)).toBe(false);
});
it("does not delete an expired subscription after a legacy claim appears", async ({ signal }) => {
const endpoint = "https://push.example.com/pending-claim";
const subscription = await registerSubscription(endpoint);