@redflow/client 0.0.2 → 0.0.4

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.
@@ -52,7 +52,7 @@ test("manual run: succeeds and records steps", async () => {
52
52
  },
53
53
  );
54
54
 
55
- const worker = await startWorker({
55
+ const worker = await startWorker({ app: "test-app",
56
56
  redis,
57
57
  prefix,
58
58
  queues: ["q1"],
@@ -82,7 +82,7 @@ test("runAt: scheduled -> promoted -> executed", async () => {
82
82
  return { ok: true };
83
83
  });
84
84
 
85
- const worker = await startWorker({
85
+ const worker = await startWorker({ app: "test-app",
86
86
  redis,
87
87
  prefix,
88
88
  queues: ["q2"],
@@ -120,7 +120,7 @@ test("runAt: cleared after retry exhaustion final failure", async () => {
120
120
  },
121
121
  );
122
122
 
123
- const worker = await startWorker({
123
+ const worker = await startWorker({ app: "test-app",
124
124
  redis,
125
125
  prefix,
126
126
  queues: ["q2_retry_fail"],
@@ -153,7 +153,7 @@ test("NonRetriableError: skips retries and fails immediately on attempt 1", asyn
153
153
  },
154
154
  );
155
155
 
156
- const worker = await startWorker({
156
+ const worker = await startWorker({ app: "test-app",
157
157
  redis,
158
158
  prefix,
159
159
  queues: ["q_non_retriable"],
@@ -196,7 +196,7 @@ test("onFailure: called after retry exhaustion with correct context", async () =
196
196
  },
197
197
  );
198
198
 
199
- const worker = await startWorker({
199
+ const worker = await startWorker({ app: "test-app",
200
200
  redis,
201
201
  prefix,
202
202
  queues: ["q_on_failure_1"],
@@ -239,7 +239,7 @@ test("onFailure: called immediately with NonRetriableError", async () => {
239
239
  },
240
240
  );
241
241
 
242
- const worker = await startWorker({
242
+ const worker = await startWorker({ app: "test-app",
243
243
  redis,
244
244
  prefix,
245
245
  queues: ["q_on_failure_2"],
@@ -283,7 +283,7 @@ test("onFailure: NOT called on cancellation", async () => {
283
283
  },
284
284
  );
285
285
 
286
- const worker = await startWorker({
286
+ const worker = await startWorker({ app: "test-app",
287
287
  redis,
288
288
  prefix,
289
289
  queues: ["q_on_failure_3"],
@@ -333,7 +333,7 @@ test("cron: creates runs and executes workflow", async () => {
333
333
  },
334
334
  );
335
335
 
336
- const worker = await startWorker({
336
+ const worker = await startWorker({ app: "test-app",
337
337
  redis,
338
338
  prefix,
339
339
  queues: ["q4"],
@@ -352,6 +352,75 @@ test("cron: creates runs and executes workflow", async () => {
352
352
  redis.close();
353
353
  });
354
354
 
355
+ test("cron: skips when workflow already running", async () => {
356
+ const prefix = testPrefix();
357
+ const redis = new RedisClient(redisServer.url);
358
+ const client = createClient({ redis, prefix });
359
+ setDefaultClient(client);
360
+
361
+ const manualCountKey = `${prefix}:t:cronSkipManualCount`;
362
+ const cronCountKey = `${prefix}:t:cronSkipCronCount`;
363
+
364
+ defineWorkflow(
365
+ "cron-skip-running-wf",
366
+ {
367
+ queue: "q4_skip",
368
+ cron: [{ expression: "*/5 * * * * *", input: { source: "cron" } }],
369
+ },
370
+ async ({ input, step }) => {
371
+ await step.run({ name: "record-source" }, async () => {
372
+ const source = (input as { source?: string } | null | undefined)?.source;
373
+ if (source === "cron") {
374
+ await redis.incr(cronCountKey);
375
+ } else {
376
+ await redis.incr(manualCountKey);
377
+ }
378
+ return true;
379
+ });
380
+
381
+ await step.run({ name: "hold" }, async () => {
382
+ await new Promise((resolve) => setTimeout(resolve, 6500));
383
+ return true;
384
+ });
385
+
386
+ return { ok: true };
387
+ },
388
+ );
389
+
390
+ const worker = await startWorker({ app: "test-app",
391
+ redis,
392
+ prefix,
393
+ queues: ["q4_skip"],
394
+ concurrency: 2,
395
+ runtime: { leaseMs: 500 },
396
+ });
397
+
398
+ try {
399
+ const manualHandle = await client.emitWorkflow("cron-skip-running-wf", { source: "manual" });
400
+
401
+ await waitFor(
402
+ async () => Number((await redis.get(manualCountKey)) ?? "0") >= 1,
403
+ { timeoutMs: 5000, label: "manual run started" },
404
+ );
405
+
406
+ // Wait long enough for one cron tick while manual run is still executing.
407
+ await new Promise((resolve) => setTimeout(resolve, 5500));
408
+ const cronWhileManualRunning = Number((await redis.get(cronCountKey)) ?? "0");
409
+ expect(cronWhileManualRunning).toBe(0);
410
+
411
+ await manualHandle.result({ timeoutMs: 15_000 });
412
+
413
+ // Cron should resume once the running instance is gone.
414
+ await waitFor(
415
+ async () => Number((await redis.get(cronCountKey)) ?? "0") >= 1,
416
+ { timeoutMs: 10_000, label: "cron resumed after manual run" },
417
+ );
418
+ } finally {
419
+ await worker.stop();
420
+ redis.close();
421
+ }
422
+ }, { timeout: 30_000 });
423
+
355
424
  test("step.runWorkflow: auto idempotency and override are forwarded to child runs", async () => {
356
425
  const prefix = testPrefix();
357
426
  const redis = new RedisClient(redisServer.url);
@@ -388,7 +457,7 @@ test("step.runWorkflow: auto idempotency and override are forwarded to child run
388
457
  return { auto, custom };
389
458
  });
390
459
 
391
- const worker = await startWorker({
460
+ const worker = await startWorker({ app: "test-app",
392
461
  redis,
393
462
  prefix,
394
463
  queues: ["q_rw_parent", "q_rw_child"],
@@ -451,7 +520,7 @@ test("step.runWorkflow: child workflow executes once across parent retries", asy
451
520
  },
452
521
  );
453
522
 
454
- const worker = await startWorker({
523
+ const worker = await startWorker({ app: "test-app",
455
524
  redis,
456
525
  prefix,
457
526
  queues: ["q_rw_retry_parent", "q_rw_retry_child"],
@@ -508,7 +577,7 @@ test("step.runWorkflow: same queue avoids self-deadlock with concurrency 1", asy
508
577
  return await step.runWorkflow({ name: "call-child" }, child, {});
509
578
  });
510
579
 
511
- const worker = await startWorker({
580
+ const worker = await startWorker({ app: "test-app",
512
581
  redis,
513
582
  prefix,
514
583
  queues: ["q_rw_single"],
@@ -532,6 +601,47 @@ test("step.runWorkflow: same queue avoids self-deadlock with concurrency 1", asy
532
601
  redis.close();
533
602
  }, { timeout: 20_000 });
534
603
 
604
+ test("step.emitWorkflow: supports workflow name strings", async () => {
605
+ const prefix = testPrefix();
606
+ const redis = new RedisClient(redisServer.url);
607
+ const client = createClient({ redis, prefix });
608
+ setDefaultClient(client);
609
+
610
+ defineWorkflow("child-emit-by-name", { queue: "q_emit_name_child" }, async ({ input }) => {
611
+ return { ok: true, input };
612
+ });
613
+
614
+ const parent = defineWorkflow("parent-emit-by-name", { queue: "q_emit_name_parent" }, async ({ step }) => {
615
+ const childRunId = await step.emitWorkflow(
616
+ { name: "emit-child-by-name" },
617
+ "child-emit-by-name",
618
+ { value: 1 },
619
+ );
620
+
621
+ return { childRunId };
622
+ });
623
+
624
+ const worker = await startWorker({ app: "test-app",
625
+ redis,
626
+ prefix,
627
+ queues: ["q_emit_name_parent", "q_emit_name_child"],
628
+ concurrency: 2,
629
+ runtime: { leaseMs: 500 },
630
+ });
631
+
632
+ try {
633
+ const parentHandle = await parent.run({});
634
+ const parentOut = await parentHandle.result({ timeoutMs: 8000 });
635
+ expect(typeof parentOut.childRunId).toBe("string");
636
+
637
+ const childOut = await client.makeRunHandle(parentOut.childRunId).result({ timeoutMs: 8000 });
638
+ expect(childOut).toEqual({ ok: true, input: { value: 1 } });
639
+ } finally {
640
+ await worker.stop();
641
+ redis.close();
642
+ }
643
+ }, { timeout: 20_000 });
644
+
535
645
 
536
646
 
537
647
  test("retries: step results are cached across attempts", async () => {
@@ -565,7 +675,7 @@ test("retries: step results are cached across attempts", async () => {
565
675
  },
566
676
  );
567
677
 
568
- const worker = await startWorker({
678
+ const worker = await startWorker({ app: "test-app",
569
679
  redis,
570
680
  prefix,
571
681
  queues: ["q5"],
@@ -613,7 +723,7 @@ test("retries: run queued before worker start keeps workflow maxAttempts", async
613
723
  const queuedState = await client.getRun(handle.id);
614
724
  expect(queuedState?.maxAttempts).toBe(2);
615
725
 
616
- const worker = await startWorker({
726
+ const worker = await startWorker({ app: "test-app",
617
727
  redis,
618
728
  prefix,
619
729
  queues: ["q5_before_sync"],
@@ -646,7 +756,7 @@ test("step timeout: run fails and error is recorded", async () => {
646
756
  return { ok: true };
647
757
  });
648
758
 
649
- const worker = await startWorker({
759
+ const worker = await startWorker({ app: "test-app",
650
760
  redis,
651
761
  prefix,
652
762
  queues: ["q6"],
@@ -696,7 +806,7 @@ test("cancellation: scheduled/queued/running", async () => {
696
806
 
697
807
  // Scheduled cancel.
698
808
  {
699
- const worker = await startWorker({
809
+ const worker = await startWorker({ app: "test-app",
700
810
  redis,
701
811
  prefix,
702
812
  queues: ["q7"],
@@ -732,7 +842,7 @@ test("cancellation: scheduled/queued/running", async () => {
732
842
 
733
843
  // Running cancel.
734
844
  {
735
- const worker = await startWorker({
845
+ const worker = await startWorker({ app: "test-app",
736
846
  redis,
737
847
  prefix,
738
848
  queues: ["q7"],
@@ -815,7 +925,7 @@ test("cancel race: queued cancel before start does not execute handler", async (
815
925
 
816
926
  let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
817
927
  try {
818
- worker = await startWorker({
928
+ worker = await startWorker({ app: "test-app",
819
929
  redis,
820
930
  prefix,
821
931
  queues: [queue],
@@ -936,7 +1046,7 @@ test("idempotencyKey: same key returns same run id and executes once", async ()
936
1046
  return { ok: true };
937
1047
  });
938
1048
 
939
- const worker = await startWorker({
1049
+ const worker = await startWorker({ app: "test-app",
940
1050
  redis,
941
1051
  prefix,
942
1052
  queues: ["q9"],
@@ -974,7 +1084,7 @@ test("idempotencyKey: delayed TTL refresh cannot fork duplicate runs", async ()
974
1084
  return { ok: true };
975
1085
  });
976
1086
 
977
- const worker = await startWorker({
1087
+ const worker = await startWorker({ app: "test-app",
978
1088
  redis,
979
1089
  prefix,
980
1090
  queues: ["q9_race_fix"],
@@ -1022,7 +1132,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
1022
1132
  setDefaultClient(client);
1023
1133
 
1024
1134
  const wf = defineWorkflow("enqueue-atomic", {queue: "q_enqueue_atomic" }, async () => ({ ok: true }));
1025
- await client.syncRegistry(getDefaultRegistry());
1135
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1026
1136
 
1027
1137
  const originalZadd = redis.zadd.bind(redis);
1028
1138
  let injected = false;
@@ -1041,7 +1151,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
1041
1151
  redis.zadd = originalZadd;
1042
1152
  }
1043
1153
 
1044
- const worker = await startWorker({
1154
+ const worker = await startWorker({ app: "test-app",
1045
1155
  redis,
1046
1156
  prefix,
1047
1157
  queues: ["q_enqueue_atomic"],
@@ -1066,7 +1176,7 @@ test("idempotencyKey: delimiter-like workflow/key pairs do not collide", async (
1066
1176
  const wfA = defineWorkflow("idem:a", {queue: "q9a" }, async () => ({ workflow: "idem:a" }));
1067
1177
  const wfB = defineWorkflow("idem:a:b", {queue: "q9b" }, async () => ({ workflow: "idem:a:b" }));
1068
1178
 
1069
- const worker = await startWorker({
1179
+ const worker = await startWorker({ app: "test-app",
1070
1180
  redis,
1071
1181
  prefix,
1072
1182
  queues: ["q9a", "q9b"],
@@ -1109,7 +1219,7 @@ test("idempotencyTtl: short TTL expires and allows a new run with same key", asy
1109
1219
  return { ok: true };
1110
1220
  });
1111
1221
 
1112
- const worker = await startWorker({
1222
+ const worker = await startWorker({ app: "test-app",
1113
1223
  redis,
1114
1224
  prefix,
1115
1225
  queues: ["q_idem_ttl"],
@@ -1158,7 +1268,7 @@ test("input validation: invalid input fails once and is not retried", async () =
1158
1268
  },
1159
1269
  );
1160
1270
 
1161
- const worker = await startWorker({
1271
+ const worker = await startWorker({ app: "test-app",
1162
1272
  redis,
1163
1273
  prefix,
1164
1274
  queues: ["q11"],
@@ -1193,7 +1303,7 @@ test("unknown workflow: run fails and is not retried", async () => {
1193
1303
  const redis = new RedisClient(redisServer.url);
1194
1304
  const client = createClient({ redis, prefix });
1195
1305
 
1196
- const worker = await startWorker({
1306
+ const worker = await startWorker({ app: "test-app",
1197
1307
  redis,
1198
1308
  prefix,
1199
1309
  queues: ["qx"],
@@ -1246,7 +1356,7 @@ test("cancel during step: run becomes canceled and step error kind is canceled",
1246
1356
  return { ok: true };
1247
1357
  });
1248
1358
 
1249
- const worker = await startWorker({
1359
+ const worker = await startWorker({ app: "test-app",
1250
1360
  redis,
1251
1361
  prefix,
1252
1362
  queues: ["q12"],
@@ -1304,7 +1414,7 @@ test("terminal run re-queued is ignored (no re-execution)", async () => {
1304
1414
  return { ok: true };
1305
1415
  });
1306
1416
 
1307
- const worker = await startWorker({
1417
+ const worker = await startWorker({ app: "test-app",
1308
1418
  redis,
1309
1419
  prefix,
1310
1420
  queues: [queue],
@@ -1352,7 +1462,7 @@ test("lease+reaper: long running step is not duplicated", async () => {
1352
1462
  return { ok: true };
1353
1463
  });
1354
1464
 
1355
- const worker = await startWorker({
1465
+ const worker = await startWorker({ app: "test-app",
1356
1466
  redis,
1357
1467
  prefix,
1358
1468
  queues: [queue],
@@ -1397,8 +1507,8 @@ test(
1397
1507
  },
1398
1508
  );
1399
1509
 
1400
- const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1401
- const w2 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1510
+ const w1 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1511
+ const w2 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1402
1512
 
1403
1513
  await new Promise((r) => setTimeout(r, 3500));
1404
1514
  const count = Number((await redis.get(counterKey)) ?? "0");
@@ -1464,7 +1574,7 @@ test(
1464
1574
  },
1465
1575
  );
1466
1576
 
1467
- const worker = await startWorker({
1577
+ const worker = await startWorker({ app: "test-app",
1468
1578
  redis,
1469
1579
  prefix,
1470
1580
  queues: ["q_parent", "q_child"],
@@ -1519,7 +1629,7 @@ test(
1519
1629
  const queuedMembers = await redis.zrange(queuedIndex, 0, -1);
1520
1630
  expect(queuedMembers.includes(handle.id)).toBe(true);
1521
1631
 
1522
- const worker = await startWorker({
1632
+ const worker = await startWorker({ app: "test-app",
1523
1633
  redis,
1524
1634
  prefix,
1525
1635
  queues: [queue],
@@ -1565,7 +1675,7 @@ test(
1565
1675
  cron: [{ expression: "*/1 * * * * *" }],
1566
1676
  }, async () => ({ ok: true }));
1567
1677
 
1568
- await client.syncRegistry(getDefaultRegistry());
1678
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1569
1679
 
1570
1680
  const workflowKey = `${prefix}:workflow:${name}`;
1571
1681
  const cronIdsJson = await redis.hget(workflowKey, "cronIdsJson");
@@ -1583,7 +1693,7 @@ test(
1583
1693
  queue: "q_update",
1584
1694
  }, async () => ({ ok: true }));
1585
1695
 
1586
- await client.syncRegistry(getDefaultRegistry());
1696
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1587
1697
 
1588
1698
  expect(await redis.hget(`${prefix}:cron:def`, cronId)).toBeNull();
1589
1699
  const cronNext2 = await redis.zrange(`${prefix}:cron:next`, 0, -1);
@@ -1617,7 +1727,7 @@ test(
1617
1727
  },
1618
1728
  );
1619
1729
 
1620
- const worker = await startWorker({
1730
+ const worker = await startWorker({ app: "test-app",
1621
1731
  redis,
1622
1732
  prefix,
1623
1733
  queues: [queue],
@@ -1667,8 +1777,8 @@ test(
1667
1777
  },
1668
1778
  );
1669
1779
 
1670
- const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1671
- const w2 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1780
+ const w1 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1781
+ const w2 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1672
1782
 
1673
1783
  await waitFor(
1674
1784
  async () => Number((await redis.get(counterKey)) ?? "0") >= 1,
@@ -1703,7 +1813,7 @@ test("listRuns: status + workflow filters are applied together", async () => {
1703
1813
  throw new Error("expected failure");
1704
1814
  });
1705
1815
 
1706
- const worker = await startWorker({ redis, prefix, queues: [queue], runtime: { leaseMs: 500 } });
1816
+ const worker = await startWorker({ app: "test-app", redis, prefix, queues: [queue], runtime: { leaseMs: 500 } });
1707
1817
 
1708
1818
  const successHandle = await succeeds.run({});
1709
1819
  const failHandle = await fails.run({});
@@ -1754,7 +1864,7 @@ test("cron trigger ids: same custom id in two workflows does not collide", async
1754
1864
  async () => ({ ok: true }),
1755
1865
  );
1756
1866
 
1757
- await client.syncRegistry(getDefaultRegistry());
1867
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1758
1868
 
1759
1869
  const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-a`, "cronIdsJson")) ?? "[]") as string[];
1760
1870
  const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-b`, "cronIdsJson")) ?? "[]") as string[];
@@ -1782,7 +1892,7 @@ test("syncRegistry: cron trigger with explicit null input preserves null payload
1782
1892
  async () => ({ ok: true }),
1783
1893
  );
1784
1894
 
1785
- await client.syncRegistry(getDefaultRegistry());
1895
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1786
1896
 
1787
1897
  const workflowKey = `${prefix}:workflow:${workflowName}`;
1788
1898
  const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
@@ -1813,7 +1923,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
1813
1923
  async () => ({ ok: true }),
1814
1924
  );
1815
1925
 
1816
- await client.syncRegistry(getDefaultRegistry());
1926
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1817
1927
 
1818
1928
  const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
1819
1929
  expect(cronIds.length).toBe(1);
@@ -1829,7 +1939,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
1829
1939
  async () => ({ ok: true }),
1830
1940
  );
1831
1941
 
1832
- await client.syncRegistry(getDefaultRegistry());
1942
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1833
1943
 
1834
1944
  const after = await redis.zrange(cronNextKey, 0, -1);
1835
1945
  expect(after.includes(cronId)).toBe(false);
@@ -1850,7 +1960,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
1850
1960
  async () => ({ ok: true }),
1851
1961
  );
1852
1962
 
1853
- await client.syncRegistry(getDefaultRegistry());
1963
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1854
1964
 
1855
1965
  const workflowKey = `${prefix}:workflow:${workflowName}`;
1856
1966
  const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
@@ -1862,7 +1972,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
1862
1972
  // Force stale age beyond grace period and sync an empty registry.
1863
1973
  await redis.hset(workflowKey, { updatedAt: "1" });
1864
1974
  __unstableResetDefaultRegistryForTests();
1865
- await client.syncRegistry(getDefaultRegistry());
1975
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1866
1976
 
1867
1977
  expect((await redis.smembers(`${prefix}:workflows`)).includes(workflowName)).toBe(false);
1868
1978
  expect(await redis.hget(workflowKey, "queue")).toBeNull();
@@ -1873,37 +1983,37 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
1873
1983
  redis.close();
1874
1984
  });
1875
1985
 
1876
- test("syncRegistry: stale cleanup is isolated by owner", async () => {
1986
+ test("syncRegistry: stale cleanup is isolated by app", async () => {
1877
1987
  const prefix = testPrefix();
1878
1988
  const redis = new RedisClient(redisServer.url);
1879
1989
  const client = createClient({ redis, prefix });
1880
1990
 
1881
1991
  __unstableResetDefaultRegistryForTests();
1882
- defineWorkflow("owner-a-wf", {queue: "qa",
1992
+ defineWorkflow("app-a-wf", {queue: "qa",
1883
1993
  cron: [{ id: "a", expression: "*/30 * * * * *" }],
1884
1994
  },
1885
1995
  async () => ({ ok: true }),
1886
1996
  );
1887
- await client.syncRegistry(getDefaultRegistry(), { owner: "svc-a" });
1997
+ await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
1888
1998
 
1889
1999
  __unstableResetDefaultRegistryForTests();
1890
- defineWorkflow("owner-b-wf", {queue: "qb",
2000
+ defineWorkflow("app-b-wf", {queue: "qb",
1891
2001
  cron: [{ id: "b", expression: "*/30 * * * * *" }],
1892
2002
  },
1893
2003
  async () => ({ ok: true }),
1894
2004
  );
1895
- await client.syncRegistry(getDefaultRegistry(), { owner: "svc-b" });
2005
+ await client.syncRegistry(getDefaultRegistry(), { app: "svc-b" });
1896
2006
 
1897
- await redis.hset(`${prefix}:workflow:owner-a-wf`, { updatedAt: "1" });
1898
- await redis.hset(`${prefix}:workflow:owner-b-wf`, { updatedAt: "1" });
2007
+ await redis.hset(`${prefix}:workflow:app-a-wf`, { updatedAt: "1" });
2008
+ await redis.hset(`${prefix}:workflow:app-b-wf`, { updatedAt: "1" });
1899
2009
 
1900
2010
  __unstableResetDefaultRegistryForTests();
1901
- await client.syncRegistry(getDefaultRegistry(), { owner: "svc-a" });
2011
+ await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
1902
2012
 
1903
- expect(await redis.hget(`${prefix}:workflow:owner-a-wf`, "queue")).toBeNull();
1904
- expect(await redis.hget(`${prefix}:workflow:owner-b-wf`, "queue")).not.toBeNull();
1905
- expect((await redis.smembers(`${prefix}:workflows`)).includes("owner-a-wf")).toBe(false);
1906
- expect((await redis.smembers(`${prefix}:workflows`)).includes("owner-b-wf")).toBe(true);
2013
+ expect(await redis.hget(`${prefix}:workflow:app-a-wf`, "queue")).toBeNull();
2014
+ expect(await redis.hget(`${prefix}:workflow:app-b-wf`, "queue")).not.toBeNull();
2015
+ expect((await redis.smembers(`${prefix}:workflows`)).includes("app-a-wf")).toBe(false);
2016
+ expect((await redis.smembers(`${prefix}:workflows`)).includes("app-b-wf")).toBe(true);
1907
2017
 
1908
2018
  redis.close();
1909
2019
  });
@@ -1916,7 +2026,7 @@ test("cancelRun: terminal runs are unchanged", async () => {
1916
2026
 
1917
2027
  const wf = defineWorkflow("cancel-terminal", {queue: "q_cancel_terminal" }, async () => ({ ok: true }));
1918
2028
 
1919
- const worker = await startWorker({
2029
+ const worker = await startWorker({ app: "test-app",
1920
2030
  redis,
1921
2031
  prefix,
1922
2032
  queues: ["q_cancel_terminal"],
@@ -1951,7 +2061,7 @@ test("output serialization: non-JSON output fails once and is not stuck", async
1951
2061
  async () => ({ bad: 1n }),
1952
2062
  );
1953
2063
 
1954
- const worker = await startWorker({
2064
+ const worker = await startWorker({ app: "test-app",
1955
2065
  redis,
1956
2066
  prefix,
1957
2067
  queues: ["q_serial"],
@@ -2006,7 +2116,7 @@ test("retry step sequence: new steps keep monotonic order after cached steps", a
2006
2116
  },
2007
2117
  );
2008
2118
 
2009
- const worker = await startWorker({
2119
+ const worker = await startWorker({ app: "test-app",
2010
2120
  redis,
2011
2121
  prefix,
2012
2122
  queues: ["q_seq"],
@@ -2037,7 +2147,7 @@ test("listRuns: negative offset is clamped to zero", async () => {
2037
2147
  setDefaultClient(client);
2038
2148
 
2039
2149
  const wf = defineWorkflow("offset-wf", {queue: "q_offset" }, async () => ({ ok: true }));
2040
- const worker = await startWorker({
2150
+ const worker = await startWorker({ app: "test-app",
2041
2151
  redis,
2042
2152
  prefix,
2043
2153
  queues: ["q_offset"],
@@ -2101,7 +2211,7 @@ test("error serialization: non-serializable thrown values do not wedge a run", a
2101
2211
  },
2102
2212
  );
2103
2213
 
2104
- const worker = await startWorker({
2214
+ const worker = await startWorker({ app: "test-app",
2105
2215
  redis,
2106
2216
  prefix,
2107
2217
  queues: ["q_bigint"],
@@ -2144,7 +2254,7 @@ test("cron trigger ids: delimiter-like custom ids do not collide", async () => {
2144
2254
  async () => ({ ok: true }),
2145
2255
  );
2146
2256
 
2147
- await client.syncRegistry(getDefaultRegistry());
2257
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
2148
2258
 
2149
2259
  const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a`, "cronIdsJson")) ?? "[]") as string[];
2150
2260
  const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a:b`, "cronIdsJson")) ?? "[]") as string[];
@@ -2178,7 +2288,7 @@ test("idempotencyKey: stale pointer is recovered instead of returning missing ru
2178
2288
 
2179
2289
  const wf = defineWorkflow(workflowName, {queue }, async () => ({ ok: true }));
2180
2290
 
2181
- const worker = await startWorker({
2291
+ const worker = await startWorker({ app: "test-app",
2182
2292
  redis,
2183
2293
  prefix,
2184
2294
  queues: [queue],
@@ -2221,7 +2331,7 @@ test("idempotencyKey: partial existing run is repaired and executed", async () =
2221
2331
  return { ok: true };
2222
2332
  });
2223
2333
 
2224
- const worker = await startWorker({
2334
+ const worker = await startWorker({ app: "test-app",
2225
2335
  redis,
2226
2336
  prefix,
2227
2337
  queues: [queue],
@@ -2273,7 +2383,7 @@ test("syncRegistry: duplicate custom cron ids in one workflow are rejected", asy
2273
2383
  async () => ({ ok: true }),
2274
2384
  );
2275
2385
 
2276
- await expect(client.syncRegistry(getDefaultRegistry())).rejects.toThrow("Duplicate cron trigger id");
2386
+ await expect(client.syncRegistry(getDefaultRegistry(), { app: "test-app" })).rejects.toThrow("Duplicate cron trigger id");
2277
2387
 
2278
2388
  redis.close();
2279
2389
  });
@@ -2290,7 +2400,7 @@ test("scheduled promoter: stale scheduled entry does not resurrect terminal run"
2290
2400
  return { ok: true };
2291
2401
  });
2292
2402
 
2293
- const worker = await startWorker({
2403
+ const worker = await startWorker({ app: "test-app",
2294
2404
  redis,
2295
2405
  prefix,
2296
2406
  queues: [queue],
@@ -2357,7 +2467,7 @@ test(
2357
2467
 
2358
2468
  let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
2359
2469
  try {
2360
- worker = await startWorker({
2470
+ worker = await startWorker({ app: "test-app",
2361
2471
  redis,
2362
2472
  prefix,
2363
2473
  queues: [queue],
@@ -2408,7 +2518,7 @@ test(
2408
2518
 
2409
2519
  let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
2410
2520
  try {
2411
- worker = await startWorker({
2521
+ worker = await startWorker({ app: "test-app",
2412
2522
  redis,
2413
2523
  prefix,
2414
2524
  queues: [queue],
@@ -2451,7 +2561,7 @@ test("workflow names ending with ':runs' do not collide with workflow run indexe
2451
2561
 
2452
2562
  expect(keys.workflow(prefix, "keyspace-base:runs")).not.toBe(keys.workflowRuns(prefix, "keyspace-base"));
2453
2563
 
2454
- const worker = await startWorker({
2564
+ const worker = await startWorker({ app: "test-app",
2455
2565
  redis,
2456
2566
  prefix,
2457
2567
  queues: [queue],
@@ -2496,7 +2606,7 @@ test("cancelRun race: near-finish cancellation settles as canceled", async () =>
2496
2606
  return await originalFinalizeRun.call(this, runId, args);
2497
2607
  }) as RedflowClient["finalizeRun"];
2498
2608
 
2499
- const worker = await startWorker({
2609
+ const worker = await startWorker({ app: "test-app",
2500
2610
  redis,
2501
2611
  prefix,
2502
2612
  queues: [queue],
@@ -2559,7 +2669,7 @@ test("cancelRun race: cancellation requested during finalize wins over success",
2559
2669
  return await originalFinalizeRun.call(this, runId, args);
2560
2670
  }) as RedflowClient["finalizeRun"];
2561
2671
 
2562
- const worker = await startWorker({
2672
+ const worker = await startWorker({ app: "test-app",
2563
2673
  redis,
2564
2674
  prefix,
2565
2675
  queues: [queue],
@@ -2609,7 +2719,7 @@ test(
2609
2719
  await new Promise<never>(() => {});
2610
2720
  });
2611
2721
 
2612
- const worker = await startWorker({
2722
+ const worker = await startWorker({ app: "test-app",
2613
2723
  redis,
2614
2724
  prefix,
2615
2725
  queues: [queue],