@outbox-event-bus/sqlite-better-sqlite3-outbox 2.0.2 → 2.0.3
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/dist/index.cjs +61 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +61 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/sqlite-better-sqlite3-outbox.ts +102 -80
package/dist/index.cjs
CHANGED
|
@@ -68,6 +68,10 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
68
68
|
config;
|
|
69
69
|
db;
|
|
70
70
|
poller;
|
|
71
|
+
archiveStatement;
|
|
72
|
+
deleteStatement;
|
|
73
|
+
fetchStatement;
|
|
74
|
+
failStatement;
|
|
71
75
|
constructor(config) {
|
|
72
76
|
if (config.db) this.db = config.db;
|
|
73
77
|
else {
|
|
@@ -98,6 +102,27 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
98
102
|
}
|
|
99
103
|
init() {
|
|
100
104
|
this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName));
|
|
105
|
+
this.archiveStatement = this.db.prepare(`
|
|
106
|
+
INSERT INTO ${this.config.archiveTableName} (
|
|
107
|
+
id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on
|
|
108
|
+
) VALUES (?, ?, ?, ?, '${outbox_event_bus.EventStatus.COMPLETED}', ?, ?, ?, ?, ?)
|
|
109
|
+
`);
|
|
110
|
+
this.deleteStatement = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`);
|
|
111
|
+
this.fetchStatement = this.db.prepare(`
|
|
112
|
+
SELECT * FROM ${this.config.tableName}
|
|
113
|
+
WHERE status = '${outbox_event_bus.EventStatus.CREATED}'
|
|
114
|
+
OR (status = '${outbox_event_bus.EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)
|
|
115
|
+
OR (status = '${outbox_event_bus.EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))
|
|
116
|
+
LIMIT ?
|
|
117
|
+
`);
|
|
118
|
+
this.failStatement = this.db.prepare(`
|
|
119
|
+
UPDATE ${this.config.tableName}
|
|
120
|
+
SET status = '${outbox_event_bus.EventStatus.FAILED}',
|
|
121
|
+
retry_count = ?,
|
|
122
|
+
last_error = ?,
|
|
123
|
+
next_retry_at = ?
|
|
124
|
+
WHERE id = ?
|
|
125
|
+
`);
|
|
101
126
|
}
|
|
102
127
|
async publish(events, transaction) {
|
|
103
128
|
if (events.length === 0) return;
|
|
@@ -148,16 +173,31 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
148
173
|
await this.poller.stop();
|
|
149
174
|
}
|
|
150
175
|
async processBatch(handler) {
|
|
176
|
+
const lockedEvents = this.lockBatch();
|
|
177
|
+
if (lockedEvents.length === 0) return;
|
|
151
178
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
152
179
|
const msNow = Date.now();
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
const completedEvents = [];
|
|
181
|
+
for (const lockedEvent of lockedEvents) {
|
|
182
|
+
const event = {
|
|
183
|
+
id: lockedEvent.id,
|
|
184
|
+
type: lockedEvent.type,
|
|
185
|
+
payload: JSON.parse(lockedEvent.payload),
|
|
186
|
+
occurredAt: new Date(lockedEvent.occurred_at)
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
await handler(event);
|
|
190
|
+
completedEvents.push(lockedEvent);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.handleEventFailure(lockedEvent, event, error, msNow);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (completedEvents.length > 0) this.archiveBatch(completedEvents, now);
|
|
196
|
+
}
|
|
197
|
+
lockBatch() {
|
|
198
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
return this.db.transaction(() => {
|
|
200
|
+
const rows = this.fetchStatement.all(this.config.maxRetries, now, now, this.config.batchSize);
|
|
161
201
|
if (rows.length === 0) return [];
|
|
162
202
|
const ids = rows.map((r) => r.id);
|
|
163
203
|
const placeholders = ids.map(() => "?").join(",");
|
|
@@ -170,48 +210,19 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
170
210
|
`).run(now, now, ...ids);
|
|
171
211
|
return rows;
|
|
172
212
|
})();
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const lockedEvent
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
completedEvents.push({
|
|
187
|
-
event,
|
|
188
|
-
lockedEvent
|
|
189
|
-
});
|
|
190
|
-
} catch (error) {
|
|
191
|
-
const retryCount = lockedEvent.retry_count + 1;
|
|
192
|
-
(0, outbox_event_bus.reportEventError)(this.poller.onError, error, event, retryCount, this.config.maxRetries);
|
|
193
|
-
const delay = this.poller.calculateBackoff(retryCount);
|
|
194
|
-
this.db.prepare(`
|
|
195
|
-
UPDATE ${this.config.tableName}
|
|
196
|
-
SET status = '${outbox_event_bus.EventStatus.FAILED}',
|
|
197
|
-
retry_count = ?,
|
|
198
|
-
last_error = ?,
|
|
199
|
-
next_retry_at = ?
|
|
200
|
-
WHERE id = ?
|
|
201
|
-
`).run(retryCount, (0, outbox_event_bus.formatErrorMessage)(error), new Date(msNow + delay).toISOString(), lockedEvent.id);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (completedEvents.length > 0) this.db.transaction(() => {
|
|
205
|
-
const insertArchive = this.db.prepare(`
|
|
206
|
-
INSERT INTO ${this.config.archiveTableName} (
|
|
207
|
-
id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on
|
|
208
|
-
) VALUES (?, ?, ?, ?, '${outbox_event_bus.EventStatus.COMPLETED}', ?, ?, ?, ?, ?)
|
|
209
|
-
`);
|
|
210
|
-
const deleteEvent = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`);
|
|
211
|
-
const completionTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
212
|
-
for (const { lockedEvent } of completedEvents) {
|
|
213
|
-
insertArchive.run(lockedEvent.id, lockedEvent.type, lockedEvent.payload, lockedEvent.occurred_at, lockedEvent.retry_count, lockedEvent.last_error, lockedEvent.created_on, now, completionTime);
|
|
214
|
-
deleteEvent.run(lockedEvent.id);
|
|
213
|
+
}
|
|
214
|
+
handleEventFailure(lockedEvent, event, error, msNow) {
|
|
215
|
+
const retryCount = lockedEvent.retry_count + 1;
|
|
216
|
+
(0, outbox_event_bus.reportEventError)(this.poller.onError, error, event, retryCount, this.config.maxRetries);
|
|
217
|
+
const delay = this.poller.calculateBackoff(retryCount);
|
|
218
|
+
this.failStatement.run(retryCount, (0, outbox_event_bus.formatErrorMessage)(error), new Date(msNow + delay).toISOString(), lockedEvent.id);
|
|
219
|
+
}
|
|
220
|
+
archiveBatch(completedEvents, now) {
|
|
221
|
+
const completionTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
222
|
+
this.db.transaction(() => {
|
|
223
|
+
for (const lockedEvent of completedEvents) {
|
|
224
|
+
this.archiveStatement.run(lockedEvent.id, lockedEvent.type, lockedEvent.payload, lockedEvent.occurred_at, lockedEvent.retry_count, lockedEvent.last_error, lockedEvent.created_on, now, completionTime);
|
|
225
|
+
this.deleteStatement.run(lockedEvent.id);
|
|
215
226
|
}
|
|
216
227
|
})();
|
|
217
228
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["EventStatus","Database","PollingService","event: FailedBusEvent","busEvents: BusEvent[]","completedEvents: { event: BusEvent; lockedEvent: OutboxRow }[]","betterSqlite3TransactionStorage: AsyncLocalStorage<Database>","AsyncLocalStorage"],"sources":["../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import Database from \"better-sqlite3\"\nimport {\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\nconst DEFAULT_EXPIRE_IN_SECONDS = 300\n\nconst getOutboxSchema = (tableName: string, archiveTableName: string) => `\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT '${EventStatus.CREATED}',\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n next_retry_at TEXT,\n created_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),\n started_on TEXT,\n completed_on TEXT,\n keep_alive TEXT,\n expire_in_seconds INTEGER NOT NULL DEFAULT ${DEFAULT_EXPIRE_IN_SECONDS}\n );\n\n CREATE TABLE IF NOT EXISTS ${archiveTableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL,\n retry_count INTEGER NOT NULL,\n last_error TEXT,\n created_on TEXT NOT NULL,\n started_on TEXT,\n completed_on TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_status_retry ON ${tableName} (status, next_retry_at);\n`\n\nexport interface SqliteBetterSqlite3OutboxConfig extends OutboxConfig {\n dbPath?: string\n db?: Database.Database\n getTransaction?: (() => Database.Database | undefined) | undefined\n tableName?: string\n archiveTableName?: string\n}\n\ninterface OutboxRow {\n id: string\n type: string\n payload: string\n occurred_at: string\n status: EventStatus\n retry_count: number\n next_retry_at: string | null\n last_error: string | null\n created_on: string\n started_on: string | null\n completed_on: string | null\n keep_alive: string | null\n expire_in_seconds: number\n}\n\nexport class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {\n private readonly config: Required<SqliteBetterSqlite3OutboxConfig>\n private readonly db: Database.Database\n private readonly poller: PollingService\n\n constructor(config: SqliteBetterSqlite3OutboxConfig) {\n if (config.db) {\n this.db = config.db\n } else {\n if (!config.dbPath) throw new Error(\"dbPath is required if db is not provided\")\n this.db = new Database(config.dbPath)\n this.db.pragma(\"journal_mode = WAL\")\n }\n\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n dbPath: config.dbPath ?? \"\",\n db: this.db,\n getTransaction: config.getTransaction,\n tableName: config.tableName ?? \"outbox_events\",\n archiveTableName: config.archiveTableName ?? \"outbox_events_archive\",\n } as Required<SqliteBetterSqlite3OutboxConfig>\n\n this.init()\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n private init() {\n this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName))\n }\n\n async publish(events: BusEvent[], transaction?: Database.Database): Promise<void> {\n if (events.length === 0) return\n\n const executor = transaction ?? this.config.getTransaction?.() ?? this.db\n\n const insert = executor.prepare(`\n INSERT INTO ${this.config.tableName} (id, type, payload, occurred_at, status)\n VALUES (?, ?, ?, ?, '${EventStatus.CREATED}')\n `)\n\n executor.transaction(() => {\n for (const event of events) {\n insert.run(\n event.id,\n event.type,\n JSON.stringify(event.payload),\n event.occurredAt.toISOString()\n )\n }\n })()\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const rows = this.db\n .prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.FAILED}'\n ORDER BY occurred_at DESC\n LIMIT 100\n `)\n .all() as OutboxRow[]\n\n return rows.map((row) => {\n const event: FailedBusEvent = {\n id: row.id,\n type: row.type,\n payload: JSON.parse(row.payload),\n occurredAt: new Date(row.occurred_at),\n retryCount: row.retry_count,\n }\n if (row.last_error) event.error = row.last_error\n if (row.started_on) event.lastAttemptAt = new Date(row.started_on)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n const placeholders = eventIds.map(() => \"?\").join(\",\")\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.CREATED}',\n retry_count = 0,\n next_retry_at = NULL,\n last_error = NULL\n WHERE id IN (${placeholders})\n `)\n .run(...eventIds)\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const now = new Date().toISOString()\n const msNow = Date.now()\n\n const lockedEvents = this.db.transaction(() => {\n // Select events that are ready to process:\n // 1. New events (status = 'created')\n // 2. Failed events that can be retried (retry_count < max AND next_retry_at has passed)\n // 3. Stuck events (status = 'active' but keepAlive + expire_in_seconds < now)\n const rows = this.db\n .prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.CREATED}'\n OR (status = '${EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)\n OR (status = '${EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))\n LIMIT ?\n `)\n .all(this.config.maxRetries, now, now, this.config.batchSize) as OutboxRow[]\n\n if (rows.length === 0) return []\n\n const ids = rows.map((r) => r.id)\n const placeholders = ids.map(() => \"?\").join(\",\")\n\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.ACTIVE}',\n started_on = ?,\n keep_alive = ?\n WHERE id IN (${placeholders})\n `)\n .run(now, now, ...ids)\n\n return rows\n })()\n\n if (lockedEvents.length === 0) return\n\n const busEvents: BusEvent[] = lockedEvents.map((row) => ({\n id: row.id,\n type: row.type,\n payload: JSON.parse(row.payload),\n occurredAt: new Date(row.occurred_at),\n }))\n\n const completedEvents: { event: BusEvent; lockedEvent: OutboxRow }[] = []\n\n for (let index = 0; index < busEvents.length; index++) {\n const event = busEvents[index]!\n const lockedEvent = lockedEvents[index]!\n\n try {\n await handler(event)\n completedEvents.push({ event, lockedEvent })\n } catch (error) {\n const retryCount = lockedEvent.retry_count + 1\n reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries)\n\n const delay = this.poller.calculateBackoff(retryCount)\n\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.FAILED}',\n retry_count = ?,\n last_error = ?,\n next_retry_at = ?\n WHERE id = ?\n `)\n .run(\n retryCount,\n formatErrorMessage(error),\n new Date(msNow + delay).toISOString(),\n lockedEvent.id\n )\n }\n }\n\n if (completedEvents.length > 0) {\n this.db.transaction(() => {\n const insertArchive = this.db.prepare(`\n INSERT INTO ${this.config.archiveTableName} (\n id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on\n ) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)\n `)\n const deleteEvent = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`)\n\n const completionTime = new Date().toISOString()\n for (const { lockedEvent } of completedEvents) {\n insertArchive.run(\n lockedEvent.id,\n lockedEvent.type,\n lockedEvent.payload,\n lockedEvent.occurred_at,\n lockedEvent.retry_count,\n lockedEvent.last_error,\n lockedEvent.created_on,\n now,\n completionTime\n )\n deleteEvent.run(lockedEvent.id)\n }\n })()\n }\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type { Database } from \"better-sqlite3\"\n\nexport type { Database }\n\nexport const betterSqlite3TransactionStorage: AsyncLocalStorage<Database> =\n new AsyncLocalStorage<Database>()\n\nexport async function withBetterSqlite3Transaction<T>(\n db: Database,\n fn: (tx: Database) => Promise<T>\n): Promise<T> {\n return betterSqlite3TransactionStorage.run(db, async () => {\n if (db.inTransaction) {\n const savepointName = `sp_${Date.now()}_${Math.random().toString(36).slice(2)}`\n db.prepare(`SAVEPOINT ${savepointName}`).run()\n try {\n const result = await fn(db)\n db.prepare(`RELEASE ${savepointName}`).run()\n return result\n } catch (error) {\n db.prepare(`ROLLBACK TO ${savepointName}`).run()\n db.prepare(`RELEASE ${savepointName}`).run()\n throw error\n }\n } else {\n db.prepare(\"BEGIN\").run()\n try {\n const result = await fn(db)\n db.prepare(\"COMMIT\").run()\n return result\n } catch (error) {\n db.prepare(\"ROLLBACK\").run()\n throw error\n }\n }\n })\n}\n\nexport function getBetterSqlite3Transaction(): () => Database | undefined {\n return () => betterSqlite3TransactionStorage.getStore()\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["EventStatus","Database","PollingService","event: FailedBusEvent","completedEvents: OutboxRow[]","event: BusEvent","betterSqlite3TransactionStorage: AsyncLocalStorage<Database>","AsyncLocalStorage"],"sources":["../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import Database from \"better-sqlite3\"\nimport {\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\nconst DEFAULT_EXPIRE_IN_SECONDS = 300\n\nconst getOutboxSchema = (tableName: string, archiveTableName: string) => `\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT '${EventStatus.CREATED}',\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n next_retry_at TEXT,\n created_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),\n started_on TEXT,\n completed_on TEXT,\n keep_alive TEXT,\n expire_in_seconds INTEGER NOT NULL DEFAULT ${DEFAULT_EXPIRE_IN_SECONDS}\n );\n\n CREATE TABLE IF NOT EXISTS ${archiveTableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL,\n retry_count INTEGER NOT NULL,\n last_error TEXT,\n created_on TEXT NOT NULL,\n started_on TEXT,\n completed_on TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_status_retry ON ${tableName} (status, next_retry_at);\n`\n\nexport interface SqliteBetterSqlite3OutboxConfig extends OutboxConfig {\n dbPath?: string\n db?: Database.Database\n getTransaction?: (() => Database.Database | undefined) | undefined\n tableName?: string\n archiveTableName?: string\n}\n\ninterface OutboxRow {\n id: string\n type: string\n payload: string\n occurred_at: string\n status: EventStatus\n retry_count: number\n next_retry_at: string | null\n last_error: string | null\n created_on: string\n started_on: string | null\n completed_on: string | null\n keep_alive: string | null\n expire_in_seconds: number\n}\n\nexport class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {\n private readonly config: Required<SqliteBetterSqlite3OutboxConfig>\n private readonly db: Database.Database\n private readonly poller: PollingService\n\n private archiveStatement!: Database.Statement\n private deleteStatement!: Database.Statement\n private fetchStatement!: Database.Statement\n private failStatement!: Database.Statement\n\n constructor(config: SqliteBetterSqlite3OutboxConfig) {\n if (config.db) {\n this.db = config.db\n } else {\n if (!config.dbPath) throw new Error(\"dbPath is required if db is not provided\")\n this.db = new Database(config.dbPath)\n this.db.pragma(\"journal_mode = WAL\")\n }\n\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n dbPath: config.dbPath ?? \"\",\n db: this.db,\n getTransaction: config.getTransaction,\n tableName: config.tableName ?? \"outbox_events\",\n archiveTableName: config.archiveTableName ?? \"outbox_events_archive\",\n } as Required<SqliteBetterSqlite3OutboxConfig>\n\n this.init()\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n private init() {\n this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName))\n\n this.archiveStatement = this.db.prepare(`\n INSERT INTO ${this.config.archiveTableName} (\n id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on\n ) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)\n `)\n\n this.deleteStatement = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`)\n\n this.fetchStatement = this.db.prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.CREATED}'\n OR (status = '${EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)\n OR (status = '${EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))\n LIMIT ?\n `)\n\n this.failStatement = this.db.prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.FAILED}',\n retry_count = ?,\n last_error = ?,\n next_retry_at = ?\n WHERE id = ?\n `)\n }\n\n async publish(events: BusEvent[], transaction?: Database.Database): Promise<void> {\n if (events.length === 0) return\n\n const executor = transaction ?? this.config.getTransaction?.() ?? this.db\n\n const insert = executor.prepare(`\n INSERT INTO ${this.config.tableName} (id, type, payload, occurred_at, status)\n VALUES (?, ?, ?, ?, '${EventStatus.CREATED}')\n `)\n\n executor.transaction(() => {\n for (const event of events) {\n insert.run(\n event.id,\n event.type,\n JSON.stringify(event.payload),\n event.occurredAt.toISOString()\n )\n }\n })()\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const rows = this.db\n .prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.FAILED}'\n ORDER BY occurred_at DESC\n LIMIT 100\n `)\n .all() as OutboxRow[]\n\n return rows.map((row) => {\n const event: FailedBusEvent = {\n id: row.id,\n type: row.type,\n payload: JSON.parse(row.payload),\n occurredAt: new Date(row.occurred_at),\n retryCount: row.retry_count,\n }\n if (row.last_error) event.error = row.last_error\n if (row.started_on) event.lastAttemptAt = new Date(row.started_on)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n const placeholders = eventIds.map(() => \"?\").join(\",\")\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.CREATED}',\n retry_count = 0,\n next_retry_at = NULL,\n last_error = NULL\n WHERE id IN (${placeholders})\n `)\n .run(...eventIds)\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const lockedEvents = this.lockBatch()\n if (lockedEvents.length === 0) return\n\n const now = new Date().toISOString()\n const msNow = Date.now()\n const completedEvents: OutboxRow[] = []\n\n for (const lockedEvent of lockedEvents) {\n const event: BusEvent = {\n id: lockedEvent.id,\n type: lockedEvent.type,\n payload: JSON.parse(lockedEvent.payload),\n occurredAt: new Date(lockedEvent.occurred_at),\n }\n\n try {\n await handler(event)\n completedEvents.push(lockedEvent)\n } catch (error) {\n this.handleEventFailure(lockedEvent, event, error, msNow)\n }\n }\n\n if (completedEvents.length > 0) {\n this.archiveBatch(completedEvents, now)\n }\n }\n\n private lockBatch(): OutboxRow[] {\n const now = new Date().toISOString()\n\n return this.db.transaction(() => {\n const rows = this.fetchStatement.all(\n this.config.maxRetries,\n now,\n now,\n this.config.batchSize\n ) as OutboxRow[]\n\n if (rows.length === 0) return []\n\n const ids = rows.map((r) => r.id)\n const placeholders = ids.map(() => \"?\").join(\",\")\n\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.ACTIVE}',\n started_on = ?,\n keep_alive = ?\n WHERE id IN (${placeholders})\n `)\n .run(now, now, ...ids)\n\n return rows\n })()\n }\n\n private handleEventFailure(\n lockedEvent: OutboxRow,\n event: BusEvent,\n error: unknown,\n msNow: number\n ) {\n const retryCount = lockedEvent.retry_count + 1\n reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries)\n\n const delay = this.poller.calculateBackoff(retryCount)\n\n this.failStatement.run(\n retryCount,\n formatErrorMessage(error),\n new Date(msNow + delay).toISOString(),\n lockedEvent.id\n )\n }\n\n private archiveBatch(completedEvents: OutboxRow[], now: string) {\n const completionTime = new Date().toISOString()\n\n this.db.transaction(() => {\n for (const lockedEvent of completedEvents) {\n this.archiveStatement.run(\n lockedEvent.id,\n lockedEvent.type,\n lockedEvent.payload,\n lockedEvent.occurred_at,\n lockedEvent.retry_count,\n lockedEvent.last_error,\n lockedEvent.created_on,\n now,\n completionTime\n )\n this.deleteStatement.run(lockedEvent.id)\n }\n })()\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type { Database } from \"better-sqlite3\"\n\nexport type { Database }\n\nexport const betterSqlite3TransactionStorage: AsyncLocalStorage<Database> =\n new AsyncLocalStorage<Database>()\n\nexport async function withBetterSqlite3Transaction<T>(\n db: Database,\n fn: (tx: Database) => Promise<T>\n): Promise<T> {\n return betterSqlite3TransactionStorage.run(db, async () => {\n if (db.inTransaction) {\n const savepointName = `sp_${Date.now()}_${Math.random().toString(36).slice(2)}`\n db.prepare(`SAVEPOINT ${savepointName}`).run()\n try {\n const result = await fn(db)\n db.prepare(`RELEASE ${savepointName}`).run()\n return result\n } catch (error) {\n db.prepare(`ROLLBACK TO ${savepointName}`).run()\n db.prepare(`RELEASE ${savepointName}`).run()\n throw error\n }\n } else {\n db.prepare(\"BEGIN\").run()\n try {\n const result = await fn(db)\n db.prepare(\"COMMIT\").run()\n return result\n } catch (error) {\n db.prepare(\"ROLLBACK\").run()\n throw error\n }\n }\n })\n}\n\nexport function getBetterSqlite3Transaction(): () => Database | undefined {\n return () => betterSqlite3TransactionStorage.getStore()\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -56,6 +56,10 @@ declare class SqliteBetterSqlite3Outbox implements IOutbox<Database$1.Database>
|
|
|
56
56
|
private readonly config;
|
|
57
57
|
private readonly db;
|
|
58
58
|
private readonly poller;
|
|
59
|
+
private archiveStatement;
|
|
60
|
+
private deleteStatement;
|
|
61
|
+
private fetchStatement;
|
|
62
|
+
private failStatement;
|
|
59
63
|
constructor(config: SqliteBetterSqlite3OutboxConfig);
|
|
60
64
|
private init;
|
|
61
65
|
publish(events: BusEvent[], transaction?: Database$1.Database): Promise<void>;
|
|
@@ -64,6 +68,9 @@ declare class SqliteBetterSqlite3Outbox implements IOutbox<Database$1.Database>
|
|
|
64
68
|
start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void;
|
|
65
69
|
stop(): Promise<void>;
|
|
66
70
|
private processBatch;
|
|
71
|
+
private lockBatch;
|
|
72
|
+
private handleEventFailure;
|
|
73
|
+
private archiveBatch;
|
|
67
74
|
}
|
|
68
75
|
//#endregion
|
|
69
76
|
//#region src/transaction-storage.d.ts
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;UAEiB,kBAAA;UACP,WAAW;;EADJ,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAkB;AAMnC;AACmB,uBADG,WACH,CAAA,iBAAA,kBAAA,GAAqB,kBAArB,CAAA,SACT,KAAA,CADS;EAAqB,OAAA,CAAA,EAErB,QAFqB,GAAA,SAAA;EAErB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAEoC,QAFpC;;;;KCPP;;EDFK,IAAA,ECIT,CDJS;EAMK,OAAA,ECDX,CDCW;EACH,UAAA,ECDL,IDCK;EAAqB,QAAA,CAAA,ECA3B,MDA2B,CAAA,MAAA,EAAA,OAAA,CAAA;CAErB;AAEoC,KCD3C,cDC2C,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCDc,QDCd,CCDuB,CDCvB,ECD0B,CDC1B,CAAA,GAAA;EAH7C,KAAA,CAAA,EAAA,MAAA;EAAK,UAAA,EAAA,MAAA;kBCKG;;AARP,KAsBC,YAAA,GAtBD,CAAA,KAAA,EAsBwB,WAtBxB,EAAA,GAAA,IAAA;;;UCJM;oBACG,0BAA0B,iBAAiB;EFF9C,KAAA,EAAA,CAAA,OAAA,EAAA,CAAA,KAAkB,EEGR,QFFjB,EAAA,GEE8B,OFFnB,CAAA,IAAA,CAAA,EAAA,OAAc,EEE6B,YFF7B,EAAA,GAAA,IAAA;EAKb,IAAA,EAAA,GAAA,GEFR,OFEmB,CAAA,IAAA,CAAA;EACd,eAAA,EAAA,GAAA,GEFM,OFEN,CEFc,cFEd,EAAA,CAAA;EAAqB,WAAA,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,EAAA,GEDD,OFCC,CAAA,IAAA,CAAA;;AEJb,UA6DV,YAAA,CA7DU;EAAa,UAAA,CAAA,EAAA,MAAA;EAAwB,aAAA,CAAA,EAAA,MAAA;EAClD,cAAA,CAAA,EAAA,MAAA;EACmB,SAAA,CAAA,EAAA,MAAA;EAAR,mBAAA,CAAA,EAAA,MAAA;EACc,iBAAA,CAAA,EAAA,MAAA;;;;UCwCtB,+BAAA,SAAwC;;EH9CxC,EAAA,CAAA,EGgDV,UAAA,CAAS,QHhDmB;EAMb,cAAW,CAAA,EAAA,CAAA,GAAA,GG2CP,UAAA,CAAS,QH3CF,GAAA,SAAA,CAAA,GAAA,SAAA;EACd,SAAA,CAAA,EAAA,MAAA;EAAqB,gBAAA,CAAA,EAAA,MAAA;;AAIe,cG2D1C,yBAAA,YAAqC,OH3DK,CG2DG,UAAA,CAAS,QH3DZ,CAAA,CAAA;EAH7C,iBAAA,MAAA;EAAK,iBAAA,EAAA
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -56,6 +56,10 @@ declare class SqliteBetterSqlite3Outbox implements IOutbox<Database$1.Database>
|
|
|
56
56
|
private readonly config;
|
|
57
57
|
private readonly db;
|
|
58
58
|
private readonly poller;
|
|
59
|
+
private archiveStatement;
|
|
60
|
+
private deleteStatement;
|
|
61
|
+
private fetchStatement;
|
|
62
|
+
private failStatement;
|
|
59
63
|
constructor(config: SqliteBetterSqlite3OutboxConfig);
|
|
60
64
|
private init;
|
|
61
65
|
publish(events: BusEvent[], transaction?: Database$1.Database): Promise<void>;
|
|
@@ -64,6 +68,9 @@ declare class SqliteBetterSqlite3Outbox implements IOutbox<Database$1.Database>
|
|
|
64
68
|
start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void;
|
|
65
69
|
stop(): Promise<void>;
|
|
66
70
|
private processBatch;
|
|
71
|
+
private lockBatch;
|
|
72
|
+
private handleEventFailure;
|
|
73
|
+
private archiveBatch;
|
|
67
74
|
}
|
|
68
75
|
//#endregion
|
|
69
76
|
//#region src/transaction-storage.d.ts
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;UAEiB,kBAAA;UACP,WAAW;;EADJ,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAkB;AAMnC;AACmB,uBADG,WACH,CAAA,iBAAA,kBAAA,GAAqB,kBAArB,CAAA,SACT,KAAA,CADS;EAAqB,OAAA,CAAA,EAErB,QAFqB,GAAA,SAAA;EAErB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAEoC,QAFpC;;;;KCPP;;EDFK,IAAA,ECIT,CDJS;EAMK,OAAA,ECDX,CDCW;EACH,UAAA,ECDL,IDCK;EAAqB,QAAA,CAAA,ECA3B,MDA2B,CAAA,MAAA,EAAA,OAAA,CAAA;CAErB;AAEoC,KCD3C,cDC2C,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCDc,QDCd,CCDuB,CDCvB,ECD0B,CDC1B,CAAA,GAAA;EAH7C,KAAA,CAAA,EAAA,MAAA;EAAK,UAAA,EAAA,MAAA;kBCKG;;AARP,KAsBC,YAAA,GAtBD,CAAA,KAAA,EAsBwB,WAtBxB,EAAA,GAAA,IAAA;;;UCJM;oBACG,0BAA0B,iBAAiB;EFF9C,KAAA,EAAA,CAAA,OAAA,EAAA,CAAA,KAAkB,EEGR,QFFjB,EAAA,GEE8B,OFFnB,CAAA,IAAA,CAAA,EAAA,OAAc,EEE6B,YFF7B,EAAA,GAAA,IAAA;EAKb,IAAA,EAAA,GAAA,GEFR,OFEmB,CAAA,IAAA,CAAA;EACd,eAAA,EAAA,GAAA,GEFM,OFEN,CEFc,cFEd,EAAA,CAAA;EAAqB,WAAA,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,EAAA,GEDD,OFCC,CAAA,IAAA,CAAA;;AEJb,UA6DV,YAAA,CA7DU;EAAa,UAAA,CAAA,EAAA,MAAA;EAAwB,aAAA,CAAA,EAAA,MAAA;EAClD,cAAA,CAAA,EAAA,MAAA;EACmB,SAAA,CAAA,EAAA,MAAA;EAAR,mBAAA,CAAA,EAAA,MAAA;EACc,iBAAA,CAAA,EAAA,MAAA;;;;UCwCtB,+BAAA,SAAwC;;EH9CxC,EAAA,CAAA,EGgDV,UAAA,CAAS,QHhDmB;EAMb,cAAW,CAAA,EAAA,CAAA,GAAA,GG2CP,UAAA,CAAS,QH3CF,GAAA,SAAA,CAAA,GAAA,SAAA;EACd,SAAA,CAAA,EAAA,MAAA;EAAqB,gBAAA,CAAA,EAAA,MAAA;;AAIe,cG2D1C,yBAAA,YAAqC,OH3DK,CG2DG,UAAA,CAAS,QH3DZ,CAAA,CAAA;EAH7C,iBAAA,MAAA;EAAK,iBAAA,EAAA
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../core/src/errors/errors.ts","../../../core/src/types/types.ts","../../../core/src/types/interfaces.ts","../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":[],"mappings":";;;;UAEiB,kBAAA;UACP,WAAW;;EADJ,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAkB;AAMnC;AACmB,uBADG,WACH,CAAA,iBAAA,kBAAA,GAAqB,kBAArB,CAAA,SACT,KAAA,CADS;EAAqB,OAAA,CAAA,EAErB,QAFqB,GAAA,SAAA;EAErB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAEoC,QAFpC;;;;KCPP;;EDFK,IAAA,ECIT,CDJS;EAMK,OAAA,ECDX,CDCW;EACH,UAAA,ECDL,IDCK;EAAqB,QAAA,CAAA,ECA3B,MDA2B,CAAA,MAAA,EAAA,OAAA,CAAA;CAErB;AAEoC,KCD3C,cDC2C,CAAA,UAAA,MAAA,GAAA,MAAA,EAAA,IAAA,OAAA,CAAA,GCDc,QDCd,CCDuB,CDCvB,ECD0B,CDC1B,CAAA,GAAA;EAH7C,KAAA,CAAA,EAAA,MAAA;EAAK,UAAA,EAAA,MAAA;kBCKG;;AARP,KAsBC,YAAA,GAtBD,CAAA,KAAA,EAsBwB,WAtBxB,EAAA,GAAA,IAAA;;;UCJM;oBACG,0BAA0B,iBAAiB;EFF9C,KAAA,EAAA,CAAA,OAAA,EAAA,CAAA,KAAkB,EEGR,QFFjB,EAAA,GEE8B,OFFnB,CAAA,IAAA,CAAA,EAAA,OAAc,EEE6B,YFF7B,EAAA,GAAA,IAAA;EAKb,IAAA,EAAA,GAAA,GEFR,OFEmB,CAAA,IAAA,CAAA;EACd,eAAA,EAAA,GAAA,GEFM,OFEN,CEFc,cFEd,EAAA,CAAA;EAAqB,WAAA,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,EAAA,GEDD,OFCC,CAAA,IAAA,CAAA;;AEJb,UA6DV,YAAA,CA7DU;EAAa,UAAA,CAAA,EAAA,MAAA;EAAwB,aAAA,CAAA,EAAA,MAAA;EAClD,cAAA,CAAA,EAAA,MAAA;EACmB,SAAA,CAAA,EAAA,MAAA;EAAR,mBAAA,CAAA,EAAA,MAAA;EACc,iBAAA,CAAA,EAAA,MAAA;;;;UCwCtB,+BAAA,SAAwC;;EH9CxC,EAAA,CAAA,EGgDV,UAAA,CAAS,QHhDmB;EAMb,cAAW,CAAA,EAAA,CAAA,GAAA,GG2CP,UAAA,CAAS,QH3CF,GAAA,SAAA,CAAA,GAAA,SAAA;EACd,SAAA,CAAA,EAAA,MAAA;EAAqB,gBAAA,CAAA,EAAA,MAAA;;AAIe,cG2D1C,yBAAA,YAAqC,OH3DK,CG2DG,UAAA,CAAS,QH3DZ,CAAA,CAAA;EAH7C,iBAAA,MAAA;EAAK,iBAAA,EAAA;;;;ECNH,QAAA,cAAQ;EAEZ,QAAA,aAAA;EACG,WAAA,CAAA,MAAA,EE2EW,+BF3EX;EACG,QAAA,IAAA;EACD,OAAA,CAAA,MAAA,EEuIW,QFvIX,EAAA,EAAA,WAAA,CAAA,EEuIqC,UAAA,CAAS,QFvI9C,CAAA,EEuIyD,OFvIzD,CAAA,IAAA,CAAA;EAAM,eAAA,CAAA,CAAA,EE6JQ,OF7JR,CE6JgB,cF7JhB,EAAA,CAAA;EAGP,WAAA,CAAA,QAAc,EAAA,MAAA,EAAA,CAAA,EEkLe,OFlLf,CAAA,IAAA,CAAA;EAAoD,KAAA,CAAA,OAAA,EAAA,CAAA,KAAA,EEkMrD,QFlMqD,EAAA,GEkMxC,OFlMwC,CAAA,IAAA,CAAA,EAAA,OAAA,EEkMhB,YFlMgB,CAAA,EAAA,IAAA;EAAG,IAAA,CAAA,CAAA,EEsMjE,OFtMiE,CAAA,IAAA,CAAA;EAAZ,QAAA,YAAA;EAGnD,QAAA,SAAA;EAAI,QAAA,kBAAA;EAcV,QAAA,YAAY;;;;cGxBX,iCAAiC,kBAAkB;AJH/C,iBIMK,4BJLD,CAAA,CAAA,CAAA,CAAA,EAAA,EIMf,QJN6B,EAAA,EAAA,EAAA,CAAA,EAAA,EIOxB,QJPwB,EAAA,GIOX,OJPW,CIOH,CJPG,CAAA,CAAA,EIQhC,OJRgC,CIQxB,CJRwB,CAAA;AAKb,iBI+BN,2BAAA,CAAA,CJ/BiB,EAAA,GAAA,GI+BoB,QJ/BpB,GAAA,SAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -40,6 +40,10 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
40
40
|
config;
|
|
41
41
|
db;
|
|
42
42
|
poller;
|
|
43
|
+
archiveStatement;
|
|
44
|
+
deleteStatement;
|
|
45
|
+
fetchStatement;
|
|
46
|
+
failStatement;
|
|
43
47
|
constructor(config) {
|
|
44
48
|
if (config.db) this.db = config.db;
|
|
45
49
|
else {
|
|
@@ -70,6 +74,27 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
70
74
|
}
|
|
71
75
|
init() {
|
|
72
76
|
this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName));
|
|
77
|
+
this.archiveStatement = this.db.prepare(`
|
|
78
|
+
INSERT INTO ${this.config.archiveTableName} (
|
|
79
|
+
id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on
|
|
80
|
+
) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)
|
|
81
|
+
`);
|
|
82
|
+
this.deleteStatement = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`);
|
|
83
|
+
this.fetchStatement = this.db.prepare(`
|
|
84
|
+
SELECT * FROM ${this.config.tableName}
|
|
85
|
+
WHERE status = '${EventStatus.CREATED}'
|
|
86
|
+
OR (status = '${EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)
|
|
87
|
+
OR (status = '${EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))
|
|
88
|
+
LIMIT ?
|
|
89
|
+
`);
|
|
90
|
+
this.failStatement = this.db.prepare(`
|
|
91
|
+
UPDATE ${this.config.tableName}
|
|
92
|
+
SET status = '${EventStatus.FAILED}',
|
|
93
|
+
retry_count = ?,
|
|
94
|
+
last_error = ?,
|
|
95
|
+
next_retry_at = ?
|
|
96
|
+
WHERE id = ?
|
|
97
|
+
`);
|
|
73
98
|
}
|
|
74
99
|
async publish(events, transaction) {
|
|
75
100
|
if (events.length === 0) return;
|
|
@@ -120,16 +145,31 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
120
145
|
await this.poller.stop();
|
|
121
146
|
}
|
|
122
147
|
async processBatch(handler) {
|
|
148
|
+
const lockedEvents = this.lockBatch();
|
|
149
|
+
if (lockedEvents.length === 0) return;
|
|
123
150
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
124
151
|
const msNow = Date.now();
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
const completedEvents = [];
|
|
153
|
+
for (const lockedEvent of lockedEvents) {
|
|
154
|
+
const event = {
|
|
155
|
+
id: lockedEvent.id,
|
|
156
|
+
type: lockedEvent.type,
|
|
157
|
+
payload: JSON.parse(lockedEvent.payload),
|
|
158
|
+
occurredAt: new Date(lockedEvent.occurred_at)
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
await handler(event);
|
|
162
|
+
completedEvents.push(lockedEvent);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.handleEventFailure(lockedEvent, event, error, msNow);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (completedEvents.length > 0) this.archiveBatch(completedEvents, now);
|
|
168
|
+
}
|
|
169
|
+
lockBatch() {
|
|
170
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
171
|
+
return this.db.transaction(() => {
|
|
172
|
+
const rows = this.fetchStatement.all(this.config.maxRetries, now, now, this.config.batchSize);
|
|
133
173
|
if (rows.length === 0) return [];
|
|
134
174
|
const ids = rows.map((r) => r.id);
|
|
135
175
|
const placeholders = ids.map(() => "?").join(",");
|
|
@@ -142,48 +182,19 @@ var SqliteBetterSqlite3Outbox = class {
|
|
|
142
182
|
`).run(now, now, ...ids);
|
|
143
183
|
return rows;
|
|
144
184
|
})();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const lockedEvent
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
completedEvents.push({
|
|
159
|
-
event,
|
|
160
|
-
lockedEvent
|
|
161
|
-
});
|
|
162
|
-
} catch (error) {
|
|
163
|
-
const retryCount = lockedEvent.retry_count + 1;
|
|
164
|
-
reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries);
|
|
165
|
-
const delay = this.poller.calculateBackoff(retryCount);
|
|
166
|
-
this.db.prepare(`
|
|
167
|
-
UPDATE ${this.config.tableName}
|
|
168
|
-
SET status = '${EventStatus.FAILED}',
|
|
169
|
-
retry_count = ?,
|
|
170
|
-
last_error = ?,
|
|
171
|
-
next_retry_at = ?
|
|
172
|
-
WHERE id = ?
|
|
173
|
-
`).run(retryCount, formatErrorMessage(error), new Date(msNow + delay).toISOString(), lockedEvent.id);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (completedEvents.length > 0) this.db.transaction(() => {
|
|
177
|
-
const insertArchive = this.db.prepare(`
|
|
178
|
-
INSERT INTO ${this.config.archiveTableName} (
|
|
179
|
-
id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on
|
|
180
|
-
) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)
|
|
181
|
-
`);
|
|
182
|
-
const deleteEvent = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`);
|
|
183
|
-
const completionTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
184
|
-
for (const { lockedEvent } of completedEvents) {
|
|
185
|
-
insertArchive.run(lockedEvent.id, lockedEvent.type, lockedEvent.payload, lockedEvent.occurred_at, lockedEvent.retry_count, lockedEvent.last_error, lockedEvent.created_on, now, completionTime);
|
|
186
|
-
deleteEvent.run(lockedEvent.id);
|
|
185
|
+
}
|
|
186
|
+
handleEventFailure(lockedEvent, event, error, msNow) {
|
|
187
|
+
const retryCount = lockedEvent.retry_count + 1;
|
|
188
|
+
reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries);
|
|
189
|
+
const delay = this.poller.calculateBackoff(retryCount);
|
|
190
|
+
this.failStatement.run(retryCount, formatErrorMessage(error), new Date(msNow + delay).toISOString(), lockedEvent.id);
|
|
191
|
+
}
|
|
192
|
+
archiveBatch(completedEvents, now) {
|
|
193
|
+
const completionTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
194
|
+
this.db.transaction(() => {
|
|
195
|
+
for (const lockedEvent of completedEvents) {
|
|
196
|
+
this.archiveStatement.run(lockedEvent.id, lockedEvent.type, lockedEvent.payload, lockedEvent.occurred_at, lockedEvent.retry_count, lockedEvent.last_error, lockedEvent.created_on, now, completionTime);
|
|
197
|
+
this.deleteStatement.run(lockedEvent.id);
|
|
187
198
|
}
|
|
188
199
|
})();
|
|
189
200
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["event: FailedBusEvent","busEvents: BusEvent[]","completedEvents: { event: BusEvent; lockedEvent: OutboxRow }[]","betterSqlite3TransactionStorage: AsyncLocalStorage<Database>"],"sources":["../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import Database from \"better-sqlite3\"\nimport {\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\nconst DEFAULT_EXPIRE_IN_SECONDS = 300\n\nconst getOutboxSchema = (tableName: string, archiveTableName: string) => `\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT '${EventStatus.CREATED}',\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n next_retry_at TEXT,\n created_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),\n started_on TEXT,\n completed_on TEXT,\n keep_alive TEXT,\n expire_in_seconds INTEGER NOT NULL DEFAULT ${DEFAULT_EXPIRE_IN_SECONDS}\n );\n\n CREATE TABLE IF NOT EXISTS ${archiveTableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL,\n retry_count INTEGER NOT NULL,\n last_error TEXT,\n created_on TEXT NOT NULL,\n started_on TEXT,\n completed_on TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_status_retry ON ${tableName} (status, next_retry_at);\n`\n\nexport interface SqliteBetterSqlite3OutboxConfig extends OutboxConfig {\n dbPath?: string\n db?: Database.Database\n getTransaction?: (() => Database.Database | undefined) | undefined\n tableName?: string\n archiveTableName?: string\n}\n\ninterface OutboxRow {\n id: string\n type: string\n payload: string\n occurred_at: string\n status: EventStatus\n retry_count: number\n next_retry_at: string | null\n last_error: string | null\n created_on: string\n started_on: string | null\n completed_on: string | null\n keep_alive: string | null\n expire_in_seconds: number\n}\n\nexport class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {\n private readonly config: Required<SqliteBetterSqlite3OutboxConfig>\n private readonly db: Database.Database\n private readonly poller: PollingService\n\n constructor(config: SqliteBetterSqlite3OutboxConfig) {\n if (config.db) {\n this.db = config.db\n } else {\n if (!config.dbPath) throw new Error(\"dbPath is required if db is not provided\")\n this.db = new Database(config.dbPath)\n this.db.pragma(\"journal_mode = WAL\")\n }\n\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n dbPath: config.dbPath ?? \"\",\n db: this.db,\n getTransaction: config.getTransaction,\n tableName: config.tableName ?? \"outbox_events\",\n archiveTableName: config.archiveTableName ?? \"outbox_events_archive\",\n } as Required<SqliteBetterSqlite3OutboxConfig>\n\n this.init()\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n private init() {\n this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName))\n }\n\n async publish(events: BusEvent[], transaction?: Database.Database): Promise<void> {\n if (events.length === 0) return\n\n const executor = transaction ?? this.config.getTransaction?.() ?? this.db\n\n const insert = executor.prepare(`\n INSERT INTO ${this.config.tableName} (id, type, payload, occurred_at, status)\n VALUES (?, ?, ?, ?, '${EventStatus.CREATED}')\n `)\n\n executor.transaction(() => {\n for (const event of events) {\n insert.run(\n event.id,\n event.type,\n JSON.stringify(event.payload),\n event.occurredAt.toISOString()\n )\n }\n })()\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const rows = this.db\n .prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.FAILED}'\n ORDER BY occurred_at DESC\n LIMIT 100\n `)\n .all() as OutboxRow[]\n\n return rows.map((row) => {\n const event: FailedBusEvent = {\n id: row.id,\n type: row.type,\n payload: JSON.parse(row.payload),\n occurredAt: new Date(row.occurred_at),\n retryCount: row.retry_count,\n }\n if (row.last_error) event.error = row.last_error\n if (row.started_on) event.lastAttemptAt = new Date(row.started_on)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n const placeholders = eventIds.map(() => \"?\").join(\",\")\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.CREATED}',\n retry_count = 0,\n next_retry_at = NULL,\n last_error = NULL\n WHERE id IN (${placeholders})\n `)\n .run(...eventIds)\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const now = new Date().toISOString()\n const msNow = Date.now()\n\n const lockedEvents = this.db.transaction(() => {\n // Select events that are ready to process:\n // 1. New events (status = 'created')\n // 2. Failed events that can be retried (retry_count < max AND next_retry_at has passed)\n // 3. Stuck events (status = 'active' but keepAlive + expire_in_seconds < now)\n const rows = this.db\n .prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.CREATED}'\n OR (status = '${EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)\n OR (status = '${EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))\n LIMIT ?\n `)\n .all(this.config.maxRetries, now, now, this.config.batchSize) as OutboxRow[]\n\n if (rows.length === 0) return []\n\n const ids = rows.map((r) => r.id)\n const placeholders = ids.map(() => \"?\").join(\",\")\n\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.ACTIVE}',\n started_on = ?,\n keep_alive = ?\n WHERE id IN (${placeholders})\n `)\n .run(now, now, ...ids)\n\n return rows\n })()\n\n if (lockedEvents.length === 0) return\n\n const busEvents: BusEvent[] = lockedEvents.map((row) => ({\n id: row.id,\n type: row.type,\n payload: JSON.parse(row.payload),\n occurredAt: new Date(row.occurred_at),\n }))\n\n const completedEvents: { event: BusEvent; lockedEvent: OutboxRow }[] = []\n\n for (let index = 0; index < busEvents.length; index++) {\n const event = busEvents[index]!\n const lockedEvent = lockedEvents[index]!\n\n try {\n await handler(event)\n completedEvents.push({ event, lockedEvent })\n } catch (error) {\n const retryCount = lockedEvent.retry_count + 1\n reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries)\n\n const delay = this.poller.calculateBackoff(retryCount)\n\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.FAILED}',\n retry_count = ?,\n last_error = ?,\n next_retry_at = ?\n WHERE id = ?\n `)\n .run(\n retryCount,\n formatErrorMessage(error),\n new Date(msNow + delay).toISOString(),\n lockedEvent.id\n )\n }\n }\n\n if (completedEvents.length > 0) {\n this.db.transaction(() => {\n const insertArchive = this.db.prepare(`\n INSERT INTO ${this.config.archiveTableName} (\n id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on\n ) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)\n `)\n const deleteEvent = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`)\n\n const completionTime = new Date().toISOString()\n for (const { lockedEvent } of completedEvents) {\n insertArchive.run(\n lockedEvent.id,\n lockedEvent.type,\n lockedEvent.payload,\n lockedEvent.occurred_at,\n lockedEvent.retry_count,\n lockedEvent.last_error,\n lockedEvent.created_on,\n now,\n completionTime\n )\n deleteEvent.run(lockedEvent.id)\n }\n })()\n }\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type { Database } from \"better-sqlite3\"\n\nexport type { Database }\n\nexport const betterSqlite3TransactionStorage: AsyncLocalStorage<Database> =\n new AsyncLocalStorage<Database>()\n\nexport async function withBetterSqlite3Transaction<T>(\n db: Database,\n fn: (tx: Database) => Promise<T>\n): Promise<T> {\n return betterSqlite3TransactionStorage.run(db, async () => {\n if (db.inTransaction) {\n const savepointName = `sp_${Date.now()}_${Math.random().toString(36).slice(2)}`\n db.prepare(`SAVEPOINT ${savepointName}`).run()\n try {\n const result = await fn(db)\n db.prepare(`RELEASE ${savepointName}`).run()\n return result\n } catch (error) {\n db.prepare(`ROLLBACK TO ${savepointName}`).run()\n db.prepare(`RELEASE ${savepointName}`).run()\n throw error\n }\n } else {\n db.prepare(\"BEGIN\").run()\n try {\n const result = await fn(db)\n db.prepare(\"COMMIT\").run()\n return result\n } catch (error) {\n db.prepare(\"ROLLBACK\").run()\n throw error\n }\n }\n })\n}\n\nexport function getBetterSqlite3Transaction(): () => Database | undefined {\n return () => betterSqlite3TransactionStorage.getStore()\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["event: FailedBusEvent","completedEvents: OutboxRow[]","event: BusEvent","betterSqlite3TransactionStorage: AsyncLocalStorage<Database>"],"sources":["../src/sqlite-better-sqlite3-outbox.ts","../src/transaction-storage.ts"],"sourcesContent":["import Database from \"better-sqlite3\"\nimport {\n type BusEvent,\n type ErrorHandler,\n EventStatus,\n type FailedBusEvent,\n formatErrorMessage,\n type IOutbox,\n type OutboxConfig,\n PollingService,\n reportEventError,\n} from \"outbox-event-bus\"\n\nconst DEFAULT_EXPIRE_IN_SECONDS = 300\n\nconst getOutboxSchema = (tableName: string, archiveTableName: string) => `\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT '${EventStatus.CREATED}',\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n next_retry_at TEXT,\n created_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),\n started_on TEXT,\n completed_on TEXT,\n keep_alive TEXT,\n expire_in_seconds INTEGER NOT NULL DEFAULT ${DEFAULT_EXPIRE_IN_SECONDS}\n );\n\n CREATE TABLE IF NOT EXISTS ${archiveTableName} (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL,\n occurred_at TEXT NOT NULL,\n status TEXT NOT NULL,\n retry_count INTEGER NOT NULL,\n last_error TEXT,\n created_on TEXT NOT NULL,\n started_on TEXT,\n completed_on TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_status_retry ON ${tableName} (status, next_retry_at);\n`\n\nexport interface SqliteBetterSqlite3OutboxConfig extends OutboxConfig {\n dbPath?: string\n db?: Database.Database\n getTransaction?: (() => Database.Database | undefined) | undefined\n tableName?: string\n archiveTableName?: string\n}\n\ninterface OutboxRow {\n id: string\n type: string\n payload: string\n occurred_at: string\n status: EventStatus\n retry_count: number\n next_retry_at: string | null\n last_error: string | null\n created_on: string\n started_on: string | null\n completed_on: string | null\n keep_alive: string | null\n expire_in_seconds: number\n}\n\nexport class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {\n private readonly config: Required<SqliteBetterSqlite3OutboxConfig>\n private readonly db: Database.Database\n private readonly poller: PollingService\n\n private archiveStatement!: Database.Statement\n private deleteStatement!: Database.Statement\n private fetchStatement!: Database.Statement\n private failStatement!: Database.Statement\n\n constructor(config: SqliteBetterSqlite3OutboxConfig) {\n if (config.db) {\n this.db = config.db\n } else {\n if (!config.dbPath) throw new Error(\"dbPath is required if db is not provided\")\n this.db = new Database(config.dbPath)\n this.db.pragma(\"journal_mode = WAL\")\n }\n\n this.config = {\n batchSize: config.batchSize ?? 50,\n pollIntervalMs: config.pollIntervalMs ?? 1000,\n maxRetries: config.maxRetries ?? 5,\n baseBackoffMs: config.baseBackoffMs ?? 1000,\n processingTimeoutMs: config.processingTimeoutMs ?? 30000,\n maxErrorBackoffMs: config.maxErrorBackoffMs ?? 30000,\n dbPath: config.dbPath ?? \"\",\n db: this.db,\n getTransaction: config.getTransaction,\n tableName: config.tableName ?? \"outbox_events\",\n archiveTableName: config.archiveTableName ?? \"outbox_events_archive\",\n } as Required<SqliteBetterSqlite3OutboxConfig>\n\n this.init()\n\n this.poller = new PollingService({\n pollIntervalMs: this.config.pollIntervalMs,\n baseBackoffMs: this.config.baseBackoffMs,\n maxErrorBackoffMs: this.config.maxErrorBackoffMs,\n processBatch: (handler) => this.processBatch(handler),\n })\n }\n\n private init() {\n this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName))\n\n this.archiveStatement = this.db.prepare(`\n INSERT INTO ${this.config.archiveTableName} (\n id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on\n ) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)\n `)\n\n this.deleteStatement = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`)\n\n this.fetchStatement = this.db.prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.CREATED}'\n OR (status = '${EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)\n OR (status = '${EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))\n LIMIT ?\n `)\n\n this.failStatement = this.db.prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.FAILED}',\n retry_count = ?,\n last_error = ?,\n next_retry_at = ?\n WHERE id = ?\n `)\n }\n\n async publish(events: BusEvent[], transaction?: Database.Database): Promise<void> {\n if (events.length === 0) return\n\n const executor = transaction ?? this.config.getTransaction?.() ?? this.db\n\n const insert = executor.prepare(`\n INSERT INTO ${this.config.tableName} (id, type, payload, occurred_at, status)\n VALUES (?, ?, ?, ?, '${EventStatus.CREATED}')\n `)\n\n executor.transaction(() => {\n for (const event of events) {\n insert.run(\n event.id,\n event.type,\n JSON.stringify(event.payload),\n event.occurredAt.toISOString()\n )\n }\n })()\n }\n\n async getFailedEvents(): Promise<FailedBusEvent[]> {\n const rows = this.db\n .prepare(`\n SELECT * FROM ${this.config.tableName}\n WHERE status = '${EventStatus.FAILED}'\n ORDER BY occurred_at DESC\n LIMIT 100\n `)\n .all() as OutboxRow[]\n\n return rows.map((row) => {\n const event: FailedBusEvent = {\n id: row.id,\n type: row.type,\n payload: JSON.parse(row.payload),\n occurredAt: new Date(row.occurred_at),\n retryCount: row.retry_count,\n }\n if (row.last_error) event.error = row.last_error\n if (row.started_on) event.lastAttemptAt = new Date(row.started_on)\n return event\n })\n }\n\n async retryEvents(eventIds: string[]): Promise<void> {\n if (eventIds.length === 0) return\n\n const placeholders = eventIds.map(() => \"?\").join(\",\")\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.CREATED}',\n retry_count = 0,\n next_retry_at = NULL,\n last_error = NULL\n WHERE id IN (${placeholders})\n `)\n .run(...eventIds)\n }\n\n start(handler: (event: BusEvent) => Promise<void>, onError: ErrorHandler): void {\n this.poller.start(handler, onError)\n }\n\n async stop(): Promise<void> {\n await this.poller.stop()\n }\n\n private async processBatch(handler: (event: BusEvent) => Promise<void>) {\n const lockedEvents = this.lockBatch()\n if (lockedEvents.length === 0) return\n\n const now = new Date().toISOString()\n const msNow = Date.now()\n const completedEvents: OutboxRow[] = []\n\n for (const lockedEvent of lockedEvents) {\n const event: BusEvent = {\n id: lockedEvent.id,\n type: lockedEvent.type,\n payload: JSON.parse(lockedEvent.payload),\n occurredAt: new Date(lockedEvent.occurred_at),\n }\n\n try {\n await handler(event)\n completedEvents.push(lockedEvent)\n } catch (error) {\n this.handleEventFailure(lockedEvent, event, error, msNow)\n }\n }\n\n if (completedEvents.length > 0) {\n this.archiveBatch(completedEvents, now)\n }\n }\n\n private lockBatch(): OutboxRow[] {\n const now = new Date().toISOString()\n\n return this.db.transaction(() => {\n const rows = this.fetchStatement.all(\n this.config.maxRetries,\n now,\n now,\n this.config.batchSize\n ) as OutboxRow[]\n\n if (rows.length === 0) return []\n\n const ids = rows.map((r) => r.id)\n const placeholders = ids.map(() => \"?\").join(\",\")\n\n this.db\n .prepare(`\n UPDATE ${this.config.tableName}\n SET status = '${EventStatus.ACTIVE}',\n started_on = ?,\n keep_alive = ?\n WHERE id IN (${placeholders})\n `)\n .run(now, now, ...ids)\n\n return rows\n })()\n }\n\n private handleEventFailure(\n lockedEvent: OutboxRow,\n event: BusEvent,\n error: unknown,\n msNow: number\n ) {\n const retryCount = lockedEvent.retry_count + 1\n reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries)\n\n const delay = this.poller.calculateBackoff(retryCount)\n\n this.failStatement.run(\n retryCount,\n formatErrorMessage(error),\n new Date(msNow + delay).toISOString(),\n lockedEvent.id\n )\n }\n\n private archiveBatch(completedEvents: OutboxRow[], now: string) {\n const completionTime = new Date().toISOString()\n\n this.db.transaction(() => {\n for (const lockedEvent of completedEvents) {\n this.archiveStatement.run(\n lockedEvent.id,\n lockedEvent.type,\n lockedEvent.payload,\n lockedEvent.occurred_at,\n lockedEvent.retry_count,\n lockedEvent.last_error,\n lockedEvent.created_on,\n now,\n completionTime\n )\n this.deleteStatement.run(lockedEvent.id)\n }\n })()\n }\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\"\nimport type { Database } from \"better-sqlite3\"\n\nexport type { Database }\n\nexport const betterSqlite3TransactionStorage: AsyncLocalStorage<Database> =\n new AsyncLocalStorage<Database>()\n\nexport async function withBetterSqlite3Transaction<T>(\n db: Database,\n fn: (tx: Database) => Promise<T>\n): Promise<T> {\n return betterSqlite3TransactionStorage.run(db, async () => {\n if (db.inTransaction) {\n const savepointName = `sp_${Date.now()}_${Math.random().toString(36).slice(2)}`\n db.prepare(`SAVEPOINT ${savepointName}`).run()\n try {\n const result = await fn(db)\n db.prepare(`RELEASE ${savepointName}`).run()\n return result\n } catch (error) {\n db.prepare(`ROLLBACK TO ${savepointName}`).run()\n db.prepare(`RELEASE ${savepointName}`).run()\n throw error\n }\n } else {\n db.prepare(\"BEGIN\").run()\n try {\n const result = await fn(db)\n db.prepare(\"COMMIT\").run()\n return result\n } catch (error) {\n db.prepare(\"ROLLBACK\").run()\n throw error\n }\n }\n })\n}\n\nexport function getBetterSqlite3Transaction(): () => Database | undefined {\n return () => betterSqlite3TransactionStorage.getStore()\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -75,6 +75,11 @@ export class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {
|
|
|
75
75
|
private readonly db: Database.Database
|
|
76
76
|
private readonly poller: PollingService
|
|
77
77
|
|
|
78
|
+
private archiveStatement!: Database.Statement
|
|
79
|
+
private deleteStatement!: Database.Statement
|
|
80
|
+
private fetchStatement!: Database.Statement
|
|
81
|
+
private failStatement!: Database.Statement
|
|
82
|
+
|
|
78
83
|
constructor(config: SqliteBetterSqlite3OutboxConfig) {
|
|
79
84
|
if (config.db) {
|
|
80
85
|
this.db = config.db
|
|
@@ -110,6 +115,31 @@ export class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {
|
|
|
110
115
|
|
|
111
116
|
private init() {
|
|
112
117
|
this.db.exec(getOutboxSchema(this.config.tableName, this.config.archiveTableName))
|
|
118
|
+
|
|
119
|
+
this.archiveStatement = this.db.prepare(`
|
|
120
|
+
INSERT INTO ${this.config.archiveTableName} (
|
|
121
|
+
id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on
|
|
122
|
+
) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)
|
|
123
|
+
`)
|
|
124
|
+
|
|
125
|
+
this.deleteStatement = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`)
|
|
126
|
+
|
|
127
|
+
this.fetchStatement = this.db.prepare(`
|
|
128
|
+
SELECT * FROM ${this.config.tableName}
|
|
129
|
+
WHERE status = '${EventStatus.CREATED}'
|
|
130
|
+
OR (status = '${EventStatus.FAILED}' AND retry_count < ? AND next_retry_at <= ?)
|
|
131
|
+
OR (status = '${EventStatus.ACTIVE}' AND datetime(keep_alive, '+' || expire_in_seconds || ' seconds') < datetime(?))
|
|
132
|
+
LIMIT ?
|
|
133
|
+
`)
|
|
134
|
+
|
|
135
|
+
this.failStatement = this.db.prepare(`
|
|
136
|
+
UPDATE ${this.config.tableName}
|
|
137
|
+
SET status = '${EventStatus.FAILED}',
|
|
138
|
+
retry_count = ?,
|
|
139
|
+
last_error = ?,
|
|
140
|
+
next_retry_at = ?
|
|
141
|
+
WHERE id = ?
|
|
142
|
+
`)
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
async publish(events: BusEvent[], transaction?: Database.Database): Promise<void> {
|
|
@@ -183,23 +213,44 @@ export class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {
|
|
|
183
213
|
}
|
|
184
214
|
|
|
185
215
|
private async processBatch(handler: (event: BusEvent) => Promise<void>) {
|
|
216
|
+
const lockedEvents = this.lockBatch()
|
|
217
|
+
if (lockedEvents.length === 0) return
|
|
218
|
+
|
|
186
219
|
const now = new Date().toISOString()
|
|
187
220
|
const msNow = Date.now()
|
|
221
|
+
const completedEvents: OutboxRow[] = []
|
|
222
|
+
|
|
223
|
+
for (const lockedEvent of lockedEvents) {
|
|
224
|
+
const event: BusEvent = {
|
|
225
|
+
id: lockedEvent.id,
|
|
226
|
+
type: lockedEvent.type,
|
|
227
|
+
payload: JSON.parse(lockedEvent.payload),
|
|
228
|
+
occurredAt: new Date(lockedEvent.occurred_at),
|
|
229
|
+
}
|
|
188
230
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
231
|
+
try {
|
|
232
|
+
await handler(event)
|
|
233
|
+
completedEvents.push(lockedEvent)
|
|
234
|
+
} catch (error) {
|
|
235
|
+
this.handleEventFailure(lockedEvent, event, error, msNow)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (completedEvents.length > 0) {
|
|
240
|
+
this.archiveBatch(completedEvents, now)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private lockBatch(): OutboxRow[] {
|
|
245
|
+
const now = new Date().toISOString()
|
|
246
|
+
|
|
247
|
+
return this.db.transaction(() => {
|
|
248
|
+
const rows = this.fetchStatement.all(
|
|
249
|
+
this.config.maxRetries,
|
|
250
|
+
now,
|
|
251
|
+
now,
|
|
252
|
+
this.config.batchSize
|
|
253
|
+
) as OutboxRow[]
|
|
203
254
|
|
|
204
255
|
if (rows.length === 0) return []
|
|
205
256
|
|
|
@@ -218,74 +269,45 @@ export class SqliteBetterSqlite3Outbox implements IOutbox<Database.Database> {
|
|
|
218
269
|
|
|
219
270
|
return rows
|
|
220
271
|
})()
|
|
272
|
+
}
|
|
221
273
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
274
|
+
private handleEventFailure(
|
|
275
|
+
lockedEvent: OutboxRow,
|
|
276
|
+
event: BusEvent,
|
|
277
|
+
error: unknown,
|
|
278
|
+
msNow: number
|
|
279
|
+
) {
|
|
280
|
+
const retryCount = lockedEvent.retry_count + 1
|
|
281
|
+
reportEventError(this.poller.onError, error, event, retryCount, this.config.maxRetries)
|
|
282
|
+
|
|
283
|
+
const delay = this.poller.calculateBackoff(retryCount)
|
|
284
|
+
|
|
285
|
+
this.failStatement.run(
|
|
286
|
+
retryCount,
|
|
287
|
+
formatErrorMessage(error),
|
|
288
|
+
new Date(msNow + delay).toISOString(),
|
|
289
|
+
lockedEvent.id
|
|
290
|
+
)
|
|
291
|
+
}
|
|
236
292
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
`)
|
|
255
|
-
.run(
|
|
256
|
-
retryCount,
|
|
257
|
-
formatErrorMessage(error),
|
|
258
|
-
new Date(msNow + delay).toISOString(),
|
|
259
|
-
lockedEvent.id
|
|
260
|
-
)
|
|
293
|
+
private archiveBatch(completedEvents: OutboxRow[], now: string) {
|
|
294
|
+
const completionTime = new Date().toISOString()
|
|
295
|
+
|
|
296
|
+
this.db.transaction(() => {
|
|
297
|
+
for (const lockedEvent of completedEvents) {
|
|
298
|
+
this.archiveStatement.run(
|
|
299
|
+
lockedEvent.id,
|
|
300
|
+
lockedEvent.type,
|
|
301
|
+
lockedEvent.payload,
|
|
302
|
+
lockedEvent.occurred_at,
|
|
303
|
+
lockedEvent.retry_count,
|
|
304
|
+
lockedEvent.last_error,
|
|
305
|
+
lockedEvent.created_on,
|
|
306
|
+
now,
|
|
307
|
+
completionTime
|
|
308
|
+
)
|
|
309
|
+
this.deleteStatement.run(lockedEvent.id)
|
|
261
310
|
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (completedEvents.length > 0) {
|
|
265
|
-
this.db.transaction(() => {
|
|
266
|
-
const insertArchive = this.db.prepare(`
|
|
267
|
-
INSERT INTO ${this.config.archiveTableName} (
|
|
268
|
-
id, type, payload, occurred_at, status, retry_count, last_error, created_on, started_on, completed_on
|
|
269
|
-
) VALUES (?, ?, ?, ?, '${EventStatus.COMPLETED}', ?, ?, ?, ?, ?)
|
|
270
|
-
`)
|
|
271
|
-
const deleteEvent = this.db.prepare(`DELETE FROM ${this.config.tableName} WHERE id = ?`)
|
|
272
|
-
|
|
273
|
-
const completionTime = new Date().toISOString()
|
|
274
|
-
for (const { lockedEvent } of completedEvents) {
|
|
275
|
-
insertArchive.run(
|
|
276
|
-
lockedEvent.id,
|
|
277
|
-
lockedEvent.type,
|
|
278
|
-
lockedEvent.payload,
|
|
279
|
-
lockedEvent.occurred_at,
|
|
280
|
-
lockedEvent.retry_count,
|
|
281
|
-
lockedEvent.last_error,
|
|
282
|
-
lockedEvent.created_on,
|
|
283
|
-
now,
|
|
284
|
-
completionTime
|
|
285
|
-
)
|
|
286
|
-
deleteEvent.run(lockedEvent.id)
|
|
287
|
-
}
|
|
288
|
-
})()
|
|
289
|
-
}
|
|
311
|
+
})()
|
|
290
312
|
}
|
|
291
313
|
}
|