@dudousxd/nestjs-durable-transport-db 0.1.1 → 0.2.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 +21 -0
- package/dist/index.cjs +323 -0
- package/dist/index.cjs.map +1 -0
- package/dist/{db-transport.d.ts → index.d.cts} +66 -6
- package/dist/index.d.ts +143 -2
- package/dist/index.js +293 -15
- package/dist/index.js.map +1 -1
- package/package.json +22 -6
- package/dist/db-transport.d.ts.map +0 -1
- package/dist/db-transport.js +0 -244
- package/dist/db-transport.js.map +0 -1
- package/dist/executors.d.ts +0 -61
- package/dist/executors.d.ts.map +0 -1
- package/dist/executors.js +0 -50
- package/dist/executors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,296 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/db-transport.ts
|
|
5
|
+
import { runStepHandler } from "@dudousxd/nestjs-durable-core";
|
|
6
|
+
|
|
7
|
+
// src/executors.ts
|
|
8
|
+
function typeOrmExecutor(ds) {
|
|
9
|
+
return {
|
|
10
|
+
dialect: /postgres/.test(String(ds.options.type)) ? "postgres" : "mysql",
|
|
11
|
+
escapeId: /* @__PURE__ */ __name((id) => ds.driver.escape(id), "escapeId"),
|
|
12
|
+
query: /* @__PURE__ */ __name((sql, params) => ds.query(sql, params), "query"),
|
|
13
|
+
transaction: /* @__PURE__ */ __name(async (fn) => {
|
|
14
|
+
const qr = ds.createQueryRunner();
|
|
15
|
+
await qr.connect();
|
|
16
|
+
await qr.startTransaction();
|
|
17
|
+
try {
|
|
18
|
+
const result = await fn({
|
|
19
|
+
query: /* @__PURE__ */ __name((sql, params) => qr.query(sql, params), "query")
|
|
20
|
+
});
|
|
21
|
+
await qr.commitTransaction();
|
|
22
|
+
return result;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
await qr.rollbackTransaction();
|
|
25
|
+
throw err;
|
|
26
|
+
} finally {
|
|
27
|
+
await qr.release();
|
|
28
|
+
}
|
|
29
|
+
}, "transaction")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
__name(typeOrmExecutor, "typeOrmExecutor");
|
|
33
|
+
function mikroOrmExecutor(em) {
|
|
34
|
+
const platform = em.getPlatform();
|
|
35
|
+
return {
|
|
36
|
+
dialect: /postgre/i.test(platform.constructor.name) ? "postgres" : "mysql",
|
|
37
|
+
escapeId: /* @__PURE__ */ __name((id) => platform.quoteIdentifier(id), "escapeId"),
|
|
38
|
+
query: /* @__PURE__ */ __name((sql, params) => em.getConnection().execute(sql, params ?? [], "all"), "query"),
|
|
39
|
+
transaction: /* @__PURE__ */ __name((fn) => em.transactional((txEm) => fn({
|
|
40
|
+
query: /* @__PURE__ */ __name((sql, params) => txEm.getConnection().execute(sql, params ?? [], "all", txEm.getTransactionContext?.()), "query")
|
|
41
|
+
})), "transaction")
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
__name(mikroOrmExecutor, "mikroOrmExecutor");
|
|
45
|
+
|
|
46
|
+
// src/db-transport.ts
|
|
47
|
+
var DbTransport = class {
|
|
48
|
+
static {
|
|
49
|
+
__name(this, "DbTransport");
|
|
50
|
+
}
|
|
51
|
+
exec;
|
|
52
|
+
group;
|
|
53
|
+
tasksTable;
|
|
54
|
+
resultsTable;
|
|
55
|
+
pollMs;
|
|
56
|
+
leaseMs;
|
|
57
|
+
batchSize;
|
|
58
|
+
autoCreate;
|
|
59
|
+
instanceId;
|
|
60
|
+
isPg;
|
|
61
|
+
handlers = /* @__PURE__ */ new Map();
|
|
62
|
+
running = true;
|
|
63
|
+
schemaReady;
|
|
64
|
+
taskLoop;
|
|
65
|
+
resultLoop;
|
|
66
|
+
constructor(options) {
|
|
67
|
+
this.exec = options.executor;
|
|
68
|
+
this.group = options.group;
|
|
69
|
+
const prefix = options.prefix ?? "durable";
|
|
70
|
+
this.tasksTable = `${prefix}_transport_tasks`;
|
|
71
|
+
this.resultsTable = `${prefix}_transport_results`;
|
|
72
|
+
this.pollMs = options.pollMs ?? 500;
|
|
73
|
+
this.leaseMs = options.leaseMs ?? 3e4;
|
|
74
|
+
this.batchSize = options.batchSize ?? 10;
|
|
75
|
+
this.autoCreate = options.autoCreate ?? true;
|
|
76
|
+
this.instanceId = options.instanceId ?? `db-${Math.floor(Date.now()).toString(36)}`;
|
|
77
|
+
this.isPg = this.exec.dialect === "postgres";
|
|
78
|
+
}
|
|
79
|
+
// ── dialect helpers ──────────────────────────────────────────────────────────────────────────
|
|
80
|
+
/** Positional placeholder for param `i` (1-based): `$i` on Postgres, `?` elsewhere. */
|
|
81
|
+
ph(i) {
|
|
82
|
+
return this.isPg ? `$${i}` : "?";
|
|
83
|
+
}
|
|
84
|
+
q(id) {
|
|
85
|
+
return this.exec.escapeId(id);
|
|
86
|
+
}
|
|
87
|
+
now() {
|
|
88
|
+
return Math.floor(Date.now());
|
|
89
|
+
}
|
|
90
|
+
// ── schema ───────────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
ensureSchema() {
|
|
92
|
+
if (!this.schemaReady) this.schemaReady = this.createTables();
|
|
93
|
+
return this.schemaReady;
|
|
94
|
+
}
|
|
95
|
+
async createTables() {
|
|
96
|
+
if (!this.autoCreate) return;
|
|
97
|
+
const str = "varchar(191)";
|
|
98
|
+
const txt = this.isPg ? "text" : "longtext";
|
|
99
|
+
const tasks = this.q(this.tasksTable);
|
|
100
|
+
const results = this.q(this.resultsTable);
|
|
101
|
+
await this.exec.query(`CREATE TABLE IF NOT EXISTS ${tasks} (
|
|
102
|
+
${this.q("step_id")} ${str} PRIMARY KEY,
|
|
103
|
+
${this.q("run_id")} ${str} NOT NULL,
|
|
104
|
+
${this.q("seq")} integer NOT NULL,
|
|
105
|
+
${this.q("name")} ${str} NOT NULL,
|
|
106
|
+
${this.q("grp")} ${str} NOT NULL,
|
|
107
|
+
${this.q("input")} ${txt},
|
|
108
|
+
${this.q("attempt")} integer NOT NULL,
|
|
109
|
+
${this.q("status")} varchar(32) NOT NULL,
|
|
110
|
+
${this.q("claimed_by")} ${str},
|
|
111
|
+
${this.q("claimed_at")} bigint,
|
|
112
|
+
${this.q("created_at")} bigint NOT NULL
|
|
113
|
+
)`);
|
|
114
|
+
await this.exec.query(`CREATE TABLE IF NOT EXISTS ${results} (
|
|
115
|
+
${this.q("step_id")} ${str} PRIMARY KEY,
|
|
116
|
+
${this.q("run_id")} ${str} NOT NULL,
|
|
117
|
+
${this.q("seq")} integer NOT NULL,
|
|
118
|
+
${this.q("status")} varchar(32) NOT NULL,
|
|
119
|
+
${this.q("output")} ${txt},
|
|
120
|
+
${this.q("error")} ${txt},
|
|
121
|
+
${this.q("started_at")} bigint,
|
|
122
|
+
${this.q("claimed_by")} ${str},
|
|
123
|
+
${this.q("claimed_at")} bigint,
|
|
124
|
+
${this.q("created_at")} bigint NOT NULL
|
|
125
|
+
)`);
|
|
126
|
+
await this.exec.query(`CREATE INDEX ${this.q(`${this.tasksTable}_grp_idx`)} ON ${tasks} (${this.q("grp")}, ${this.q("status")}, ${this.q("created_at")})`).catch(() => {
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ── engine → worker ────────────────────────────────────────────────────────────────────────
|
|
130
|
+
async dispatch(task) {
|
|
131
|
+
await this.ensureSchema();
|
|
132
|
+
const t = this.q(this.tasksTable);
|
|
133
|
+
const ignore = this.isPg ? `INSERT INTO ${t} (...) ON CONFLICT (${this.q("step_id")}) DO NOTHING` : `INSERT IGNORE INTO ${t} (...)`;
|
|
134
|
+
const cols = [
|
|
135
|
+
"step_id",
|
|
136
|
+
"run_id",
|
|
137
|
+
"seq",
|
|
138
|
+
"name",
|
|
139
|
+
"grp",
|
|
140
|
+
"input",
|
|
141
|
+
"attempt",
|
|
142
|
+
"status",
|
|
143
|
+
"created_at"
|
|
144
|
+
];
|
|
145
|
+
const sql = ignore.replace("(...)", `(${cols.map((c) => this.q(c)).join(", ")}) VALUES (${cols.map((_, i) => this.ph(i + 1)).join(", ")})`);
|
|
146
|
+
await this.exec.query(sql, [
|
|
147
|
+
task.stepId,
|
|
148
|
+
task.runId,
|
|
149
|
+
task.seq,
|
|
150
|
+
task.name,
|
|
151
|
+
task.group,
|
|
152
|
+
JSON.stringify(task.input ?? null),
|
|
153
|
+
task.attempt,
|
|
154
|
+
"pending",
|
|
155
|
+
this.now()
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
// ── worker → engine ────────────────────────────────────────────────────────────────────────
|
|
159
|
+
handle(name, fn) {
|
|
160
|
+
if (!this.group) throw new Error("DbTransport needs a `group` to register handlers");
|
|
161
|
+
this.handlers.set(name, fn);
|
|
162
|
+
if (!this.taskLoop) this.taskLoop = this.loop(() => this.drainTasks(this.group));
|
|
163
|
+
}
|
|
164
|
+
onResult(handler) {
|
|
165
|
+
if (!this.resultLoop) this.resultLoop = this.loop(() => this.drainResults(handler));
|
|
166
|
+
}
|
|
167
|
+
onHeartbeat(_handler) {
|
|
168
|
+
}
|
|
169
|
+
// ── polling ──────────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
/** Run `tick` repeatedly; sleep `pollMs` when it reports an empty queue. Stops on `close()`. */
|
|
171
|
+
async loop(tick) {
|
|
172
|
+
await this.ensureSchema();
|
|
173
|
+
while (this.running) {
|
|
174
|
+
let processed = 0;
|
|
175
|
+
try {
|
|
176
|
+
processed = await tick();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (this.running) console.error("[DbTransport] poll failed", err);
|
|
179
|
+
}
|
|
180
|
+
if (processed === 0) await this.sleep(this.pollMs);
|
|
7
181
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
for (
|
|
182
|
+
}
|
|
183
|
+
/** Claim a batch of tasks (FOR UPDATE SKIP LOCKED), then run each outside the lock. */
|
|
184
|
+
async drainTasks(group) {
|
|
185
|
+
const claimed = await this.claim(this.tasksTable, `${this.q("grp")} = ${this.ph(1)}`, [
|
|
186
|
+
group
|
|
187
|
+
]);
|
|
188
|
+
for (const row of claimed) {
|
|
189
|
+
const task = {
|
|
190
|
+
runId: row.run_id,
|
|
191
|
+
seq: Number(row.seq),
|
|
192
|
+
stepId: row.step_id,
|
|
193
|
+
name: row.name,
|
|
194
|
+
group,
|
|
195
|
+
input: row.input == null ? void 0 : JSON.parse(row.input),
|
|
196
|
+
attempt: Number(row.attempt)
|
|
197
|
+
};
|
|
198
|
+
const result = await runStepHandler(task, this.handlers.get(task.name));
|
|
199
|
+
await this.insertResult(result);
|
|
200
|
+
await this.exec.query(`DELETE FROM ${this.q(this.tasksTable)} WHERE ${this.q("step_id")} = ${this.ph(1)}`, [
|
|
201
|
+
task.stepId
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
return claimed.length;
|
|
205
|
+
}
|
|
206
|
+
/** Claim a batch of results, deliver each to the engine, then delete it. */
|
|
207
|
+
async drainResults(handler) {
|
|
208
|
+
const claimed = await this.claim(this.resultsTable, "1 = 1", []);
|
|
209
|
+
for (const row of claimed) {
|
|
210
|
+
const result = {
|
|
211
|
+
runId: row.run_id,
|
|
212
|
+
seq: Number(row.seq),
|
|
213
|
+
stepId: row.step_id,
|
|
214
|
+
status: row.status,
|
|
215
|
+
output: row.output == null ? void 0 : JSON.parse(row.output),
|
|
216
|
+
error: row.error == null ? void 0 : JSON.parse(row.error),
|
|
217
|
+
startedAt: row.started_at == null ? void 0 : Number(row.started_at)
|
|
218
|
+
};
|
|
219
|
+
await handler(result);
|
|
220
|
+
await this.exec.query(`DELETE FROM ${this.q(this.resultsTable)} WHERE ${this.q("step_id")} = ${this.ph(1)}`, [
|
|
221
|
+
result.stepId
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
return claimed.length;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Atomically claim up to `batchSize` un-leased rows matching `where` and stamp them with this
|
|
228
|
+
* instance's lease. `SELECT … FOR UPDATE SKIP LOCKED` inside the txn means concurrent instances
|
|
229
|
+
* skip each other's locked rows instead of blocking, so no row is claimed twice.
|
|
230
|
+
*/
|
|
231
|
+
async claim(table, where, whereParams) {
|
|
232
|
+
const t = this.q(table);
|
|
233
|
+
const staleBefore = this.now() - this.leaseMs;
|
|
234
|
+
return this.exec.transaction(async (tx) => {
|
|
235
|
+
const leaseClause = `(${this.q("claimed_at")} IS NULL OR ${this.q("claimed_at")} < ${this.ph(whereParams.length + 1)})`;
|
|
236
|
+
const rows = await tx.query(`SELECT * FROM ${t} WHERE ${where} AND ${leaseClause}
|
|
237
|
+
ORDER BY ${this.q("created_at")} ASC LIMIT ${this.batchSize} FOR UPDATE SKIP LOCKED`, [
|
|
238
|
+
...whereParams,
|
|
239
|
+
staleBefore
|
|
240
|
+
]);
|
|
241
|
+
if (rows.length > 0) {
|
|
242
|
+
const ids = rows.map((r) => r.step_id);
|
|
243
|
+
const placeholders = ids.map((_, i) => this.ph(i + 3)).join(", ");
|
|
244
|
+
await tx.query(`UPDATE ${t} SET ${this.q("claimed_by")} = ${this.ph(1)}, ${this.q("claimed_at")} = ${this.ph(2)}
|
|
245
|
+
WHERE ${this.q("step_id")} IN (${placeholders})`, [
|
|
246
|
+
this.instanceId,
|
|
247
|
+
this.now(),
|
|
248
|
+
...ids
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
return rows;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async insertResult(result) {
|
|
255
|
+
const t = this.q(this.resultsTable);
|
|
256
|
+
const cols = [
|
|
257
|
+
"step_id",
|
|
258
|
+
"run_id",
|
|
259
|
+
"seq",
|
|
260
|
+
"status",
|
|
261
|
+
"output",
|
|
262
|
+
"error",
|
|
263
|
+
"started_at",
|
|
264
|
+
"created_at"
|
|
265
|
+
];
|
|
266
|
+
const head = this.isPg ? `INSERT INTO ${t} (...) ON CONFLICT (${this.q("step_id")}) DO NOTHING` : `INSERT IGNORE INTO ${t} (...)`;
|
|
267
|
+
const sql = head.replace("(...)", `(${cols.map((c) => this.q(c)).join(", ")}) VALUES (${cols.map((_, i) => this.ph(i + 1)).join(", ")})`);
|
|
268
|
+
await this.exec.query(sql, [
|
|
269
|
+
result.stepId,
|
|
270
|
+
result.runId,
|
|
271
|
+
result.seq,
|
|
272
|
+
result.status,
|
|
273
|
+
result.output === void 0 ? null : JSON.stringify(result.output),
|
|
274
|
+
result.error === void 0 ? null : JSON.stringify(result.error),
|
|
275
|
+
result.startedAt ?? null,
|
|
276
|
+
this.now()
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
sleep(ms) {
|
|
280
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
281
|
+
}
|
|
282
|
+
/** Stop the pollers and wait for the current ticks to settle. Does not close the shared DataSource. */
|
|
283
|
+
async close() {
|
|
284
|
+
this.running = false;
|
|
285
|
+
await Promise.allSettled([
|
|
286
|
+
this.taskLoop,
|
|
287
|
+
this.resultLoop
|
|
288
|
+
]);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
export {
|
|
292
|
+
DbTransport,
|
|
293
|
+
mikroOrmExecutor,
|
|
294
|
+
typeOrmExecutor
|
|
15
295
|
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./db-transport"), exports);
|
|
18
296
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iDAA+B"}
|
|
1
|
+
{"version":3,"sources":["../src/db-transport.ts","../src/executors.ts"],"sourcesContent":["import {\n type Heartbeat,\n type RemoteTask,\n type StepHandler,\n type StepResult,\n type Transport,\n runStepHandler,\n} from '@dudousxd/nestjs-durable-core';\nimport type { SqlExecutor } from './executors';\n\nexport { mikroOrmExecutor, type SqlExecutor, type SqlTx, typeOrmExecutor } from './executors';\n\n/** Raw row shapes returned by `SELECT *` (declared fields, so they're not `| undefined` under noUncheckedIndexedAccess). */\ninterface TaskRow {\n step_id: string;\n run_id: string;\n seq: string | number;\n name: string;\n input: string | null;\n attempt: string | number;\n}\ninterface ResultRow {\n step_id: string;\n run_id: string;\n seq: string | number;\n status: string;\n output: string | null;\n error: string | null;\n started_at: string | number | null;\n}\n\nexport interface DbTransportOptions {\n /**\n * How to run SQL — built from the app's **own** ORM/connection (no broker, no extra connection).\n * Use `mikroOrmExecutor(em)` to ride the app's MikroORM, `typeOrmExecutor(dataSource)` for TypeORM,\n * or implement `SqlExecutor` for anything else.\n */\n executor: SqlExecutor;\n /** The worker group this instance serves. Required to register `handle()` consumers. */\n group?: string;\n /** Table-name prefix. Tables: `${prefix}_transport_tasks` / `${prefix}_transport_results`. Default `durable`. */\n prefix?: string;\n /** Poll interval in ms when a queue is empty. Default 500. */\n pollMs?: number;\n /** How long a claimed row is owned before it's reclaimable (crash recovery), in ms. Default 30_000. */\n leaseMs?: number;\n /** Max rows claimed per poll. Default 10. */\n batchSize?: number;\n /** Create the two tables on first use if missing. Default true. */\n autoCreate?: boolean;\n /** Identifies this instance in `claimed_by` (for debugging). Defaults to a random id. */\n instanceId?: string;\n}\n\n/**\n * A SQL-backed `Transport` — DBOS-style. Instead of a broker (Redis/SQS), remote steps are **rows**\n * in the same database the durable store already uses: `dispatch` inserts a task row, a worker\n * claims it with `SELECT … FOR UPDATE SKIP LOCKED` (so instances never double-claim), runs it, and\n * writes a result row the engine polls. Zero new infrastructure — the DB you already have IS the\n * queue.\n *\n * Trade-off vs a real broker: throughput is bounded by polling + row contention. Great for\n * workflow/pipeline scale (modest rate, long steps); not for high-fanout firehoses.\n *\n * Requires `FOR UPDATE SKIP LOCKED` — **MySQL 8+** or **Postgres 9.5+** (not SQLite). Run one\n * instance engine-side (`onResult`, dispatches) and one per worker process (`handle()` for its group).\n */\nexport class DbTransport implements Transport {\n private readonly exec: SqlExecutor;\n private readonly group?: string | undefined;\n private readonly tasksTable: string;\n private readonly resultsTable: string;\n private readonly pollMs: number;\n private readonly leaseMs: number;\n private readonly batchSize: number;\n private readonly autoCreate: boolean;\n private readonly instanceId: string;\n private readonly isPg: boolean;\n\n private readonly handlers = new Map<string, StepHandler>();\n private running = true;\n private schemaReady?: Promise<void>;\n private taskLoop?: Promise<void>;\n private resultLoop?: Promise<void>;\n\n constructor(options: DbTransportOptions) {\n this.exec = options.executor;\n this.group = options.group;\n const prefix = options.prefix ?? 'durable';\n this.tasksTable = `${prefix}_transport_tasks`;\n this.resultsTable = `${prefix}_transport_results`;\n this.pollMs = options.pollMs ?? 500;\n this.leaseMs = options.leaseMs ?? 30_000;\n this.batchSize = options.batchSize ?? 10;\n this.autoCreate = options.autoCreate ?? true;\n this.instanceId = options.instanceId ?? `db-${Math.floor(Date.now()).toString(36)}`;\n this.isPg = this.exec.dialect === 'postgres';\n }\n\n // ── dialect helpers ──────────────────────────────────────────────────────────────────────────\n /** Positional placeholder for param `i` (1-based): `$i` on Postgres, `?` elsewhere. */\n private ph(i: number): string {\n return this.isPg ? `$${i}` : '?';\n }\n private q(id: string): string {\n return this.exec.escapeId(id);\n }\n private now(): number {\n return Math.floor(Date.now());\n }\n\n // ── schema ───────────────────────────────────────────────────────────────────────────────────\n private ensureSchema(): Promise<void> {\n if (!this.schemaReady) this.schemaReady = this.createTables();\n return this.schemaReady;\n }\n\n private async createTables(): Promise<void> {\n if (!this.autoCreate) return;\n const str = 'varchar(191)';\n const txt = this.isPg ? 'text' : 'longtext';\n const tasks = this.q(this.tasksTable);\n const results = this.q(this.resultsTable);\n await this.exec.query(\n `CREATE TABLE IF NOT EXISTS ${tasks} (\n ${this.q('step_id')} ${str} PRIMARY KEY,\n ${this.q('run_id')} ${str} NOT NULL,\n ${this.q('seq')} integer NOT NULL,\n ${this.q('name')} ${str} NOT NULL,\n ${this.q('grp')} ${str} NOT NULL,\n ${this.q('input')} ${txt},\n ${this.q('attempt')} integer NOT NULL,\n ${this.q('status')} varchar(32) NOT NULL,\n ${this.q('claimed_by')} ${str},\n ${this.q('claimed_at')} bigint,\n ${this.q('created_at')} bigint NOT NULL\n )`,\n );\n await this.exec.query(\n `CREATE TABLE IF NOT EXISTS ${results} (\n ${this.q('step_id')} ${str} PRIMARY KEY,\n ${this.q('run_id')} ${str} NOT NULL,\n ${this.q('seq')} integer NOT NULL,\n ${this.q('status')} varchar(32) NOT NULL,\n ${this.q('output')} ${txt},\n ${this.q('error')} ${txt},\n ${this.q('started_at')} bigint,\n ${this.q('claimed_by')} ${str},\n ${this.q('claimed_at')} bigint,\n ${this.q('created_at')} bigint NOT NULL\n )`,\n );\n // Best-effort index for the group poll (MySQL has no CREATE INDEX IF NOT EXISTS < 8.0.13).\n await this.exec\n .query(\n `CREATE INDEX ${this.q(`${this.tasksTable}_grp_idx`)} ON ${tasks} (${this.q('grp')}, ${this.q('status')}, ${this.q('created_at')})`,\n )\n .catch(() => {});\n }\n\n // ── engine → worker ────────────────────────────────────────────────────────────────────────\n async dispatch(task: RemoteTask): Promise<void> {\n await this.ensureSchema();\n const t = this.q(this.tasksTable);\n // Idempotent: a redelivered dispatch for the same step_id is ignored (the row already exists).\n const ignore = this.isPg\n ? `INSERT INTO ${t} (...) ON CONFLICT (${this.q('step_id')}) DO NOTHING`\n : `INSERT IGNORE INTO ${t} (...)`;\n const cols = ['step_id', 'run_id', 'seq', 'name', 'grp', 'input', 'attempt', 'status', 'created_at'];\n const sql = ignore.replace(\n '(...)',\n `(${cols.map((c) => this.q(c)).join(', ')}) VALUES (${cols.map((_, i) => this.ph(i + 1)).join(', ')})`,\n );\n await this.exec.query(sql, [\n task.stepId,\n task.runId,\n task.seq,\n task.name,\n task.group,\n JSON.stringify(task.input ?? null),\n task.attempt,\n 'pending',\n this.now(),\n ]);\n }\n\n // ── worker → engine ────────────────────────────────────────────────────────────────────────\n handle(name: string, fn: StepHandler): void {\n if (!this.group) throw new Error('DbTransport needs a `group` to register handlers');\n this.handlers.set(name, fn);\n if (!this.taskLoop) this.taskLoop = this.loop(() => this.drainTasks(this.group as string));\n }\n\n onResult(handler: (result: StepResult) => Promise<void>): void {\n if (!this.resultLoop) this.resultLoop = this.loop(() => this.drainResults(handler));\n }\n\n onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void {\n // Not modelled; the lease/reclaim on `claimed_at` is the liveness mechanism.\n }\n\n // ── polling ──────────────────────────────────────────────────────────────────────────────────\n /** Run `tick` repeatedly; sleep `pollMs` when it reports an empty queue. Stops on `close()`. */\n private async loop(tick: () => Promise<number>): Promise<void> {\n await this.ensureSchema();\n while (this.running) {\n let processed = 0;\n try {\n processed = await tick();\n } catch (err) {\n if (this.running) console.error('[DbTransport] poll failed', err);\n }\n if (processed === 0) await this.sleep(this.pollMs);\n }\n }\n\n /** Claim a batch of tasks (FOR UPDATE SKIP LOCKED), then run each outside the lock. */\n private async drainTasks(group: string): Promise<number> {\n const claimed = await this.claim<TaskRow>(\n this.tasksTable,\n `${this.q('grp')} = ${this.ph(1)}`,\n [group],\n );\n for (const row of claimed) {\n const task: RemoteTask = {\n runId: row.run_id,\n seq: Number(row.seq),\n stepId: row.step_id,\n name: row.name,\n group,\n input: row.input == null ? undefined : JSON.parse(row.input),\n attempt: Number(row.attempt),\n };\n const result = await runStepHandler(task, this.handlers.get(task.name));\n await this.insertResult(result);\n await this.exec.query(\n `DELETE FROM ${this.q(this.tasksTable)} WHERE ${this.q('step_id')} = ${this.ph(1)}`,\n [task.stepId],\n );\n }\n return claimed.length;\n }\n\n /** Claim a batch of results, deliver each to the engine, then delete it. */\n private async drainResults(handler: (result: StepResult) => Promise<void>): Promise<number> {\n const claimed = await this.claim<ResultRow>(this.resultsTable, '1 = 1', []);\n for (const row of claimed) {\n const result: StepResult = {\n runId: row.run_id,\n seq: Number(row.seq),\n stepId: row.step_id,\n status: row.status as StepResult['status'],\n output: row.output == null ? undefined : JSON.parse(row.output),\n error: row.error == null ? undefined : JSON.parse(row.error),\n startedAt: row.started_at == null ? undefined : Number(row.started_at),\n };\n await handler(result);\n await this.exec.query(\n `DELETE FROM ${this.q(this.resultsTable)} WHERE ${this.q('step_id')} = ${this.ph(1)}`,\n [result.stepId],\n );\n }\n return claimed.length;\n }\n\n /**\n * Atomically claim up to `batchSize` un-leased rows matching `where` and stamp them with this\n * instance's lease. `SELECT … FOR UPDATE SKIP LOCKED` inside the txn means concurrent instances\n * skip each other's locked rows instead of blocking, so no row is claimed twice.\n */\n private async claim<T extends { step_id: string }>(\n table: string,\n where: string,\n whereParams: unknown[],\n ): Promise<T[]> {\n const t = this.q(table);\n const staleBefore = this.now() - this.leaseMs;\n return this.exec.transaction(async (tx) => {\n const leaseClause = `(${this.q('claimed_at')} IS NULL OR ${this.q('claimed_at')} < ${this.ph(whereParams.length + 1)})`;\n const rows = await tx.query<T>(\n `SELECT * FROM ${t} WHERE ${where} AND ${leaseClause}\n ORDER BY ${this.q('created_at')} ASC LIMIT ${this.batchSize} FOR UPDATE SKIP LOCKED`,\n [...whereParams, staleBefore],\n );\n if (rows.length > 0) {\n const ids = rows.map((r) => r.step_id);\n const placeholders = ids.map((_, i) => this.ph(i + 3)).join(', ');\n await tx.query(\n `UPDATE ${t} SET ${this.q('claimed_by')} = ${this.ph(1)}, ${this.q('claimed_at')} = ${this.ph(2)}\n WHERE ${this.q('step_id')} IN (${placeholders})`,\n [this.instanceId, this.now(), ...ids],\n );\n }\n return rows;\n });\n }\n\n private async insertResult(result: StepResult): Promise<void> {\n const t = this.q(this.resultsTable);\n const cols = ['step_id', 'run_id', 'seq', 'status', 'output', 'error', 'started_at', 'created_at'];\n const head = this.isPg\n ? `INSERT INTO ${t} (...) ON CONFLICT (${this.q('step_id')}) DO NOTHING`\n : `INSERT IGNORE INTO ${t} (...)`;\n const sql = head.replace(\n '(...)',\n `(${cols.map((c) => this.q(c)).join(', ')}) VALUES (${cols.map((_, i) => this.ph(i + 1)).join(', ')})`,\n );\n await this.exec.query(sql, [\n result.stepId,\n result.runId,\n result.seq,\n result.status,\n result.output === undefined ? null : JSON.stringify(result.output),\n result.error === undefined ? null : JSON.stringify(result.error),\n result.startedAt ?? null,\n this.now(),\n ]);\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n }\n\n /** Stop the pollers and wait for the current ticks to settle. Does not close the shared DataSource. */\n async close(): Promise<void> {\n this.running = false;\n await Promise.allSettled([this.taskLoop, this.resultLoop]);\n }\n}\n","/**\n * The tiny SQL surface `DbTransport` needs, so it rides **whatever ORM/connection the app already\n * has** instead of forcing a specific one. Implement it for your ORM (adapters for TypeORM and\n * MikroORM ship below) and hand it to `new DbTransport({ executor })`. Mirrors how the durable\n * *store* is ORM-pluggable (store-typeorm / store-mikro-orm / …).\n */\nexport interface SqlExecutor {\n /** Drives placeholder style (`$n` vs `?`) and a couple of DDL type choices. */\n readonly dialect: 'mysql' | 'postgres';\n /** Quote/escape an identifier (table/column name) for this dialect. */\n escapeId(id: string): string;\n /** Run a statement outside a transaction. */\n query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;\n /** Run `fn` inside a transaction — the claim uses `FOR UPDATE SKIP LOCKED` within it. */\n transaction<T>(fn: (tx: SqlTx) => Promise<T>): Promise<T>;\n}\n\nexport interface SqlTx {\n query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;\n}\n\n// ── TypeORM ──────────────────────────────────────────────────────────────────────────────────────\n\n/** Structural slice of a TypeORM `DataSource` — avoids a hard dependency on typeorm. */\ninterface TypeOrmDataSourceLike {\n options: { type: string };\n driver: { escape(id: string): string };\n query(sql: string, params?: unknown[]): Promise<unknown>;\n createQueryRunner(): {\n connect(): Promise<void>;\n startTransaction(): Promise<void>;\n commitTransaction(): Promise<void>;\n rollbackTransaction(): Promise<void>;\n release(): Promise<void>;\n query(sql: string, params?: unknown[]): Promise<unknown>;\n };\n}\n\n/** Build an executor from a TypeORM `DataSource` (e.g. the one the durable store already uses). */\nexport function typeOrmExecutor(ds: TypeOrmDataSourceLike): SqlExecutor {\n return {\n dialect: /postgres/.test(String(ds.options.type)) ? 'postgres' : 'mysql',\n escapeId: (id) => ds.driver.escape(id),\n query: <T>(sql: string, params?: unknown[]) => ds.query(sql, params) as Promise<T[]>,\n transaction: async <T>(fn: (tx: SqlTx) => Promise<T>) => {\n const qr = ds.createQueryRunner();\n await qr.connect();\n await qr.startTransaction();\n try {\n const result = await fn({\n query: <U>(sql: string, params?: unknown[]) => qr.query(sql, params) as Promise<U[]>,\n });\n await qr.commitTransaction();\n return result;\n } catch (err) {\n await qr.rollbackTransaction();\n throw err;\n } finally {\n await qr.release();\n }\n },\n };\n}\n\n// ── MikroORM ─────────────────────────────────────────────────────────────────────────────────────\n\n/** Structural slice of a MikroORM `EntityManager` — avoids a hard dependency on @mikro-orm/core. */\ninterface MikroOrmEmLike {\n getPlatform(): { constructor: { name: string }; quoteIdentifier(id: string): string };\n getConnection(): {\n execute(sql: string, params?: unknown[], method?: 'all', ctx?: unknown): Promise<unknown>;\n };\n getTransactionContext?(): unknown;\n transactional<T>(cb: (em: MikroOrmEmLike) => Promise<T>): Promise<T>;\n}\n\n/**\n * Build an executor from a MikroORM `EntityManager` — i.e. **the app's own configured ORM**. Runs\n * raw SQL through `em.getConnection().execute(...)` and wraps the claim in `em.transactional(...)`,\n * threading the transaction context so `FOR UPDATE SKIP LOCKED` locks within the tx.\n */\nexport function mikroOrmExecutor(em: MikroOrmEmLike): SqlExecutor {\n const platform = em.getPlatform();\n return {\n dialect: /postgre/i.test(platform.constructor.name) ? 'postgres' : 'mysql',\n escapeId: (id) => platform.quoteIdentifier(id),\n query: <T>(sql: string, params?: unknown[]) =>\n em.getConnection().execute(sql, params ?? [], 'all') as Promise<T[]>,\n transaction: <T>(fn: (tx: SqlTx) => Promise<T>) =>\n em.transactional((txEm) =>\n fn({\n query: <U>(sql: string, params?: unknown[]) =>\n txEm\n .getConnection()\n .execute(sql, params ?? [], 'all', txEm.getTransactionContext?.()) as Promise<U[]>,\n }),\n ),\n };\n}\n"],"mappings":";;;;AAAA,SAMEA,sBACK;;;ACgCA,SAASC,gBAAgBC,IAAyB;AACvD,SAAO;IACLC,SAAS,WAAWC,KAAKC,OAAOH,GAAGI,QAAQC,IAAI,CAAA,IAAK,aAAa;IACjEC,UAAU,wBAACC,OAAOP,GAAGQ,OAAOC,OAAOF,EAAAA,GAAzB;IACVG,OAAO,wBAAIC,KAAaC,WAAuBZ,GAAGU,MAAMC,KAAKC,MAAAA,GAAtD;IACPC,aAAa,8BAAUC,OAAAA;AACrB,YAAMC,KAAKf,GAAGgB,kBAAiB;AAC/B,YAAMD,GAAGE,QAAO;AAChB,YAAMF,GAAGG,iBAAgB;AACzB,UAAI;AACF,cAAMC,SAAS,MAAML,GAAG;UACtBJ,OAAO,wBAAIC,KAAaC,WAAuBG,GAAGL,MAAMC,KAAKC,MAAAA,GAAtD;QACT,CAAA;AACA,cAAMG,GAAGK,kBAAiB;AAC1B,eAAOD;MACT,SAASE,KAAK;AACZ,cAAMN,GAAGO,oBAAmB;AAC5B,cAAMD;MACR,UAAA;AACE,cAAMN,GAAGQ,QAAO;MAClB;IACF,GAhBa;EAiBf;AACF;AAvBgBxB;AA0CT,SAASyB,iBAAiBC,IAAkB;AACjD,QAAMC,WAAWD,GAAGE,YAAW;AAC/B,SAAO;IACL1B,SAAS,WAAWC,KAAKwB,SAAS,YAAYE,IAAI,IAAI,aAAa;IACnEtB,UAAU,wBAACC,OAAOmB,SAASG,gBAAgBtB,EAAAA,GAAjC;IACVG,OAAO,wBAAIC,KAAaC,WACtBa,GAAGK,cAAa,EAAGC,QAAQpB,KAAKC,UAAU,CAAA,GAAI,KAAA,GADzC;IAEPC,aAAa,wBAAIC,OACfW,GAAGO,cAAc,CAACC,SAChBnB,GAAG;MACDJ,OAAO,wBAAIC,KAAaC,WACtBqB,KACGH,cAAa,EACbC,QAAQpB,KAAKC,UAAU,CAAA,GAAI,OAAOqB,KAAKC,wBAAqB,CAAA,GAH1D;IAIT,CAAA,CAAA,GAPS;EASf;AACF;AAjBgBV;;;ADdT,IAAMW,cAAN,MAAMA;EAnEb,OAmEaA;;;EACMC;EACAC;EACAC;EACAC;EACAC;EACAC;EACAC;EACAC;EACAC;EACAC;EAEAC,WAAW,oBAAIC,IAAAA;EACxBC,UAAU;EACVC;EACAC;EACAC;EAER,YAAYC,SAA6B;AACvC,SAAKhB,OAAOgB,QAAQC;AACpB,SAAKhB,QAAQe,QAAQf;AACrB,UAAMiB,SAASF,QAAQE,UAAU;AACjC,SAAKhB,aAAa,GAAGgB,MAAAA;AACrB,SAAKf,eAAe,GAAGe,MAAAA;AACvB,SAAKd,SAASY,QAAQZ,UAAU;AAChC,SAAKC,UAAUW,QAAQX,WAAW;AAClC,SAAKC,YAAYU,QAAQV,aAAa;AACtC,SAAKC,aAAaS,QAAQT,cAAc;AACxC,SAAKC,aAAaQ,QAAQR,cAAc,MAAMW,KAAKC,MAAMC,KAAKC,IAAG,CAAA,EAAIC,SAAS,EAAA,CAAA;AAC9E,SAAKd,OAAO,KAAKT,KAAKwB,YAAY;EACpC;;;EAIQC,GAAGC,GAAmB;AAC5B,WAAO,KAAKjB,OAAO,IAAIiB,CAAAA,KAAM;EAC/B;EACQC,EAAEC,IAAoB;AAC5B,WAAO,KAAK5B,KAAK6B,SAASD,EAAAA;EAC5B;EACQN,MAAc;AACpB,WAAOH,KAAKC,MAAMC,KAAKC,IAAG,CAAA;EAC5B;;EAGQQ,eAA8B;AACpC,QAAI,CAAC,KAAKjB,YAAa,MAAKA,cAAc,KAAKkB,aAAY;AAC3D,WAAO,KAAKlB;EACd;EAEA,MAAckB,eAA8B;AAC1C,QAAI,CAAC,KAAKxB,WAAY;AACtB,UAAMyB,MAAM;AACZ,UAAMC,MAAM,KAAKxB,OAAO,SAAS;AACjC,UAAMyB,QAAQ,KAAKP,EAAE,KAAKzB,UAAU;AACpC,UAAMiC,UAAU,KAAKR,EAAE,KAAKxB,YAAY;AACxC,UAAM,KAAKH,KAAKoC,MACd,8BAA8BF,KAAAA;UAC1B,KAAKP,EAAE,SAAA,CAAA,IAAcK,GAAAA;UACrB,KAAKL,EAAE,QAAA,CAAA,IAAaK,GAAAA;UACpB,KAAKL,EAAE,KAAA,CAAA;UACP,KAAKA,EAAE,MAAA,CAAA,IAAWK,GAAAA;UAClB,KAAKL,EAAE,KAAA,CAAA,IAAUK,GAAAA;UACjB,KAAKL,EAAE,OAAA,CAAA,IAAYM,GAAAA;UACnB,KAAKN,EAAE,SAAA,CAAA;UACP,KAAKA,EAAE,QAAA,CAAA;UACP,KAAKA,EAAE,YAAA,CAAA,IAAiBK,GAAAA;UACxB,KAAKL,EAAE,YAAA,CAAA;UACP,KAAKA,EAAE,YAAA,CAAA;QACT;AAEJ,UAAM,KAAK3B,KAAKoC,MACd,8BAA8BD,OAAAA;UAC1B,KAAKR,EAAE,SAAA,CAAA,IAAcK,GAAAA;UACrB,KAAKL,EAAE,QAAA,CAAA,IAAaK,GAAAA;UACpB,KAAKL,EAAE,KAAA,CAAA;UACP,KAAKA,EAAE,QAAA,CAAA;UACP,KAAKA,EAAE,QAAA,CAAA,IAAaM,GAAAA;UACpB,KAAKN,EAAE,OAAA,CAAA,IAAYM,GAAAA;UACnB,KAAKN,EAAE,YAAA,CAAA;UACP,KAAKA,EAAE,YAAA,CAAA,IAAiBK,GAAAA;UACxB,KAAKL,EAAE,YAAA,CAAA;UACP,KAAKA,EAAE,YAAA,CAAA;QACT;AAGJ,UAAM,KAAK3B,KACRoC,MACC,gBAAgB,KAAKT,EAAE,GAAG,KAAKzB,UAAU,UAAU,CAAA,OAAQgC,KAAAA,KAAU,KAAKP,EAAE,KAAA,CAAA,KAAW,KAAKA,EAAE,QAAA,CAAA,KAAc,KAAKA,EAAE,YAAA,CAAA,GAAgB,EAEpIU,MAAM,MAAA;IAAO,CAAA;EAClB;;EAGA,MAAMC,SAASC,MAAiC;AAC9C,UAAM,KAAKT,aAAY;AACvB,UAAMU,IAAI,KAAKb,EAAE,KAAKzB,UAAU;AAEhC,UAAMuC,SAAS,KAAKhC,OAChB,eAAe+B,CAAAA,uBAAwB,KAAKb,EAAE,SAAA,CAAA,iBAC9C,sBAAsBa,CAAAA;AAC1B,UAAME,OAAO;MAAC;MAAW;MAAU;MAAO;MAAQ;MAAO;MAAS;MAAW;MAAU;;AACvF,UAAMC,MAAMF,OAAOG,QACjB,SACA,IAAIF,KAAKG,IAAI,CAACC,MAAM,KAAKnB,EAAEmB,CAAAA,CAAAA,EAAIC,KAAK,IAAA,CAAA,aAAkBL,KAAKG,IAAI,CAACG,GAAGtB,MAAM,KAAKD,GAAGC,IAAI,CAAA,CAAA,EAAIqB,KAAK,IAAA,CAAA,GAAQ;AAExG,UAAM,KAAK/C,KAAKoC,MAAMO,KAAK;MACzBJ,KAAKU;MACLV,KAAKW;MACLX,KAAKY;MACLZ,KAAKa;MACLb,KAAKtC;MACLoD,KAAKC,UAAUf,KAAKgB,SAAS,IAAA;MAC7BhB,KAAKiB;MACL;MACA,KAAKlC,IAAG;KACT;EACH;;EAGAmC,OAAOL,MAAcM,IAAuB;AAC1C,QAAI,CAAC,KAAKzD,MAAO,OAAM,IAAI0D,MAAM,kDAAA;AACjC,SAAKjD,SAASkD,IAAIR,MAAMM,EAAAA;AACxB,QAAI,CAAC,KAAK5C,SAAU,MAAKA,WAAW,KAAK+C,KAAK,MAAM,KAAKC,WAAW,KAAK7D,KAAK,CAAA;EAChF;EAEA8D,SAASC,SAAsD;AAC7D,QAAI,CAAC,KAAKjD,WAAY,MAAKA,aAAa,KAAK8C,KAAK,MAAM,KAAKI,aAAaD,OAAAA,CAAAA;EAC5E;EAEAE,YAAYC,UAAoD;EAEhE;;;EAIA,MAAcN,KAAKO,MAA4C;AAC7D,UAAM,KAAKtC,aAAY;AACvB,WAAO,KAAKlB,SAAS;AACnB,UAAIyD,YAAY;AAChB,UAAI;AACFA,oBAAY,MAAMD,KAAAA;MACpB,SAASE,KAAK;AACZ,YAAI,KAAK1D,QAAS2D,SAAQC,MAAM,6BAA6BF,GAAAA;MAC/D;AACA,UAAID,cAAc,EAAG,OAAM,KAAKI,MAAM,KAAKrE,MAAM;IACnD;EACF;;EAGA,MAAc0D,WAAW7D,OAAgC;AACvD,UAAMyE,UAAU,MAAM,KAAKC,MACzB,KAAKzE,YACL,GAAG,KAAKyB,EAAE,KAAA,CAAA,MAAY,KAAKF,GAAG,CAAA,CAAA,IAC9B;MAACxB;KAAM;AAET,eAAW2E,OAAOF,SAAS;AACzB,YAAMnC,OAAmB;QACvBW,OAAO0B,IAAIC;QACX1B,KAAK2B,OAAOF,IAAIzB,GAAG;QACnBF,QAAQ2B,IAAIG;QACZ3B,MAAMwB,IAAIxB;QACVnD;QACAsD,OAAOqB,IAAIrB,SAAS,OAAOyB,SAAY3B,KAAK4B,MAAML,IAAIrB,KAAK;QAC3DC,SAASsB,OAAOF,IAAIpB,OAAO;MAC7B;AACA,YAAM0B,SAAS,MAAMC,eAAe5C,MAAM,KAAK7B,SAAS0E,IAAI7C,KAAKa,IAAI,CAAA;AACrE,YAAM,KAAKiC,aAAaH,MAAAA;AACxB,YAAM,KAAKlF,KAAKoC,MACd,eAAe,KAAKT,EAAE,KAAKzB,UAAU,CAAA,UAAW,KAAKyB,EAAE,SAAA,CAAA,MAAgB,KAAKF,GAAG,CAAA,CAAA,IAC/E;QAACc,KAAKU;OAAO;IAEjB;AACA,WAAOyB,QAAQY;EACjB;;EAGA,MAAcrB,aAAaD,SAAiE;AAC1F,UAAMU,UAAU,MAAM,KAAKC,MAAiB,KAAKxE,cAAc,SAAS,CAAA,CAAE;AAC1E,eAAWyE,OAAOF,SAAS;AACzB,YAAMQ,SAAqB;QACzBhC,OAAO0B,IAAIC;QACX1B,KAAK2B,OAAOF,IAAIzB,GAAG;QACnBF,QAAQ2B,IAAIG;QACZQ,QAAQX,IAAIW;QACZC,QAAQZ,IAAIY,UAAU,OAAOR,SAAY3B,KAAK4B,MAAML,IAAIY,MAAM;QAC9DhB,OAAOI,IAAIJ,SAAS,OAAOQ,SAAY3B,KAAK4B,MAAML,IAAIJ,KAAK;QAC3DiB,WAAWb,IAAIc,cAAc,OAAOV,SAAYF,OAAOF,IAAIc,UAAU;MACvE;AACA,YAAM1B,QAAQkB,MAAAA;AACd,YAAM,KAAKlF,KAAKoC,MACd,eAAe,KAAKT,EAAE,KAAKxB,YAAY,CAAA,UAAW,KAAKwB,EAAE,SAAA,CAAA,MAAgB,KAAKF,GAAG,CAAA,CAAA,IACjF;QAACyD,OAAOjC;OAAO;IAEnB;AACA,WAAOyB,QAAQY;EACjB;;;;;;EAOA,MAAcX,MACZgB,OACAC,OACAC,aACc;AACd,UAAMrD,IAAI,KAAKb,EAAEgE,KAAAA;AACjB,UAAMG,cAAc,KAAKxE,IAAG,IAAK,KAAKjB;AACtC,WAAO,KAAKL,KAAK+F,YAAY,OAAOC,OAAAA;AAClC,YAAMC,cAAc,IAAI,KAAKtE,EAAE,YAAA,CAAA,eAA4B,KAAKA,EAAE,YAAA,CAAA,MAAmB,KAAKF,GAAGoE,YAAYP,SAAS,CAAA,CAAA;AAClH,YAAMY,OAAO,MAAMF,GAAG5D,MACpB,iBAAiBI,CAAAA,UAAWoD,KAAAA,QAAaK,WAAAA;oBAC7B,KAAKtE,EAAE,YAAA,CAAA,cAA2B,KAAKrB,SAAS,2BAC5D;WAAIuF;QAAaC;OAAY;AAE/B,UAAII,KAAKZ,SAAS,GAAG;AACnB,cAAMa,MAAMD,KAAKrD,IAAI,CAACuD,MAAMA,EAAErB,OAAO;AACrC,cAAMsB,eAAeF,IAAItD,IAAI,CAACG,GAAGtB,MAAM,KAAKD,GAAGC,IAAI,CAAA,CAAA,EAAIqB,KAAK,IAAA;AAC5D,cAAMiD,GAAG5D,MACP,UAAUI,CAAAA,QAAS,KAAKb,EAAE,YAAA,CAAA,MAAmB,KAAKF,GAAG,CAAA,CAAA,KAAO,KAAKE,EAAE,YAAA,CAAA,MAAmB,KAAKF,GAAG,CAAA,CAAA;mBACrF,KAAKE,EAAE,SAAA,CAAA,QAAkB0E,YAAAA,KAClC;UAAC,KAAK7F;UAAY,KAAKc,IAAG;aAAO6E;SAAI;MAEzC;AACA,aAAOD;IACT,CAAA;EACF;EAEA,MAAcb,aAAaH,QAAmC;AAC5D,UAAM1C,IAAI,KAAKb,EAAE,KAAKxB,YAAY;AAClC,UAAMuC,OAAO;MAAC;MAAW;MAAU;MAAO;MAAU;MAAU;MAAS;MAAc;;AACrF,UAAM4D,OAAO,KAAK7F,OACd,eAAe+B,CAAAA,uBAAwB,KAAKb,EAAE,SAAA,CAAA,iBAC9C,sBAAsBa,CAAAA;AAC1B,UAAMG,MAAM2D,KAAK1D,QACf,SACA,IAAIF,KAAKG,IAAI,CAACC,MAAM,KAAKnB,EAAEmB,CAAAA,CAAAA,EAAIC,KAAK,IAAA,CAAA,aAAkBL,KAAKG,IAAI,CAACG,GAAGtB,MAAM,KAAKD,GAAGC,IAAI,CAAA,CAAA,EAAIqB,KAAK,IAAA,CAAA,GAAQ;AAExG,UAAM,KAAK/C,KAAKoC,MAAMO,KAAK;MACzBuC,OAAOjC;MACPiC,OAAOhC;MACPgC,OAAO/B;MACP+B,OAAOK;MACPL,OAAOM,WAAWR,SAAY,OAAO3B,KAAKC,UAAU4B,OAAOM,MAAM;MACjEN,OAAOV,UAAUQ,SAAY,OAAO3B,KAAKC,UAAU4B,OAAOV,KAAK;MAC/DU,OAAOO,aAAa;MACpB,KAAKnE,IAAG;KACT;EACH;EAEQmD,MAAM8B,IAA2B;AACvC,WAAO,IAAIC,QAAQ,CAACJ,MAAMK,WAAWL,GAAGG,EAAAA,CAAAA;EAC1C;;EAGA,MAAMG,QAAuB;AAC3B,SAAK9F,UAAU;AACf,UAAM4F,QAAQG,WAAW;MAAC,KAAK7F;MAAU,KAAKC;KAAW;EAC3D;AACF;","names":["runStepHandler","typeOrmExecutor","ds","dialect","test","String","options","type","escapeId","id","driver","escape","query","sql","params","transaction","fn","qr","createQueryRunner","connect","startTransaction","result","commitTransaction","err","rollbackTransaction","release","mikroOrmExecutor","em","platform","getPlatform","name","quoteIdentifier","getConnection","execute","transactional","txEm","getTransactionContext","DbTransport","exec","group","tasksTable","resultsTable","pollMs","leaseMs","batchSize","autoCreate","instanceId","isPg","handlers","Map","running","schemaReady","taskLoop","resultLoop","options","executor","prefix","Math","floor","Date","now","toString","dialect","ph","i","q","id","escapeId","ensureSchema","createTables","str","txt","tasks","results","query","catch","dispatch","task","t","ignore","cols","sql","replace","map","c","join","_","stepId","runId","seq","name","JSON","stringify","input","attempt","handle","fn","Error","set","loop","drainTasks","onResult","handler","drainResults","onHeartbeat","_handler","tick","processed","err","console","error","sleep","claimed","claim","row","run_id","Number","step_id","undefined","parse","result","runStepHandler","get","insertResult","length","status","output","startedAt","started_at","table","where","whereParams","staleBefore","transaction","tx","leaseClause","rows","ids","r","placeholders","head","ms","Promise","setTimeout","close","allSettled"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-durable-transport-db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Database (SQL) Transport for nestjs-durable — DBOS-style: dispatch remote steps as rows in the durable DB, no broker",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"url": "https://github.com/DavideCarvalho/nestjs-durable.git",
|
|
10
10
|
"directory": "packages/transport-db"
|
|
11
11
|
},
|
|
12
|
-
"type": "
|
|
13
|
-
"main": "dist/index.
|
|
14
|
-
"types": "dist/index.d.ts",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
@@ -20,15 +20,31 @@
|
|
|
20
20
|
"typeorm": "^0.3.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
+
"@testcontainers/postgresql": "10.18.0",
|
|
23
24
|
"mysql2": "3.11.5",
|
|
25
|
+
"pg": "8.13.1",
|
|
24
26
|
"reflect-metadata": "0.2.2",
|
|
25
27
|
"typeorm": "0.3.30",
|
|
26
28
|
"typescript": "5.9.3",
|
|
27
29
|
"zod": "3.25.76",
|
|
28
|
-
"
|
|
30
|
+
"tsup": "8.3.5",
|
|
31
|
+
"@dudousxd/nestjs-durable-core": "^0.28.0"
|
|
32
|
+
},
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"default": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/index.d.cts",
|
|
42
|
+
"default": "./dist/index.cjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
29
45
|
},
|
|
30
46
|
"scripts": {
|
|
31
|
-
"build": "
|
|
47
|
+
"build": "tsup",
|
|
32
48
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
33
49
|
}
|
|
34
50
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"db-transport.d.ts","sourceRoot":"","sources":["../src/db-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EAEf,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAqB9F,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,EAAE,WAAW,CAAC;IACtB,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iHAAiH;IACjH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uGAAuG;IACvG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAY,YAAW,SAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,WAAW,CAAC,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,UAAU,CAAC,CAAgB;gBAEvB,OAAO,EAAE,kBAAkB;IAevC,uFAAuF;IACvF,OAAO,CAAC,EAAE;IAGV,OAAO,CAAC,CAAC;IAGT,OAAO,CAAC,GAAG;IAKX,OAAO,CAAC,YAAY;YAKN,YAAY;IA4CpB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B/C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI;IAM3C,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI9D,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAK/D,gGAAgG;YAClF,IAAI;IAalB,uFAAuF;YACzE,UAAU;IA0BxB,4EAA4E;YAC9D,YAAY;IAqB1B;;;;OAIG;YACW,KAAK;YA2BL,YAAY;IAsB1B,OAAO,CAAC,KAAK;IAIb,uGAAuG;IACjG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|