@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 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
  },
@@ -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.0",
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": "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.3.0"
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
  }