@end-close/relay-sqlite 0.12.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 End Close
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @end-close/relay-sqlite
2
+
3
+ SQLite `EventStore` and `ControlStore` for [`@end-close/relay`](https://www.npmjs.com/package/@end-close/relay).
4
+ This is the store the End Close relay application ships with: rollback journal,
5
+ `synchronous=FULL`, safe on network filesystems (EFS/NFS), migrations inlined in the package.
6
+
7
+ ```sh
8
+ npm install @end-close/relay @end-close/relay-sqlite
9
+ ```
10
+
11
+ Install both at the same version. `@end-close/relay` is a peer dependency; the two packages are
12
+ released together in lockstep with the relay application.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { createRelay } from '@end-close/relay'
18
+ import { SqliteEventStore, SqliteControlStore, openDb, migrate } from '@end-close/relay-sqlite'
19
+
20
+ const db = openDb('/var/lib/myapp/relay.db')
21
+ migrate(db)
22
+
23
+ const relay = createRelay({
24
+ store: new SqliteEventStore(db),
25
+ control: new SqliteControlStore(db), // killswitch + per-route pause
26
+ // ...routes, secrets, endclose, encryption, maskingKey — see @end-close/relay
27
+ })
28
+ ```
29
+
30
+ `openDb` configures the connection (journal mode, busy timeout, foreign keys); `migrate` applies
31
+ the schema idempotently and is safe to run on every boot. Several relay instances may share one
32
+ database file; claiming is lease-based.
33
+
34
+ Requires Node `>=22.12`. Depends on `better-sqlite3`, which ships prebuilt binaries for the
35
+ common platforms.
36
+
37
+ See the [engine README](https://github.com/End-Close/endclose-relay/tree/main/packages/core#readme)
38
+ for the full embedding guide, and [`COMPATIBILITY.md`](https://github.com/End-Close/endclose-relay/blob/main/packages/core/COMPATIBILITY.md)
39
+ for what is a stable contract.
package/dist/busy.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { type Logger } from '@end-close/relay';
2
+ export declare const SQLITE_BUSY_TIMEOUT_MS = 15000;
3
+ export declare const BUSY_RETRY_ATTEMPTS = 3;
4
+ export declare const INGEST_BUSY_RETRY_ATTEMPTS = 2;
5
+ export declare function isSqliteBusy(err: unknown): boolean;
6
+ export interface BusyRetryOpts {
7
+ attempts?: number;
8
+ delayMs?: number;
9
+ logger?: Logger;
10
+ }
11
+ /** Retry a synchronous SQLite call after SQLITE_BUSY; sqlite's busy_timeout already waited once. */
12
+ export declare function withBusyRetry<T>(op: string, fn: () => T, opts?: BusyRetryOpts): Promise<T>;
13
+ //# sourceMappingURL=busy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"busy.d.ts","sourceRoot":"","sources":["../src/busy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAE1D,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAC5C,eAAO,MAAM,mBAAmB,IAAI,CAAA;AACpC,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAI3C,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGlD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,oGAAoG;AACpG,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,CAAC,EACX,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
package/dist/busy.js ADDED
@@ -0,0 +1,45 @@
1
+ import { noopLogger } from '@end-close/relay';
2
+ export const SQLITE_BUSY_TIMEOUT_MS = 15_000;
3
+ export const BUSY_RETRY_ATTEMPTS = 3;
4
+ export const INGEST_BUSY_RETRY_ATTEMPTS = 2;
5
+ const DEFAULT_DELAY_MS = 200;
6
+ export function isSqliteBusy(err) {
7
+ const code = err?.code;
8
+ return typeof code === 'string' && (code === 'SQLITE_BUSY' || code.startsWith('SQLITE_BUSY_'));
9
+ }
10
+ /** Retry a synchronous SQLite call after SQLITE_BUSY; sqlite's busy_timeout already waited once. */
11
+ export async function withBusyRetry(op, fn, opts = {}) {
12
+ const attempts = opts.attempts ?? BUSY_RETRY_ATTEMPTS;
13
+ const delayMs = opts.delayMs ?? DEFAULT_DELAY_MS;
14
+ const logger = opts.logger ?? noopLogger;
15
+ let last;
16
+ for (let i = 0; i < attempts; i++) {
17
+ try {
18
+ return fn();
19
+ }
20
+ catch (err) {
21
+ last = err;
22
+ if (!isSqliteBusy(err) || i === attempts - 1) {
23
+ if (isSqliteBusy(err)) {
24
+ logger.warn('sqlite busy exhausted', {
25
+ op,
26
+ attempts,
27
+ error: err.message,
28
+ });
29
+ }
30
+ throw err;
31
+ }
32
+ logger.warn('sqlite busy, retrying', {
33
+ op,
34
+ attempt: i + 1,
35
+ error: err.message,
36
+ });
37
+ await sleep(delayMs * (i + 1));
38
+ }
39
+ }
40
+ throw last;
41
+ }
42
+ function sleep(ms) {
43
+ return new Promise((r) => setTimeout(r, ms));
44
+ }
45
+ //# sourceMappingURL=busy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"busy.js","sourceRoot":"","sources":["../src/busy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAe,MAAM,kBAAkB,CAAA;AAE1D,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAA;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAA;AACpC,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAA;AAE3C,MAAM,gBAAgB,GAAG,GAAG,CAAA;AAE5B,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,MAAM,IAAI,GAAI,GAAgC,EAAE,IAAI,CAAA;IACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAA;AAChG,CAAC;AAQD,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,EAAU,EACV,EAAW,EACX,OAAsB,EAAE;IAExB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAA;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAA;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAA;IACxC,IAAI,IAAa,CAAA;IACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,OAAO,EAAE,EAAE,CAAA;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,GAAG,CAAA;YACV,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,GAAG,CAAC,EAAE,CAAC;gBAC7C,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtB,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE;wBACnC,EAAE;wBACF,QAAQ;wBACR,KAAK,EAAG,GAAa,CAAC,OAAO;qBAC9B,CAAC,CAAA;gBACJ,CAAC;gBACD,MAAM,GAAG,CAAA;YACX,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE;gBACnC,EAAE;gBACF,OAAO,EAAE,CAAC,GAAG,CAAC;gBACd,KAAK,EAAG,GAAa,CAAC,OAAO;aAC9B,CAAC,CAAA;YACF,MAAM,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;IACD,MAAM,IAAI,CAAA;AACZ,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AAC9C,CAAC"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import Database from 'better-sqlite3';
2
+ export type Db = Database.Database;
3
+ export declare function openDb(path: string): Db;
4
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAKrC,MAAM,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAA;AAElC,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAavC"}
package/dist/db.js ADDED
@@ -0,0 +1,20 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { SQLITE_BUSY_TIMEOUT_MS } from './busy.js';
5
+ export function openDb(path) {
6
+ if (path !== ':memory:')
7
+ mkdirSync(dirname(path), { recursive: true });
8
+ const db = new Database(path);
9
+ // DELETE (rollback journal), not WAL: WAL needs shared-memory primitives that network
10
+ // filesystems (EFS/NFS) do not provide correctly. Durability comes from synchronous=FULL
11
+ // below — an acked webhook must survive power loss (the store-and-forward contract).
12
+ db.pragma('journal_mode = DELETE');
13
+ db.pragma('synchronous = FULL');
14
+ db.pragma('foreign_keys = ON');
15
+ // EFS lock/fsync can exceed a few seconds; sqlite retries internally up to this timeout
16
+ // before throwing SQLITE_BUSY. Application-level retries sit on top (see withBusyRetry).
17
+ db.pragma(`busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
18
+ return db;
19
+ }
20
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAA;AAIlD,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,IAAI,IAAI,KAAK,UAAU;QAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACtE,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC7B,sFAAsF;IACtF,yFAAyF;IACzF,qFAAqF;IACrF,EAAE,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAA;IAClC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;IAC/B,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAC9B,wFAAwF;IACxF,yFAAyF;IACzF,EAAE,CAAC,MAAM,CAAC,kBAAkB,sBAAsB,EAAE,CAAC,CAAA;IACrD,OAAO,EAAE,CAAA;AACX,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { openDb, type Db } from './db.js';
2
+ export { migrate, MIGRATIONS, type Migration } from './migrations.js';
3
+ export { withBusyRetry, isSqliteBusy } from './busy.js';
4
+ export { SqliteEventStore, SqliteControlStore, runSqlite } from './store.js';
5
+ export { EventsRepo, type EventRow } from './repo/events.js';
6
+ export { KvRepo, ROUTE_PAUSED_PREFIX, type GlobalKillswitch } from './repo/kv.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC5E,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // @end-close/relay-sqlite — SQLite storage for the End Close relay engine.
2
+ export { openDb } from './db.js';
3
+ export { migrate, MIGRATIONS } from './migrations.js';
4
+ export { withBusyRetry, isSqliteBusy } from './busy.js';
5
+ export { SqliteEventStore, SqliteControlStore, runSqlite } from './store.js';
6
+ export { EventsRepo } from './repo/events.js';
7
+ export { KvRepo, ROUTE_PAUSED_PREFIX } from './repo/kv.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,OAAO,EAAE,MAAM,EAAW,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAkB,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC5E,OAAO,EAAE,UAAU,EAAiB,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAyB,MAAM,cAAc,CAAA"}
@@ -0,0 +1,8 @@
1
+ import type { Db } from './db.js';
2
+ export interface Migration {
3
+ name: string;
4
+ sql: string;
5
+ }
6
+ export declare const MIGRATIONS: Migration[];
7
+ export declare function migrate(db: Db, sets?: Migration[][]): void;
8
+ //# sourceMappingURL=migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAOjC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,eAAO,MAAM,UAAU,EAAE,SAAS,EA0CjC,CAAA;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,GAAE,SAAS,EAAE,EAAiB,GAAG,IAAI,CAuBxE"}
@@ -0,0 +1,62 @@
1
+ export const MIGRATIONS = [
2
+ {
3
+ name: '001_init.sql',
4
+ sql: `
5
+ CREATE TABLE events (
6
+ id INTEGER PRIMARY KEY,
7
+ route_id TEXT NOT NULL,
8
+ source TEXT NOT NULL,
9
+ event_id TEXT NOT NULL,
10
+ event_type TEXT,
11
+ payload_enc BLOB NOT NULL,
12
+ payload_iv BLOB NOT NULL,
13
+ headers_json TEXT NOT NULL,
14
+ received_at TEXT NOT NULL,
15
+ status TEXT NOT NULL DEFAULT 'pending'
16
+ CHECK (status IN ('pending','retry','delivering','delivered','parked','dropped_by_filter')),
17
+ attempts INTEGER NOT NULL DEFAULT 0,
18
+ next_attempt_at TEXT,
19
+ delivered_at TEXT,
20
+ bulk_request_id TEXT,
21
+ last_error TEXT,
22
+ idempotency_key TEXT NOT NULL,
23
+ UNIQUE (idempotency_key)
24
+ );
25
+ CREATE INDEX idx_events_dispatch ON events (status, next_attempt_at, route_id, id);
26
+ CREATE INDEX idx_events_route_status ON events (route_id, status);
27
+
28
+ CREATE TABLE kv (
29
+ key TEXT PRIMARY KEY,
30
+ value TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL
32
+ );
33
+ `,
34
+ },
35
+ {
36
+ // Lease-based claiming: a crashed or slow instance's rows are recoverable by others.
37
+ name: '002_leases.sql',
38
+ sql: `
39
+ ALTER TABLE events ADD COLUMN claimed_by TEXT;
40
+ ALTER TABLE events ADD COLUMN lease_until TEXT;
41
+ `,
42
+ },
43
+ ];
44
+ export function migrate(db, sets = [MIGRATIONS]) {
45
+ db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)');
46
+ const applied = new Set(db
47
+ .prepare('SELECT name FROM schema_migrations')
48
+ .all()
49
+ .map((r) => r.name));
50
+ for (const set of sets) {
51
+ for (const m of set) {
52
+ if (applied.has(m.name))
53
+ continue;
54
+ const run = db.transaction(() => {
55
+ db.exec(m.sql);
56
+ db.prepare('INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?)').run(m.name, new Date().toISOString());
57
+ });
58
+ run();
59
+ }
60
+ }
61
+ }
62
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,UAAU,GAAgB;IACrC;QACE,IAAI,EAAE,cAAc;QACpB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BR;KACE;IACD;QACE,qFAAqF;QACrF,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE;;;CAGR;KACE;CACF,CAAA;AAED,MAAM,UAAU,OAAO,CAAC,EAAM,EAAE,OAAsB,CAAC,UAAU,CAAC;IAChE,EAAE,CAAC,IAAI,CACL,gGAAgG,CACjG,CAAA;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,EAAE;SACC,OAAO,CAAC,oCAAoC,CAAC;SAC7C,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAsB,CAAC,IAAI,CAAC,CAC5C,CAAA;IACD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;YACpB,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAE,SAAQ;YACjC,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;gBAC9B,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;gBACd,EAAE,CAAC,OAAO,CAAC,gEAAgE,CAAC,CAAC,GAAG,CAC9E,CAAC,CAAC,IAAI,EACN,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CACzB,CAAA;YACH,CAAC,CAAC,CAAA;YACF,GAAG,EAAE,CAAA;QACP,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { Db } from '../db.js';
2
+ import type { EventStatus, RouteStats } from '@end-close/relay';
3
+ export type { EventStatus, RouteStats };
4
+ /** A raw row of the events table. */
5
+ export interface EventRow {
6
+ id: number;
7
+ route_id: string;
8
+ source: string;
9
+ event_id: string;
10
+ event_type: string | null;
11
+ payload_enc: Buffer;
12
+ payload_iv: Buffer;
13
+ headers_json: string;
14
+ received_at: string;
15
+ status: EventStatus;
16
+ attempts: number;
17
+ next_attempt_at: string | null;
18
+ delivered_at: string | null;
19
+ bulk_request_id: string | null;
20
+ last_error: string | null;
21
+ idempotency_key: string;
22
+ claimed_by: string | null;
23
+ lease_until: string | null;
24
+ }
25
+ export interface InsertEvent {
26
+ route_id: string;
27
+ source: string;
28
+ event_id: string;
29
+ event_type: string | null;
30
+ payload_enc: Buffer;
31
+ payload_iv: Buffer;
32
+ headers_json: string;
33
+ received_at: string;
34
+ status: EventStatus;
35
+ idempotency_key: string;
36
+ }
37
+ export type EventSummary = Omit<EventRow, 'payload_enc' | 'payload_iv' | 'headers_json' | 'idempotency_key' | 'claimed_by' | 'lease_until'>;
38
+ export declare class EventsRepo {
39
+ private db;
40
+ constructor(db: Db);
41
+ /** Returns the new row id, or null if the idempotency key already exists (duplicate delivery). */
42
+ insert(e: InsertEvent): number | null;
43
+ /** Claim due events for a route, oldest first, mark them 'delivering' and lease them. */
44
+ claimDue(routeId: string, now: string, limit: number, owner: string, leaseUntil: string): EventRow[];
45
+ markDelivered(ids: number[], deliveredAt: string, bulkRequestId: string | null): void;
46
+ markFailed(ids: number[], nextAttemptAt: string, error: string): void;
47
+ markParked(ids: number[], error: string): void;
48
+ /**
49
+ * Rows stuck in 'delivering' whose lease expired (or that belong to `owner`, or that
50
+ * predate leases) go back to 'retry'.
51
+ */
52
+ recoverDelivering(now: string, owner?: string): number;
53
+ /**
54
+ * Return leftover 'delivering' rows to 'retry' without touching already-settled rows.
55
+ * Used when a dispatch batch throws after claimDue. Increments attempts so a persist
56
+ * failure after a successful POST backs off instead of tight-looping.
57
+ */
58
+ releaseDelivering(ids: number[], nextAttemptAt: string, error: string): number;
59
+ routesWithDueEvents(now: string): string[];
60
+ countByStatus(): Record<string, number>;
61
+ getById(id: number): EventRow | undefined;
62
+ /** Park events that have been retrying longer than maxAgeMs (never silently dropped). */
63
+ parkExpired(now: string, maxAgeMs: number): number;
64
+ perRouteStats(): RouteStats[];
65
+ /** Payload-free event listing for the admin plane. */
66
+ list(filter: {
67
+ status?: EventStatus;
68
+ route?: string;
69
+ limit?: number;
70
+ }): EventSummary[];
71
+ /** Re-queue a parked event. Attempts reset so backoff starts fresh. */
72
+ replay(id: number): boolean;
73
+ replayAllParked(): number;
74
+ /**
75
+ * Retention. Terminal events (delivered / dropped_by_filter) lose their payload after
76
+ * `deliveredDays` (row kept as the idempotency ledger) and the row itself after
77
+ * `ledgerDays`. Parked events are never touched — they are unresolved by definition.
78
+ */
79
+ prune(now: string, deliveredDays: number, ledgerDays: number): {
80
+ wiped: number;
81
+ deleted: number;
82
+ };
83
+ /**
84
+ * One short exclusive lock: wipe a limited set of expired payloads, or delete a
85
+ * limited set of expired ledger rows, not both. Caller loops (and yields) so ingest
86
+ * and dispatch can run between batches on EFS.
87
+ */
88
+ pruneBatch(now: string, deliveredDays: number, ledgerDays: number, limit: number): {
89
+ wiped: number;
90
+ deleted: number;
91
+ };
92
+ }
93
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/repo/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAElC,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE/D,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,CAAA;AAEvC,qCAAqC;AACrC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,WAAW,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAC7B,QAAQ,EACR,aAAa,GAAG,YAAY,GAAG,cAAc,GAAG,iBAAiB,GAAG,YAAY,GAAG,aAAa,CACjG,CAAA;AAED,qBAAa,UAAU;IACT,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B,kGAAkG;IAClG,MAAM,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI;IAerC,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE;IA6BpG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAWrF,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAWrE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAU9C;;;OAGG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM;IAUtD;;;;OAIG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAW9E,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAW1C,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAOvC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIzC,yFAAyF;IACzF,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAUlD,aAAa,IAAI,UAAU,EAAE;IAkC7B,sDAAsD;IACtD,IAAI,CAAC,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,YAAY,EAAE;IAqBtF,uEAAuE;IACvE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAW3B,eAAe,IAAI,MAAM;IASzB;;;;OAIG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAYjG;;;;OAIG;IACH,UAAU,CACR,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,GACZ;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;CAqCtC"}
@@ -0,0 +1,227 @@
1
+ export class EventsRepo {
2
+ db;
3
+ constructor(db) {
4
+ this.db = db;
5
+ }
6
+ /** Returns the new row id, or null if the idempotency key already exists (duplicate delivery). */
7
+ insert(e) {
8
+ const res = this.db
9
+ .prepare(`INSERT INTO events
10
+ (route_id, source, event_id, event_type, payload_enc, payload_iv,
11
+ headers_json, received_at, status, next_attempt_at, idempotency_key)
12
+ VALUES
13
+ (@route_id, @source, @event_id, @event_type, @payload_enc, @payload_iv,
14
+ @headers_json, @received_at, @status, @received_at, @idempotency_key)
15
+ ON CONFLICT (idempotency_key) DO NOTHING`)
16
+ .run(e);
17
+ return res.changes === 0 ? null : Number(res.lastInsertRowid);
18
+ }
19
+ /** Claim due events for a route, oldest first, mark them 'delivering' and lease them. */
20
+ claimDue(routeId, now, limit, owner, leaseUntil) {
21
+ const claim = this.db.transaction(() => {
22
+ const rows = this.db
23
+ .prepare(`SELECT * FROM events
24
+ WHERE route_id = ? AND status IN ('pending','retry') AND next_attempt_at <= ?
25
+ ORDER BY id ASC LIMIT ?`)
26
+ .all(routeId, now, limit);
27
+ if (rows.length > 0) {
28
+ const ids = rows.map((r) => r.id);
29
+ this.db
30
+ .prepare(`UPDATE events SET status = 'delivering', next_attempt_at = ?, claimed_by = ?, lease_until = ?
31
+ WHERE id IN (${ids.map(() => '?').join(',')})`)
32
+ .run(now, owner, leaseUntil, ...ids);
33
+ for (const r of rows) {
34
+ r.status = 'delivering';
35
+ r.next_attempt_at = now;
36
+ r.claimed_by = owner;
37
+ r.lease_until = leaseUntil;
38
+ }
39
+ }
40
+ return rows;
41
+ });
42
+ return claim();
43
+ }
44
+ markDelivered(ids, deliveredAt, bulkRequestId) {
45
+ if (ids.length === 0)
46
+ return;
47
+ this.db
48
+ .prepare(`UPDATE events SET status = 'delivered', delivered_at = ?, bulk_request_id = ?, last_error = NULL,
49
+ claimed_by = NULL, lease_until = NULL
50
+ WHERE id IN (${ids.map(() => '?').join(',')})`)
51
+ .run(deliveredAt, bulkRequestId, ...ids);
52
+ }
53
+ markFailed(ids, nextAttemptAt, error) {
54
+ if (ids.length === 0)
55
+ return;
56
+ this.db
57
+ .prepare(`UPDATE events SET status = 'retry', attempts = attempts + 1,
58
+ next_attempt_at = ?, last_error = ?, claimed_by = NULL, lease_until = NULL
59
+ WHERE id IN (${ids.map(() => '?').join(',')})`)
60
+ .run(nextAttemptAt, error.slice(0, 500), ...ids);
61
+ }
62
+ markParked(ids, error) {
63
+ if (ids.length === 0)
64
+ return;
65
+ this.db
66
+ .prepare(`UPDATE events SET status = 'parked', last_error = ?, claimed_by = NULL, lease_until = NULL
67
+ WHERE id IN (${ids.map(() => '?').join(',')})`)
68
+ .run(error.slice(0, 500), ...ids);
69
+ }
70
+ /**
71
+ * Rows stuck in 'delivering' whose lease expired (or that belong to `owner`, or that
72
+ * predate leases) go back to 'retry'.
73
+ */
74
+ recoverDelivering(now, owner) {
75
+ return this.db
76
+ .prepare(`UPDATE events SET status = 'retry', next_attempt_at = ?, claimed_by = NULL, lease_until = NULL
77
+ WHERE status = 'delivering'
78
+ AND (lease_until IS NULL OR lease_until < ? OR claimed_by = ?)`)
79
+ .run(now, now, owner ?? null).changes;
80
+ }
81
+ /**
82
+ * Return leftover 'delivering' rows to 'retry' without touching already-settled rows.
83
+ * Used when a dispatch batch throws after claimDue. Increments attempts so a persist
84
+ * failure after a successful POST backs off instead of tight-looping.
85
+ */
86
+ releaseDelivering(ids, nextAttemptAt, error) {
87
+ if (ids.length === 0)
88
+ return 0;
89
+ return this.db
90
+ .prepare(`UPDATE events SET status = 'retry', attempts = attempts + 1,
91
+ next_attempt_at = ?, last_error = ?, claimed_by = NULL, lease_until = NULL
92
+ WHERE status = 'delivering' AND id IN (${ids.map(() => '?').join(',')})`)
93
+ .run(nextAttemptAt, error.slice(0, 500), ...ids).changes;
94
+ }
95
+ routesWithDueEvents(now) {
96
+ return this.db
97
+ .prepare(`SELECT DISTINCT route_id FROM events
98
+ WHERE status IN ('pending','retry') AND next_attempt_at <= ?`)
99
+ .all(now).map((r) => r.route_id);
100
+ }
101
+ countByStatus() {
102
+ const rows = this.db
103
+ .prepare('SELECT status, COUNT(*) AS n FROM events GROUP BY status')
104
+ .all();
105
+ return Object.fromEntries(rows.map((r) => [r.status, r.n]));
106
+ }
107
+ getById(id) {
108
+ return this.db.prepare('SELECT * FROM events WHERE id = ?').get(id);
109
+ }
110
+ /** Park events that have been retrying longer than maxAgeMs (never silently dropped). */
111
+ parkExpired(now, maxAgeMs) {
112
+ const cutoff = new Date(Date.parse(now) - maxAgeMs).toISOString();
113
+ return this.db
114
+ .prepare(`UPDATE events SET status = 'parked', last_error = 'retry window exhausted'
115
+ WHERE status = 'retry' AND received_at < ?`)
116
+ .run(cutoff).changes;
117
+ }
118
+ perRouteStats() {
119
+ const counts = this.db
120
+ .prepare('SELECT route_id, status, COUNT(*) AS n FROM events GROUP BY route_id, status')
121
+ .all();
122
+ const extremes = this.db
123
+ .prepare(`SELECT route_id,
124
+ MAX(delivered_at) AS last_delivered_at,
125
+ MIN(CASE WHEN status IN ('pending','retry') THEN received_at END) AS oldest_pending_at
126
+ FROM events GROUP BY route_id`)
127
+ .all();
128
+ const byRoute = new Map();
129
+ for (const row of counts) {
130
+ const stats = byRoute.get(row.route_id) ??
131
+ { route_id: row.route_id, counts: {}, last_delivered_at: null, oldest_pending_at: null };
132
+ stats.counts[row.status] = row.n;
133
+ byRoute.set(row.route_id, stats);
134
+ }
135
+ for (const row of extremes) {
136
+ const stats = byRoute.get(row.route_id);
137
+ if (stats) {
138
+ stats.last_delivered_at = row.last_delivered_at;
139
+ stats.oldest_pending_at = row.oldest_pending_at;
140
+ }
141
+ }
142
+ return [...byRoute.values()];
143
+ }
144
+ /** Payload-free event listing for the admin plane. */
145
+ list(filter) {
146
+ const clauses = [];
147
+ const params = [];
148
+ if (filter.status) {
149
+ clauses.push('status = ?');
150
+ params.push(filter.status);
151
+ }
152
+ if (filter.route) {
153
+ clauses.push('route_id = ?');
154
+ params.push(filter.route);
155
+ }
156
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
157
+ return this.db
158
+ .prepare(`SELECT id, route_id, source, event_id, event_type, received_at, status,
159
+ attempts, next_attempt_at, delivered_at, bulk_request_id, last_error
160
+ FROM events ${where} ORDER BY id DESC LIMIT ?`)
161
+ .all(...params, filter.limit ?? 50);
162
+ }
163
+ /** Re-queue a parked event. Attempts reset so backoff starts fresh. */
164
+ replay(id) {
165
+ return (this.db
166
+ .prepare(`UPDATE events SET status = 'retry', attempts = 0, next_attempt_at = ?, last_error = NULL
167
+ WHERE id = ? AND status = 'parked'`)
168
+ .run(new Date().toISOString(), id).changes === 1);
169
+ }
170
+ replayAllParked() {
171
+ return this.db
172
+ .prepare(`UPDATE events SET status = 'retry', attempts = 0, next_attempt_at = ?, last_error = NULL
173
+ WHERE status = 'parked'`)
174
+ .run(new Date().toISOString()).changes;
175
+ }
176
+ /**
177
+ * Retention. Terminal events (delivered / dropped_by_filter) lose their payload after
178
+ * `deliveredDays` (row kept as the idempotency ledger) and the row itself after
179
+ * `ledgerDays`. Parked events are never touched — they are unresolved by definition.
180
+ */
181
+ prune(now, deliveredDays, ledgerDays) {
182
+ let wiped = 0;
183
+ let deleted = 0;
184
+ for (;;) {
185
+ const batch = this.pruneBatch(now, deliveredDays, ledgerDays, 500);
186
+ wiped += batch.wiped;
187
+ deleted += batch.deleted;
188
+ if (batch.wiped === 0 && batch.deleted === 0)
189
+ break;
190
+ }
191
+ return { wiped, deleted };
192
+ }
193
+ /**
194
+ * One short exclusive lock: wipe a limited set of expired payloads, or delete a
195
+ * limited set of expired ledger rows, not both. Caller loops (and yields) so ingest
196
+ * and dispatch can run between batches on EFS.
197
+ */
198
+ pruneBatch(now, deliveredDays, ledgerDays, limit) {
199
+ const wipeCutoff = new Date(Date.parse(now) - deliveredDays * 86_400_000).toISOString();
200
+ const deleteCutoff = new Date(Date.parse(now) - ledgerDays * 86_400_000).toISOString();
201
+ const wipeIds = this.db
202
+ .prepare(`SELECT id FROM events
203
+ WHERE status IN ('delivered','dropped_by_filter')
204
+ AND received_at < ? AND length(payload_enc) > 0
205
+ LIMIT ?`)
206
+ .all(wipeCutoff, limit).map((r) => r.id);
207
+ if (wipeIds.length > 0) {
208
+ const wiped = this.db
209
+ .prepare(`UPDATE events SET payload_enc = x'', payload_iv = x'', headers_json = '{}'
210
+ WHERE id IN (${wipeIds.map(() => '?').join(',')})`)
211
+ .run(...wipeIds).changes;
212
+ return { wiped, deleted: 0 };
213
+ }
214
+ const deleteIds = this.db
215
+ .prepare(`SELECT id FROM events
216
+ WHERE status IN ('delivered','dropped_by_filter') AND received_at < ?
217
+ LIMIT ?`)
218
+ .all(deleteCutoff, limit).map((r) => r.id);
219
+ if (deleteIds.length === 0)
220
+ return { wiped: 0, deleted: 0 };
221
+ const deleted = this.db
222
+ .prepare(`DELETE FROM events WHERE id IN (${deleteIds.map(() => '?').join(',')})`)
223
+ .run(...deleteIds).changes;
224
+ return { wiped: 0, deleted };
225
+ }
226
+ }
227
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/repo/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { Killswitch } from '@end-close/relay';
2
+ import type { Db } from '../db.js';
3
+ export type GlobalKillswitch = Killswitch;
4
+ /** kv key prefix for per-route pause flags: `route_paused.<routeId>` = '1'. */
5
+ export declare const ROUTE_PAUSED_PREFIX = "route_paused.";
6
+ export declare class KvRepo {
7
+ private db;
8
+ constructor(db: Db);
9
+ get(key: string): string | undefined;
10
+ set(key: string, value: string): void;
11
+ delete(key: string): void;
12
+ globalKillswitch(): GlobalKillswitch;
13
+ setGlobalKillswitch(state: GlobalKillswitch): void;
14
+ isRoutePaused(routeId: string): boolean;
15
+ setRoutePaused(routeId: string, paused: boolean): void;
16
+ /** Every paused route id in one query. */
17
+ pausedRoutes(): Set<string>;
18
+ }
19
+ //# sourceMappingURL=kv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../../src/repo/kv.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAElC,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAA;AAEzC,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,kBAAkB,CAAA;AAElD,qBAAa,MAAM;IACL,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOpC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IASrC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,gBAAgB,IAAI,gBAAgB;IAKpC,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAIlD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIvC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAKtD,0CAA0C;IAC1C,YAAY,IAAI,GAAG,CAAC,MAAM,CAAC;CAM5B"}