@iterativeflow/mysql 2.3.0 → 2.5.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/README.md +1 -3
- package/dist/index.mjs +53 -5
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -13,9 +13,7 @@ import { createEngine } from "@iterativeflow/core";
|
|
|
13
13
|
|
|
14
14
|
const sql = mysqlPool(createPool(process.env.MYSQL_URL));
|
|
15
15
|
await applySchema(sql);
|
|
16
|
-
const engine = createEngine(createMysqlBackend(sql), [
|
|
17
|
-
/* your flows */
|
|
18
|
-
]);
|
|
16
|
+
const engine = createEngine(createMysqlBackend(sql), [/* your flows */]);
|
|
19
17
|
```
|
|
20
18
|
|
|
21
19
|
## Notes
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NON_SUCCESS_TERMINAL_STATUSES, RECONCILABLE_STATUSES, TERMINAL_STATUSES, assertSqlIdentifier, createLocalWakeup, isTerminal, newId, orphanedRunsSql, purgeWhereSql, queueDepthOf, statusList, zeroRunStats } from "@iterativeflow/core/backend";
|
|
1
|
+
import { ACTIVE_STATUSES, NON_SUCCESS_TERMINAL_STATUSES, RECONCILABLE_STATUSES, TERMINAL_STATUSES, assertSqlIdentifier, createLocalWakeup, isTerminal, newId, orphanedRunsSql, purgeWhereSql, queueDepthOf, runSetWhereSql, statusList, zeroRunStats } from "@iterativeflow/core/backend";
|
|
2
2
|
//#region src/queue.ts
|
|
3
3
|
/** @internal */
|
|
4
4
|
const createMysqlQueue = (sql, t, id) => {
|
|
@@ -183,6 +183,7 @@ const ddl = (prefix = "") => {
|
|
|
183
183
|
name VARCHAR(191) NOT NULL,
|
|
184
184
|
payload LONGTEXT,
|
|
185
185
|
idem_key VARCHAR(191),
|
|
186
|
+
consumed TINYINT(1) NOT NULL DEFAULT 0,
|
|
186
187
|
KEY signal_inbox (run_id, seq),
|
|
187
188
|
UNIQUE KEY signal_idem (run_id, idem_key)
|
|
188
189
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
|
@@ -216,9 +217,16 @@ const ddl = (prefix = "") => {
|
|
|
216
217
|
)`
|
|
217
218
|
];
|
|
218
219
|
};
|
|
220
|
+
const addSignalConsumed = async (sql, t) => {
|
|
221
|
+
const cols = await sql.query(`SELECT COUNT(*) AS n FROM information_schema.columns
|
|
222
|
+
WHERE table_schema = DATABASE() AND table_name = ? AND column_name = 'consumed'`, [t.signal.replace(/`/g, "")]);
|
|
223
|
+
if (Number(cols[0]?.n ?? 0) > 0) return;
|
|
224
|
+
await sql.query(`ALTER TABLE ${t.signal} ADD COLUMN consumed TINYINT(1) NOT NULL DEFAULT 0`);
|
|
225
|
+
};
|
|
219
226
|
/** Apply the schema DDL (idempotent). Run once before use. */
|
|
220
227
|
const applySchema = async (sql, prefix = "") => {
|
|
221
228
|
for (const stmt of ddl(prefix)) await sql.query(stmt);
|
|
229
|
+
await addSignalConsumed(sql, tables(prefix));
|
|
222
230
|
};
|
|
223
231
|
//#endregion
|
|
224
232
|
//#region src/codec.ts
|
|
@@ -300,7 +308,7 @@ const applyOutbox = async (sql, t, fx) => {
|
|
|
300
308
|
for (const e of fx.enqueue ?? []) await enqueueStmt(sql, t, e.runId, e.opts);
|
|
301
309
|
for (const tm of fx.timers ?? []) await scheduleStmt(sql, t, tm.runId, tm.fireAt);
|
|
302
310
|
if (fx.cancelTimers?.length) await sql.exec(`DELETE FROM ${t.timer} WHERE run_id IN ${inList$1(fx.cancelTimers.length)}`, fx.cancelTimers);
|
|
303
|
-
if (fx.consumeSignals?.length) await sql.exec(`
|
|
311
|
+
if (fx.consumeSignals?.length) await sql.exec(`UPDATE ${t.signal} SET consumed = 1 WHERE id IN ${inList$1(fx.consumeSignals.length)}`, fx.consumeSignals);
|
|
304
312
|
if (fx.joinTarget) await sql.exec(`UPDATE ${t.run} SET join_remaining = ? WHERE id = ?`, [fx.joinTarget.count, fx.joinTarget.runId]);
|
|
305
313
|
};
|
|
306
314
|
//#endregion
|
|
@@ -311,7 +319,8 @@ const RECONCILABLE = sqlTuple(RECONCILABLE_STATUSES);
|
|
|
311
319
|
const NON_SUCCESS_TERMINAL = sqlTuple(NON_SUCCESS_TERMINAL_STATUSES);
|
|
312
320
|
const MS_PURGE = {
|
|
313
321
|
placeholder: () => "?",
|
|
314
|
-
time: (at) => at.getTime()
|
|
322
|
+
time: (at) => at.getTime(),
|
|
323
|
+
statusTuple: sqlTuple
|
|
315
324
|
};
|
|
316
325
|
const inList = (n) => `(${Array.from({ length: n }, () => "?").join(",")})`;
|
|
317
326
|
/** @internal */
|
|
@@ -375,11 +384,12 @@ const createMysqlStore = (sql, t, id) => {
|
|
|
375
384
|
const deleteRuns = async (filter, limit) => {
|
|
376
385
|
const q = purgeWhereSql(filter, MS_PURGE);
|
|
377
386
|
if (!q) return 0;
|
|
387
|
+
const order = filter.before === void 0 ? "" : " ORDER BY seq";
|
|
378
388
|
const params = [...q.params, limit];
|
|
379
389
|
return sql.tx(async (tx) => {
|
|
380
390
|
const ids = (await tx.query(`SELECT id FROM ${t.run}
|
|
381
391
|
WHERE ${q.where}
|
|
382
|
-
|
|
392
|
+
${order} LIMIT ?`, params)).map((r) => r.id);
|
|
383
393
|
if (ids.length === 0) return 0;
|
|
384
394
|
const inIds = inList(ids.length);
|
|
385
395
|
await tx.exec(`DELETE FROM ${t.step} WHERE run_id IN ${inIds}`, ids);
|
|
@@ -405,7 +415,7 @@ const createMysqlStore = (sql, t, id) => {
|
|
|
405
415
|
const [runRows, stepRows, sigRows] = await Promise.all([
|
|
406
416
|
sql.query(`SELECT * FROM ${t.run} WHERE id = ?`, [runId]),
|
|
407
417
|
sql.query(`SELECT cursor_key, status, result, error, attempts, shape FROM ${t.step} WHERE run_id = ?`, [runId]),
|
|
408
|
-
sql.query(`SELECT id, name, payload FROM ${t.signal} WHERE run_id = ? ORDER BY seq`, [runId])
|
|
418
|
+
sql.query(`SELECT id, name, payload FROM ${t.signal} WHERE run_id = ? AND consumed = 0 ORDER BY seq`, [runId])
|
|
409
419
|
]);
|
|
410
420
|
const runRow = runRows[0];
|
|
411
421
|
if (!runRow) return void 0;
|
|
@@ -512,6 +522,10 @@ const createMysqlStore = (sql, t, id) => {
|
|
|
512
522
|
where.push(`name = ?`);
|
|
513
523
|
params.push(filter.name);
|
|
514
524
|
}
|
|
525
|
+
if (filter.version !== void 0) {
|
|
526
|
+
where.push(`version = ?`);
|
|
527
|
+
params.push(filter.version);
|
|
528
|
+
}
|
|
515
529
|
if (filter.tag) {
|
|
516
530
|
where.push(`JSON_CONTAINS(tags, JSON_QUOTE(?))`);
|
|
517
531
|
params.push(filter.tag);
|
|
@@ -530,6 +544,32 @@ const createMysqlStore = (sql, t, id) => {
|
|
|
530
544
|
cursor
|
|
531
545
|
};
|
|
532
546
|
},
|
|
547
|
+
async cancelRuns(filter, limit) {
|
|
548
|
+
const q = runSetWhereSql(filter, ACTIVE_STATUSES, "cancelRuns", MS_PURGE);
|
|
549
|
+
if (!q) return 0;
|
|
550
|
+
const params = [...q.params, limit];
|
|
551
|
+
return sql.tx(async (tx) => {
|
|
552
|
+
const ids = (await tx.query(`SELECT id FROM ${t.run} WHERE ${q.where} LIMIT ? FOR UPDATE`, params)).map((r) => r.id);
|
|
553
|
+
if (ids.length === 0) return 0;
|
|
554
|
+
const inIds = inList(ids.length);
|
|
555
|
+
await tx.exec(`DELETE FROM ${t.timer} WHERE run_id IN ${inIds}`, ids);
|
|
556
|
+
await tx.exec(`UPDATE ${t.run} SET status = 'canceled' WHERE id IN ${inIds}`, ids);
|
|
557
|
+
return ids.length;
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
async retryRuns(filter, limit) {
|
|
561
|
+
const q = runSetWhereSql(filter, ["failed"], "retryRuns", MS_PURGE);
|
|
562
|
+
if (!q) return 0;
|
|
563
|
+
const params = [...q.params, limit];
|
|
564
|
+
return sql.tx(async (tx) => {
|
|
565
|
+
const ids = (await tx.query(`SELECT id FROM ${t.run} WHERE ${q.where} LIMIT ? FOR UPDATE`, params)).map((r) => r.id);
|
|
566
|
+
if (ids.length === 0) return 0;
|
|
567
|
+
const inIds = inList(ids.length);
|
|
568
|
+
await tx.exec(`UPDATE ${t.run} SET status = 'pending', error = NULL, attempts = 0 WHERE id IN ${inIds}`, ids);
|
|
569
|
+
for (const runId of ids) await enqueueStmt(tx, t, runId);
|
|
570
|
+
return ids.length;
|
|
571
|
+
});
|
|
572
|
+
},
|
|
533
573
|
async childrenOf(runId) {
|
|
534
574
|
return (await sql.query(`SELECT * FROM ${t.run} WHERE parent_run_id = ?`, [runId])).map((r) => mapRun(r));
|
|
535
575
|
},
|
|
@@ -566,6 +606,8 @@ const createMysqlStore = (sql, t, id) => {
|
|
|
566
606
|
(name, schedule, flow_name, flow_version, input, overlap, next_run_at)
|
|
567
607
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
568
608
|
ON DUPLICATE KEY UPDATE
|
|
609
|
+
-- MySQL applies these left to right, so compare the schedule BEFORE overwriting it
|
|
610
|
+
next_run_at = IF(schedule <> VALUES(schedule), VALUES(next_run_at), next_run_at),
|
|
569
611
|
schedule = VALUES(schedule),
|
|
570
612
|
flow_name = VALUES(flow_name),
|
|
571
613
|
flow_version = VALUES(flow_version),
|
|
@@ -583,6 +625,12 @@ const createMysqlStore = (sql, t, id) => {
|
|
|
583
625
|
async dueCrons(now, limit) {
|
|
584
626
|
return (await sql.query(`SELECT * FROM ${t.cron} WHERE next_run_at <= ? ORDER BY next_run_at LIMIT ?`, [now.getTime(), limit])).map(mapCron);
|
|
585
627
|
},
|
|
628
|
+
async listCrons() {
|
|
629
|
+
return (await sql.query(`SELECT * FROM ${t.cron} ORDER BY name`)).map(mapCron);
|
|
630
|
+
},
|
|
631
|
+
async removeCron(name) {
|
|
632
|
+
return (await sql.exec(`DELETE FROM ${t.cron} WHERE name = ?`, [name])).affectedRows > 0;
|
|
633
|
+
},
|
|
586
634
|
async dueCronCount(now, names) {
|
|
587
635
|
if (names && names.length === 0) return 0;
|
|
588
636
|
const namePredicate = names ? ` AND flow_name IN ${inList(names.length)}` : "";
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iterativeflow/mysql",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ahmedrowaihi/iterativeflow.git",
|
|
8
|
-
"directory": "
|
|
8
|
+
"directory": "packages/mysql"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@iterativeflow/core": "2.
|
|
28
|
+
"@iterativeflow/core": "2.5.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"mysql2": ">=3.0.0"
|
|
32
32
|
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
33
36
|
"scripts": {
|
|
34
37
|
"build": "tsdown"
|
|
35
38
|
}
|