@iterativeflow/mysql 2.2.0 → 2.4.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 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(`DELETE FROM ${t.signal} WHERE id IN ${inList$1(fx.consumeSignals.length)}`, fx.consumeSignals);
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
- ORDER BY seq LIMIT ?`, params)).map((r) => r.id);
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
  },
@@ -555,7 +595,8 @@ const createMysqlStore = (sql, t, id) => {
555
595
  deleteRunsOlderThan: (before, limit) => deleteRuns({ before }, limit),
556
596
  retryRun(runId) {
557
597
  return sql.tx(async (tx) => {
558
- if ((await tx.exec(`UPDATE ${t.run} SET status = 'pending', error = NULL WHERE id = ? AND status = 'failed'`, [runId])).affectedRows !== 1) return { retried: false };
598
+ if ((await tx.exec(`UPDATE ${t.run} SET status = 'pending', error = NULL, attempts = 0
599
+ WHERE id = ? AND status = 'failed'`, [runId])).affectedRows !== 1) return { retried: false };
559
600
  await enqueueStmt(tx, t, runId);
560
601
  return { retried: true };
561
602
  });
@@ -565,6 +606,8 @@ const createMysqlStore = (sql, t, id) => {
565
606
  (name, schedule, flow_name, flow_version, input, overlap, next_run_at)
566
607
  VALUES (?, ?, ?, ?, ?, ?, ?)
567
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),
568
611
  schedule = VALUES(schedule),
569
612
  flow_name = VALUES(flow_name),
570
613
  flow_version = VALUES(flow_version),
@@ -582,6 +625,12 @@ const createMysqlStore = (sql, t, id) => {
582
625
  async dueCrons(now, limit) {
583
626
  return (await sql.query(`SELECT * FROM ${t.cron} WHERE next_run_at <= ? ORDER BY next_run_at LIMIT ?`, [now.getTime(), limit])).map(mapCron);
584
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
+ },
585
634
  async dueCronCount(now, names) {
586
635
  if (names && names.length === 0) return 0;
587
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.2.0",
3
+ "version": "2.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ahmedrowaihi/iterativeflow.git",
8
- "directory": "v2/packages/mysql"
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.2.0"
28
+ "@iterativeflow/core": "2.4.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
  }