mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
264 lines
11 KiB
TypeScript
264 lines
11 KiB
TypeScript
import { isDismissedAlert, reconcileQueuedDismissal } from './push-queued-dismissal.js'
|
||
import { parsePushDeliveryPayload } from './push-delivery-payload.js'
|
||
import { createHash, randomUUID } from 'node:crypto'
|
||
import { PUSH_LIMITS, type PushNotification } from '@orca-cloud/push-contract'
|
||
import { WORKER_DRAINS } from './push-worker-concurrency.js'
|
||
import type { PushDatabase, SqlRow } from './push-database.js'
|
||
import { limitPushDatabaseConcurrency } from './push-background-database.js'
|
||
|
||
const RETENTION_MS = 24 * 60 * 60_000
|
||
// accept() caps expires_at at due_at + TTL, so older due_at is expired: scans skip an unpruned backlog.
|
||
const TTL_MS = PUSH_LIMITS.notificationTtlSeconds * 1000
|
||
// One winner plus every other drain holding a device head, so a full set of peers cannot exhaust it.
|
||
const CLAIM_CANDIDATE_ATTEMPTS = WORKER_DRAINS
|
||
export const PRUNE_BATCH_ROWS = 2_000
|
||
export const PRUNE_MAX_BATCHES = 50
|
||
export const DELIVERY_LEASE_MS = 30_000
|
||
// `saturated` means the batch budget ran out with rows still matching, so a backlog remains.
|
||
export type PushPruneSweep = { deleted: number; saturated: boolean }
|
||
export type QueuedPushDelivery = {
|
||
id: string
|
||
registrationId: string
|
||
hostFingerprint: string
|
||
notification: PushNotification
|
||
expiresAt: number
|
||
lease: string
|
||
attempts: number
|
||
}
|
||
|
||
export class DurablePushStore {
|
||
private readonly claims: PushDatabase
|
||
|
||
constructor(
|
||
private readonly database: PushDatabase,
|
||
private readonly now = Date.now,
|
||
// Claim, finish and prune traffic; accept() and renew() stay on the request-path database.
|
||
private readonly background = database
|
||
) {
|
||
// Wait outside the background pool so competing claims leave room for lease completion.
|
||
this.claims = limitPushDatabaseConcurrency(background, 3)
|
||
}
|
||
|
||
async accept(
|
||
host: string,
|
||
registrationId: string,
|
||
notification: PushNotification
|
||
): Promise<'queued' | 'rate_limited' | 'error'> {
|
||
const now = this.now()
|
||
const kind = notification.kind ?? 'alert'
|
||
const eventId = createHash('sha256')
|
||
.update(
|
||
JSON.stringify([host, kind, notification.notificationEpoch, notification.notificationSeq])
|
||
)
|
||
.digest('hex')
|
||
const { sound: _sound, kind: _kind, ...content } = notification
|
||
const fingerprint = createHash('sha256')
|
||
.update(JSON.stringify({ kind, ...content }))
|
||
.digest('hex')
|
||
return this.database.transaction(async (tx) => {
|
||
await tx.lockQuotaScope(`push-events:${host}`)
|
||
const [existing] = await tx.query('SELECT * FROM push_events WHERE event_id = ?', [eventId])
|
||
if (existing && existing.fingerprint !== fingerprint) return 'error'
|
||
const expiresAt = existing
|
||
? Number(existing.expires_at)
|
||
: Math.min(
|
||
notification.expiresAt ?? Infinity,
|
||
now + PUSH_LIMITS.notificationTtlSeconds * 1000
|
||
)
|
||
if (expiresAt <= now) return 'error'
|
||
if (!existing) {
|
||
const [count] = await tx.query(
|
||
'SELECT COUNT(*) AS total FROM push_events WHERE host_fingerprint = ? AND kind = ? AND created_at > ?',
|
||
[host, kind, now - PUSH_LIMITS.eventQuotaWindowMs]
|
||
)
|
||
if (Number(count?.total ?? 0) >= PUSH_LIMITS.hostEventsPerWindow) return 'rate_limited'
|
||
await tx.query(
|
||
'INSERT INTO push_events(event_id, host_fingerprint, kind, fingerprint, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||
[eventId, host, kind, fingerprint, now, expiresAt]
|
||
)
|
||
}
|
||
const [recipient] = await tx.query(
|
||
'SELECT event_id FROM push_event_recipients WHERE event_id = ? AND registration_id = ?',
|
||
[eventId, registrationId]
|
||
)
|
||
if (recipient) return 'queued'
|
||
if (await reconcileQueuedDismissal(tx, host, registrationId, notification, now))
|
||
return 'queued'
|
||
await tx.query(
|
||
`INSERT INTO push_delivery_batches(batch_id, host_fingerprint, registration_id, kind, payload_json, state, due_at, expires_at, lease_until, attempts, created_at)
|
||
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, 0, 0, ?)`,
|
||
[
|
||
randomUUID(),
|
||
host,
|
||
registrationId,
|
||
kind,
|
||
JSON.stringify(notification),
|
||
now,
|
||
expiresAt,
|
||
now
|
||
]
|
||
)
|
||
await tx.query(
|
||
'INSERT INTO push_event_recipients(event_id, registration_id, created_at) VALUES (?, ?, ?)',
|
||
[eventId, registrationId, now]
|
||
)
|
||
return 'queued'
|
||
})
|
||
}
|
||
|
||
async claim(canClaim?: () => boolean): Promise<QueuedPushDelivery | null> {
|
||
return this.claims.transaction(async (tx) => {
|
||
if (canClaim && !canClaim()) return null
|
||
// Share the previous revision’s exclusive claim key so it snapshots after our leases commit; drop one release after all workers upgrade.
|
||
if (!(await tx.tryLockSharedScope('push-worker-claim'))) return null
|
||
const now = this.now()
|
||
// SQLite already serializes the whole transaction.
|
||
const lockRow = tx.dialect === 'postgres' ? ' FOR UPDATE SKIP LOCKED' : ''
|
||
// Keep the busy-device check correlated so PostgreSQL uses the existing device index.
|
||
const busyOffset = tx.dialect === 'postgres' ? ' OFFSET 0' : ''
|
||
const skipped: string[] = []
|
||
for (let attempt = 0; attempt < CLAIM_CANDIDATE_ATTEMPTS; attempt++) {
|
||
const excluded = skipped.map(() => ' AND registration_id <> ?').join('')
|
||
const [row] = await tx.query(
|
||
`SELECT * FROM push_delivery_batches WHERE state = 'pending' AND lease_until <= ? AND expires_at > ? AND due_at <= ? AND due_at > ?${excluded}
|
||
AND NOT EXISTS (SELECT 1 FROM push_delivery_batches busy WHERE busy.registration_id = push_delivery_batches.registration_id AND busy.state = 'pending' AND busy.lease_until > 0 AND busy.lease_until > ?${busyOffset})
|
||
ORDER BY due_at, created_at, batch_id LIMIT 1${lockRow}`,
|
||
[now, now, now, now - TTL_MS, ...skipped, now]
|
||
)
|
||
if (!row) return null
|
||
if (await this.ownsDeviceHead(tx, row, now)) return await this.lease(tx, row, now)
|
||
skipped.push(String(row.registration_id))
|
||
}
|
||
return null
|
||
})
|
||
}
|
||
|
||
// Row locks alone allow leasing behind a skipped device head; device locking plus reread sees committed leases and uncommitted heads.
|
||
private async ownsDeviceHead(tx: PushDatabase, row: SqlRow, now: number): Promise<boolean> {
|
||
const registrationId = String(row.registration_id)
|
||
if (!(await tx.tryLockScope(`push-device:${registrationId}`))) return false
|
||
const [device] = await tx.query(
|
||
`SELECT (SELECT batch_id FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND expires_at > ? AND due_at > ? ORDER BY due_at, created_at, batch_id LIMIT 1) AS head,
|
||
EXISTS (SELECT 1 FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND lease_until > 0 AND lease_until > ?) AS busy`,
|
||
[registrationId, now, now - TTL_MS, registrationId, now]
|
||
)
|
||
return device?.head === row.batch_id && !Number(device?.busy)
|
||
}
|
||
|
||
private async lease(
|
||
tx: PushDatabase,
|
||
row: SqlRow,
|
||
now: number
|
||
): Promise<QueuedPushDelivery | null> {
|
||
const notification = parsePushDeliveryPayload(String(row.payload_json))
|
||
if (await isDismissedAlert(tx, String(row.host_fingerprint), notification)) {
|
||
await tx.query('DELETE FROM push_delivery_batches WHERE batch_id = ?', [row.batch_id])
|
||
return null
|
||
}
|
||
const lease = randomUUID()
|
||
await tx.query(
|
||
'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?',
|
||
[lease, now + DELIVERY_LEASE_MS, row.batch_id]
|
||
)
|
||
return this.delivery(row, lease, notification)
|
||
}
|
||
|
||
private delivery(row: SqlRow, lease: string, notification: PushNotification): QueuedPushDelivery {
|
||
return {
|
||
id: String(row.batch_id),
|
||
registrationId: String(row.registration_id),
|
||
hostFingerprint: String(row.host_fingerprint),
|
||
notification,
|
||
expiresAt: Number(row.expires_at),
|
||
lease,
|
||
attempts: Number(row.attempts) + 1
|
||
}
|
||
}
|
||
|
||
// Ungated: a keyed one-row write must not queue behind claims, or a lease can lapse mid-send.
|
||
async renew(delivery: QueuedPushDelivery): Promise<void> {
|
||
await this.database.query(
|
||
"UPDATE push_delivery_batches SET lease_until = ? WHERE batch_id = ? AND lease_token = ? AND state = 'pending'",
|
||
[this.now() + DELIVERY_LEASE_MS, delivery.id, delivery.lease]
|
||
)
|
||
}
|
||
|
||
// A finished batch is deleted: accept() dedups on push_event_recipients, never on this row.
|
||
async finish(delivery: QueuedPushDelivery, retryAfterMs?: number): Promise<void> {
|
||
const now = this.now()
|
||
const retryAt = retryAfterMs === undefined ? Infinity : now + Math.max(1000, retryAfterMs)
|
||
if (retryAt >= delivery.expiresAt) {
|
||
await this.background.query(
|
||
"DELETE FROM push_delivery_batches WHERE batch_id = ? AND lease_token = ? AND state = 'pending'",
|
||
[delivery.id, delivery.lease]
|
||
)
|
||
return
|
||
}
|
||
await this.background.query(
|
||
`UPDATE push_delivery_batches SET payload_json = ?, due_at = ?, lease_until = 0, lease_token = NULL
|
||
WHERE batch_id = ? AND lease_token = ? AND state = 'pending'`,
|
||
[JSON.stringify(delivery.notification), retryAt, delivery.id, delivery.lease]
|
||
)
|
||
}
|
||
|
||
async pendingCount(registrationId: string): Promise<number> {
|
||
const [row] = await this.database.query(
|
||
"SELECT COUNT(*) AS total FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending'",
|
||
[registrationId]
|
||
)
|
||
return Number(row?.total ?? 0)
|
||
}
|
||
|
||
// Bounded per call and per statement, so it drains any backlog on its own without holding locks.
|
||
async prune(): Promise<PushPruneSweep> {
|
||
const now = this.now()
|
||
// Also clears terminal rows older revisions kept, since each carries a past expires_at.
|
||
let { deleted, saturated } = await this.deleteInBatches(
|
||
'push_delivery_batches',
|
||
'batch_id',
|
||
'expires_at <= ? AND lease_until <= ?',
|
||
[now, now]
|
||
)
|
||
// A day, not the 15-minute quota window: a host replaying an event after that must stay deduped.
|
||
for (const [table, key] of [
|
||
['push_dismissed_events', 'host_fingerprint, notification_epoch, notification_id'],
|
||
['push_event_recipients', 'event_id, registration_id'],
|
||
['push_events', 'event_id']
|
||
] as const) {
|
||
const sweep = await this.deleteInBatches(
|
||
table,
|
||
key,
|
||
'created_at < ?',
|
||
[now - RETENTION_MS],
|
||
'created_at'
|
||
)
|
||
deleted += sweep.deleted
|
||
saturated ||= sweep.saturated
|
||
}
|
||
return { deleted, saturated }
|
||
}
|
||
|
||
private async deleteInBatches(
|
||
table: string,
|
||
key: string,
|
||
where: string,
|
||
params: unknown[],
|
||
orderBy?: 'created_at'
|
||
): Promise<PushPruneSweep> {
|
||
const lockRows = this.background.dialect === 'postgres' ? ' FOR UPDATE SKIP LOCKED' : ''
|
||
const order = orderBy ? ` ORDER BY ${orderBy}` : ''
|
||
let total = 0
|
||
for (let batch = 0; batch < PRUNE_MAX_BATCHES; batch++) {
|
||
const [result] = await this.background.query(
|
||
`DELETE FROM ${table} WHERE (${key}) IN (SELECT ${key} FROM ${table} WHERE ${where}${order} LIMIT ${PRUNE_BATCH_ROWS}${lockRows})`,
|
||
params
|
||
)
|
||
const changes = Number(result?.changes ?? 0)
|
||
total += changes
|
||
// A short batch drained the predicate; only a full last batch leaves rows behind.
|
||
if (changes < PRUNE_BATCH_ROWS) return { deleted: total, saturated: false }
|
||
}
|
||
return { deleted: total, saturated: true }
|
||
}
|
||
}
|