@radishbot/sdk 0.4.0 → 0.6.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
@@ -139,6 +139,34 @@ action.info("continuing from service B");
139
139
  await action.finish();
140
140
  ```
141
141
 
142
+ ## Run ID
143
+
144
+ Every `RL()` call auto-generates a **run ID** — a short random string that groups flows across processes. Pass it to subprocesses via env vars, CLI args, or message queues so all flows from one logical execution link together.
145
+
146
+ ```ts
147
+ const root = await RL(key);
148
+
149
+ // Pass to subprocess
150
+ spawn("worker.ts", { env: { ...process.env, RADISH_RUN_ID: root.runId } });
151
+
152
+ // In the subprocess — same run ID groups them together
153
+ const worker = await RL(key, { runId: process.env.RADISH_RUN_ID });
154
+ worker.info("processing");
155
+ await worker.finishAndDisconnect();
156
+ ```
157
+
158
+ You can also generate a run ID upfront:
159
+
160
+ ```ts
161
+ import { generateRunId } from "@radishbot/sdk";
162
+ const runId = generateRunId();
163
+ // pass to multiple services
164
+ const a = await RL(key, { runId });
165
+ const b = await RL(key, { runId });
166
+ ```
167
+
168
+ In the dashboard, click a run ID badge to filter all flows from that run.
169
+
142
170
  ## Release Tracking
143
171
 
144
172
  Tag every flow with a version or commit SHA. Errors are tracked per-release, and regressions (errors reappearing after being resolved) are detected automatically.
@@ -181,6 +209,7 @@ const root = await RL(key, {
181
209
  defaultTimeout: 100, // seconds, default 100
182
210
  release: "v1.0.0", // version or commit SHA
183
211
  retention: "30d", // data retention period
212
+ runId: "abc123", // optional, auto-generated if omitted
184
213
  });
185
214
  ```
186
215
 
@@ -204,12 +233,16 @@ Actions that exceed their timeout are automatically marked as timed out.
204
233
 
205
234
  ### `RL(secretKey, options?) → Promise<Flow>`
206
235
 
207
- Connect and create a root action. Options: `host`, `dbName`, `defaultTimeout`, `release`, `retention`.
236
+ Connect and create a root action. Options: `host`, `dbName`, `defaultTimeout`, `release`, `retention`, `runId`.
208
237
 
209
238
  ### `generateKey() → string`
210
239
 
211
240
  Generate a random secret key (prefix `rl_`).
212
241
 
242
+ ### `generateRunId() → string`
243
+
244
+ Generate a random run ID. Useful when you want to create the ID before calling `RL()`.
245
+
213
246
  ### `restoreFlow(secretKey, handle, options?) → Promise<Flow>`
214
247
 
215
248
  Restore an action from an exported handle string.
@@ -228,4 +261,5 @@ Restore an action from an exported handle string.
228
261
  | `.debug(msg, data?)` | Log at debug level. |
229
262
  | `.log(msg, data?, level?)` | Log at any level. |
230
263
  | `.exportID()` | Export handle for cross-context restore. |
264
+ | `.runId` | The run ID (read-only). Pass to subprocesses. |
231
265
  | `.getId()` | Get server-assigned action ID. |
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type LogLevel = "info" | "warn" | "error" | "debug";
2
2
 
3
3
  export declare class Flow {
4
+ readonly runId: string;
4
5
  log(message: string, data?: unknown, level?: LogLevel): this;
5
6
  info(message: string, data?: unknown): this;
6
7
  warn(message: string, data?: unknown): this;
@@ -24,8 +25,10 @@ export interface RLOptions {
24
25
  defaultTimeout?: number;
25
26
  release?: string;
26
27
  retention?: string;
28
+ runId?: string;
27
29
  }
28
30
 
29
31
  export declare function RL(secretKey: string, options?: RLOptions): Promise<Flow>;
30
32
  export declare function restoreFlow(secretKey: string, exportedId: string, options?: RLOptions): Promise<Flow>;
31
33
  export declare function generateKey(): string;
34
+ export declare function generateRunId(): string;
package/dist/index.js CHANGED
@@ -34,159 +34,153 @@ var add_logs_batch_reducer_default = {
34
34
  entries: __t2.string()
35
35
  };
36
36
 
37
- // src/module_bindings/check_timeouts_reducer.ts
37
+ // src/module_bindings/create_root_flow_reducer.ts
38
38
  import {
39
39
  t as __t3
40
40
  } from "spacetimedb";
41
- var check_timeouts_reducer_default = {
42
- keyHash: __t3.string()
41
+ var create_root_flow_reducer_default = {
42
+ keyHash: __t3.string(),
43
+ runId: __t3.string(),
44
+ timeoutSeconds: __t3.u64(),
45
+ exportToken: __t3.string(),
46
+ release: __t3.string()
43
47
  };
44
48
 
45
- // src/module_bindings/create_root_flow_reducer.ts
49
+ // src/module_bindings/create_sub_flow_reducer.ts
46
50
  import {
47
51
  t as __t4
48
52
  } from "spacetimedb";
49
- var create_root_flow_reducer_default = {
53
+ var create_sub_flow_reducer_default = {
50
54
  keyHash: __t4.string(),
55
+ parentFlowId: __t4.u64(),
56
+ name: __t4.string(),
51
57
  timeoutSeconds: __t4.u64(),
52
- exportToken: __t4.string(),
53
- release: __t4.string()
58
+ exportToken: __t4.string()
54
59
  };
55
60
 
56
- // src/module_bindings/create_sub_flow_reducer.ts
61
+ // src/module_bindings/finish_action_reducer.ts
57
62
  import {
58
63
  t as __t5
59
64
  } from "spacetimedb";
60
- var create_sub_flow_reducer_default = {
65
+ var finish_action_reducer_default = {
61
66
  keyHash: __t5.string(),
62
- parentFlowId: __t5.u64(),
63
- name: __t5.string(),
64
- timeoutSeconds: __t5.u64(),
65
- exportToken: __t5.string()
67
+ actionId: __t5.u64(),
68
+ status: __t5.string()
66
69
  };
67
70
 
68
- // src/module_bindings/finish_action_reducer.ts
71
+ // src/module_bindings/finish_flow_reducer.ts
69
72
  import {
70
73
  t as __t6
71
74
  } from "spacetimedb";
72
- var finish_action_reducer_default = {
75
+ var finish_flow_reducer_default = {
73
76
  keyHash: __t6.string(),
74
- actionId: __t6.u64(),
75
- status: __t6.string()
77
+ flowId: __t6.u64(),
78
+ status: __t6.string(),
79
+ errorMessage: __t6.string()
76
80
  };
77
81
 
78
- // src/module_bindings/finish_flow_reducer.ts
82
+ // src/module_bindings/register_key_reducer.ts
79
83
  import {
80
84
  t as __t7
81
85
  } from "spacetimedb";
82
- var finish_flow_reducer_default = {
86
+ var register_key_reducer_default = {
83
87
  keyHash: __t7.string(),
84
- flowId: __t7.u64(),
85
- status: __t7.string(),
86
- errorMessage: __t7.string()
88
+ label: __t7.string(),
89
+ retentionDays: __t7.u64()
87
90
  };
88
91
 
89
- // src/module_bindings/register_key_reducer.ts
92
+ // src/module_bindings/start_action_reducer.ts
90
93
  import {
91
94
  t as __t8
92
95
  } from "spacetimedb";
93
- var register_key_reducer_default = {
96
+ var start_action_reducer_default = {
94
97
  keyHash: __t8.string(),
95
- label: __t8.string(),
96
- retentionDays: __t8.u64()
98
+ flowId: __t8.u64(),
99
+ name: __t8.string()
97
100
  };
98
101
 
99
- // src/module_bindings/start_action_reducer.ts
102
+ // src/module_bindings/update_error_group_status_reducer.ts
100
103
  import {
101
104
  t as __t9
102
105
  } from "spacetimedb";
103
- var start_action_reducer_default = {
106
+ var update_error_group_status_reducer_default = {
104
107
  keyHash: __t9.string(),
105
- flowId: __t9.u64(),
106
- name: __t9.string()
108
+ errorGroupId: __t9.u64(),
109
+ status: __t9.string()
107
110
  };
108
111
 
109
- // src/module_bindings/update_error_group_status_reducer.ts
112
+ // src/module_bindings/action_table.ts
110
113
  import {
111
114
  t as __t10
112
115
  } from "spacetimedb";
113
- var update_error_group_status_reducer_default = {
114
- keyHash: __t10.string(),
115
- errorGroupId: __t10.u64(),
116
- status: __t10.string()
117
- };
116
+ var action_table_default = __t10.row({
117
+ id: __t10.u64().primaryKey(),
118
+ flowId: __t10.u64().name("flow_id"),
119
+ name: __t10.string(),
120
+ status: __t10.string(),
121
+ createdAt: __t10.timestamp().name("created_at"),
122
+ finishedAt: __t10.u64().name("finished_at")
123
+ });
118
124
 
119
- // src/module_bindings/action_table.ts
125
+ // src/module_bindings/api_key_table.ts
120
126
  import {
121
127
  t as __t11
122
128
  } from "spacetimedb";
123
- var action_table_default = __t11.row({
124
- id: __t11.u64().primaryKey(),
125
- flowId: __t11.u64().name("flow_id"),
126
- name: __t11.string(),
127
- status: __t11.string(),
128
- createdAt: __t11.timestamp().name("created_at"),
129
- finishedAt: __t11.u64().name("finished_at")
129
+ var api_key_table_default = __t11.row({
130
+ keyHash: __t11.string().primaryKey().name("key_hash"),
131
+ label: __t11.string(),
132
+ retentionDays: __t11.u64().name("retention_days"),
133
+ createdAt: __t11.timestamp().name("created_at")
130
134
  });
131
135
 
132
- // src/module_bindings/api_key_table.ts
136
+ // src/module_bindings/error_group_table.ts
133
137
  import {
134
138
  t as __t12
135
139
  } from "spacetimedb";
136
- var api_key_table_default = __t12.row({
137
- keyHash: __t12.string().primaryKey().name("key_hash"),
138
- label: __t12.string(),
139
- retentionDays: __t12.u64().name("retention_days"),
140
- createdAt: __t12.timestamp().name("created_at")
140
+ var error_group_table_default = __t12.row({
141
+ id: __t12.u64().primaryKey(),
142
+ keyHash: __t12.string().name("key_hash"),
143
+ fingerprint: __t12.string(),
144
+ message: __t12.string(),
145
+ path: __t12.string(),
146
+ release: __t12.string(),
147
+ count: __t12.u64(),
148
+ status: __t12.string(),
149
+ firstSeenAt: __t12.timestamp().name("first_seen_at"),
150
+ lastSeenAt: __t12.u64().name("last_seen_at"),
151
+ lastFlowId: __t12.u64().name("last_flow_id")
141
152
  });
142
153
 
143
- // src/module_bindings/error_group_table.ts
154
+ // src/module_bindings/flow_table.ts
144
155
  import {
145
156
  t as __t13
146
157
  } from "spacetimedb";
147
- var error_group_table_default = __t13.row({
158
+ var flow_table_default = __t13.row({
148
159
  id: __t13.u64().primaryKey(),
149
160
  keyHash: __t13.string().name("key_hash"),
150
- fingerprint: __t13.string(),
151
- message: __t13.string(),
161
+ runId: __t13.string().name("run_id"),
162
+ parentFlowId: __t13.u64().name("parent_flow_id"),
163
+ name: __t13.string(),
152
164
  path: __t13.string(),
153
- release: __t13.string(),
154
- count: __t13.u64(),
155
165
  status: __t13.string(),
156
- firstSeenAt: __t13.timestamp().name("first_seen_at"),
157
- lastSeenAt: __t13.u64().name("last_seen_at"),
158
- lastFlowId: __t13.u64().name("last_flow_id")
166
+ release: __t13.string(),
167
+ timeoutSeconds: __t13.u64().name("timeout_seconds"),
168
+ createdAt: __t13.timestamp().name("created_at"),
169
+ finishedAt: __t13.u64().name("finished_at"),
170
+ exportToken: __t13.string().name("export_token")
159
171
  });
160
172
 
161
- // src/module_bindings/flow_table.ts
173
+ // src/module_bindings/log_entry_table.ts
162
174
  import {
163
175
  t as __t14
164
176
  } from "spacetimedb";
165
- var flow_table_default = __t14.row({
177
+ var log_entry_table_default = __t14.row({
166
178
  id: __t14.u64().primaryKey(),
167
- keyHash: __t14.string().name("key_hash"),
168
- parentFlowId: __t14.u64().name("parent_flow_id"),
169
- name: __t14.string(),
170
- path: __t14.string(),
171
- status: __t14.string(),
172
- release: __t14.string(),
173
- timeoutSeconds: __t14.u64().name("timeout_seconds"),
174
- createdAt: __t14.timestamp().name("created_at"),
175
- finishedAt: __t14.u64().name("finished_at"),
176
- exportToken: __t14.string().name("export_token")
177
- });
178
-
179
- // src/module_bindings/log_entry_table.ts
180
- import {
181
- t as __t15
182
- } from "spacetimedb";
183
- var log_entry_table_default = __t15.row({
184
- id: __t15.u64().primaryKey(),
185
- flowId: __t15.u64().name("flow_id"),
186
- level: __t15.string(),
187
- message: __t15.string(),
188
- data: __t15.string(),
189
- createdAt: __t15.timestamp().name("created_at")
179
+ flowId: __t14.u64().name("flow_id"),
180
+ level: __t14.string(),
181
+ message: __t14.string(),
182
+ data: __t14.string(),
183
+ createdAt: __t14.timestamp().name("created_at")
190
184
  });
191
185
 
192
186
  // src/module_bindings/index.ts
@@ -255,7 +249,7 @@ var tablesSchema = __schema({
255
249
  ]
256
250
  }, log_entry_table_default)
257
251
  });
258
- var reducersSchema = __reducers(__reducerSchema("add_log", add_log_reducer_default), __reducerSchema("add_logs_batch", add_logs_batch_reducer_default), __reducerSchema("check_timeouts", check_timeouts_reducer_default), __reducerSchema("create_root_flow", create_root_flow_reducer_default), __reducerSchema("create_sub_flow", create_sub_flow_reducer_default), __reducerSchema("finish_action", finish_action_reducer_default), __reducerSchema("finish_flow", finish_flow_reducer_default), __reducerSchema("register_key", register_key_reducer_default), __reducerSchema("start_action", start_action_reducer_default), __reducerSchema("update_error_group_status", update_error_group_status_reducer_default));
252
+ var reducersSchema = __reducers(__reducerSchema("add_log", add_log_reducer_default), __reducerSchema("add_logs_batch", add_logs_batch_reducer_default), __reducerSchema("create_root_flow", create_root_flow_reducer_default), __reducerSchema("create_sub_flow", create_sub_flow_reducer_default), __reducerSchema("finish_action", finish_action_reducer_default), __reducerSchema("finish_flow", finish_flow_reducer_default), __reducerSchema("register_key", register_key_reducer_default), __reducerSchema("start_action", start_action_reducer_default), __reducerSchema("update_error_group_status", update_error_group_status_reducer_default));
259
253
  var proceduresSchema = __procedures();
260
254
  var REMOTE_MODULE = {
261
255
  versionInfo: {
@@ -289,7 +283,6 @@ class SdkConnection {
289
283
  _dbName;
290
284
  _conn = null;
291
285
  _connectPromise = null;
292
- _timeoutInterval = null;
293
286
  _keyHash = null;
294
287
  _flowWaiters = new Map;
295
288
  constructor(host, dbName) {
@@ -309,12 +302,7 @@ class SdkConnection {
309
302
  this._conn = c;
310
303
  c.subscriptionBuilder().onApplied(() => {
311
304
  resolve(c);
312
- }).subscribeToAllTables();
313
- this._timeoutInterval = setInterval(() => {
314
- if (this._keyHash) {
315
- c.reducers.checkTimeouts({ keyHash: this._keyHash });
316
- }
317
- }, 30000);
305
+ }).subscribe([`SELECT * FROM flow WHERE key_hash = '${this._keyHash}'`]);
318
306
  }).onConnectError((_ctx, err) => {
319
307
  reject(new Error(`SpacetimeDB connection failed: ${err}`));
320
308
  }).build();
@@ -358,10 +346,6 @@ class SdkConnection {
358
346
  });
359
347
  }
360
348
  disconnect() {
361
- if (this._timeoutInterval) {
362
- clearInterval(this._timeoutInterval);
363
- this._timeoutInterval = null;
364
- }
365
349
  if (this._conn) {
366
350
  this._conn.disconnect();
367
351
  this._conn = null;
@@ -454,13 +438,15 @@ class Flow {
454
438
  _name;
455
439
  _timeoutSeconds;
456
440
  _release;
457
- constructor(sdk, keyHash, parentId, name, timeoutSeconds, release = "") {
441
+ _runId;
442
+ constructor(sdk, keyHash, parentId, name, timeoutSeconds, release = "", runId = "") {
458
443
  this._sdk = sdk;
459
444
  this._keyHash = keyHash;
460
445
  this._parentId = parentId;
461
446
  this._name = name;
462
447
  this._timeoutSeconds = !timeoutSeconds || timeoutSeconds === Infinity ? 0n : BigInt(timeoutSeconds);
463
448
  this._release = release;
449
+ this._runId = runId;
464
450
  this._exportToken = generateToken();
465
451
  this._ready = new Promise((resolve) => {
466
452
  this._resolveReady = resolve;
@@ -472,7 +458,13 @@ class Flow {
472
458
  const keyHash = this._keyHash;
473
459
  const timeoutSeconds = this._timeoutSeconds;
474
460
  if (this._parentId === 0n) {
475
- const id = await this._sdk.createFlowAndResolveId(() => conn.reducers.createRootFlow({ keyHash, timeoutSeconds, exportToken, release: this._release }), exportToken);
461
+ const id = await this._sdk.createFlowAndResolveId(() => conn.reducers.createRootFlow({
462
+ keyHash,
463
+ runId: this._runId,
464
+ timeoutSeconds,
465
+ exportToken,
466
+ release: this._release
467
+ }), exportToken);
476
468
  this._id = id;
477
469
  } else {
478
470
  const parentFlowId = this._parentId;
@@ -521,7 +513,7 @@ class Flow {
521
513
  return this.log(message, data, "debug");
522
514
  }
523
515
  action(name, timeoutSeconds = 100) {
524
- const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release);
516
+ const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release, this._runId);
525
517
  this._ready.then(() => {
526
518
  child._parentId = this._id;
527
519
  child._create();
@@ -574,12 +566,16 @@ class Flow {
574
566
  errorMessage
575
567
  });
576
568
  }
569
+ get runId() {
570
+ return this._runId;
571
+ }
577
572
  async exportID() {
578
573
  await this._ready;
579
574
  return JSON.stringify({
580
575
  flowId: this._id.toString(),
581
576
  exportToken: this._exportToken,
582
- keyHash: this._keyHash
577
+ keyHash: this._keyHash,
578
+ runId: this._runId
583
579
  });
584
580
  }
585
581
  async getId() {
@@ -639,7 +635,8 @@ async function RL(secretKey, options = {}) {
639
635
  label = "",
640
636
  defaultTimeout = 100,
641
637
  release = "",
642
- retention = "30d"
638
+ retention = "30d",
639
+ runId = generateRunId()
643
640
  } = options;
644
641
  const retentionDays = parseRetention(retention);
645
642
  const keyHash = await hashKey(secretKey);
@@ -653,10 +650,7 @@ async function RL(secretKey, options = {}) {
653
650
  try {
654
651
  sdk.conn.reducers.registerKey({ keyHash, label, retentionDays: BigInt(retentionDays) });
655
652
  } catch {}
656
- try {
657
- sdk.conn.reducers.checkTimeouts({ keyHash });
658
- } catch {}
659
- const root = new Flow(sdk, keyHash, 0n, "/", 0, release);
653
+ const root = new Flow(sdk, keyHash, 0n, "/", 0, release, runId);
660
654
  root._create();
661
655
  return root;
662
656
  }
@@ -669,7 +663,7 @@ async function restoreFlow(secretKey, exportedId, options = {}) {
669
663
  }
670
664
  const sdk = new SdkConnection(host, dbName);
671
665
  await sdk.connect();
672
- const flow = new Flow(sdk, keyHash, 0n, "restored", 100);
666
+ const flow = new Flow(sdk, keyHash, 0n, "restored", 100, "", parsed.runId || "");
673
667
  flow._id = BigInt(parsed.flowId);
674
668
  flow._exportToken = parsed.exportToken;
675
669
  flow._resolveReady();
@@ -680,8 +674,14 @@ function generateKey() {
680
674
  crypto.getRandomValues(bytes);
681
675
  return "rl_" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
682
676
  }
677
+ function generateRunId() {
678
+ const bytes = new Uint8Array(8);
679
+ crypto.getRandomValues(bytes);
680
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
681
+ }
683
682
  export {
684
683
  restoreFlow,
684
+ generateRunId,
685
685
  generateKey,
686
686
  RL,
687
687
  Flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radishbot/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/cli.ts CHANGED
@@ -144,24 +144,16 @@ function connect(
144
144
  .onConnect((c, _identity, _token) => {
145
145
  c.subscriptionBuilder()
146
146
  .onApplied(() => {
147
- const flows = () => {
148
- const result: any[] = [];
149
- for (const f of c.db.flow.iter()) if (f.keyHash === keyHash) result.push(f);
150
- return result;
151
- };
152
- const logs = () => {
153
- const result: any[] = [];
154
- for (const l of c.db.logEntry.iter()) result.push(l);
155
- return result;
156
- };
157
- const errorGroups = () => {
158
- const result: any[] = [];
159
- for (const g of c.db.errorGroup.iter()) if (g.keyHash === keyHash) result.push(g);
160
- return result;
161
- };
147
+ const flows = () => [...c.db.flow.iter()];
148
+ const logs = () => [...c.db.logEntry.iter()];
149
+ const errorGroups = () => [...c.db.errorGroup.iter()];
162
150
  resolve({ conn: c, flows, logs, errorGroups });
163
151
  })
164
- .subscribeToAllTables();
152
+ .subscribe([
153
+ `SELECT * FROM flow WHERE key_hash = '${keyHash}'`,
154
+ `SELECT * FROM log_entry WHERE flow_id IN (SELECT id FROM flow WHERE key_hash = '${keyHash}')`,
155
+ `SELECT * FROM error_group WHERE key_hash = '${keyHash}'`,
156
+ ]);
165
157
  })
166
158
  .onConnectError((_ctx, err) => reject(new Error(`Connection failed: ${err}`)))
167
159
  .build();
@@ -185,19 +177,27 @@ function cmdList(flows: any[]) {
185
177
  }
186
178
 
187
179
  const hasRelease = roots.some((f) => f.release && f.release !== "");
180
+ const hasRunId = roots.some((f) => f.runId && f.runId !== "");
188
181
 
189
182
  console.log(`${c.bold}Flows${c.reset} ${c.dim}(${roots.length} total)${c.reset}\n`);
190
- if (hasRelease) {
191
- console.log(
192
- `${c.dim} ${"ID".padEnd(8)} ${"STATUS".padEnd(10)} ${"RELEASE".padEnd(10)} ${"DURATION".padEnd(10)} CREATED${c.reset}`,
193
- );
194
- console.log(
195
- `${c.dim} ${"".repeat(8)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(24)}${c.reset}`,
196
- );
197
- } else {
198
- console.log(`${c.dim} ${"ID".padEnd(8)} ${"STATUS".padEnd(10)} ${"DURATION".padEnd(10)} CREATED${c.reset}`);
199
- console.log(`${c.dim} ${"─".repeat(8)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(24)}${c.reset}`);
200
- }
183
+ const cols = [
184
+ "ID".padEnd(8),
185
+ "STATUS".padEnd(10),
186
+ ...(hasRunId ? ["RUN".padEnd(18)] : []),
187
+ ...(hasRelease ? ["RELEASE".padEnd(10)] : []),
188
+ "DURATION".padEnd(10),
189
+ "CREATED",
190
+ ];
191
+ const divs = [
192
+ "─".repeat(8),
193
+ "─".repeat(10),
194
+ ...(hasRunId ? ["─".repeat(18)] : []),
195
+ ...(hasRelease ? ["─".repeat(10)] : []),
196
+ "─".repeat(10),
197
+ "─".repeat(24),
198
+ ];
199
+ console.log(`${c.dim} ${cols.join(" ")}${c.reset}`);
200
+ console.log(`${c.dim} ${divs.join(" ")}${c.reset}`);
201
201
 
202
202
  for (const f of roots.slice(0, 30)) {
203
203
  const id = f.id.toString().padEnd(8);
@@ -205,8 +205,11 @@ function cmdList(flows: any[]) {
205
205
  const status = padRight(f.status, 10);
206
206
  const dur = f.finishedAt !== 0n ? padRight(fmtDuration(f.createdAt, f.finishedAt), 10) : padRight("—", 10);
207
207
  const time = fmtDateTime(f.createdAt);
208
+ const runIdCol = hasRunId ? padRight(f.runId ? f.runId.slice(0, 16) : "—", 18) : "";
208
209
  const release = hasRelease ? padRight(f.release || "—", 10) + " " : "";
209
- console.log(` ${c.dim}${id}${c.reset} ${sc}${status}${c.reset} ${release}${dur} ${c.dim}${time}${c.reset}`);
210
+ console.log(
211
+ ` ${c.dim}${id}${c.reset} ${sc}${status}${c.reset} ${hasRunId ? runIdCol + " " : ""}${release}${dur} ${c.dim}${time}${c.reset}`,
212
+ );
210
213
  }
211
214
 
212
215
  if (roots.length > 30) {
@@ -225,6 +228,9 @@ function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
225
228
  const sc = statusColor(flow.status);
226
229
  const releaseTag = flow.release ? ` ${c.dim}(${flow.release})${c.reset}` : "";
227
230
  console.log(`${c.bold}Flow #${flow.id}${c.reset} ${sc}${flow.status}${c.reset}${releaseTag}`);
231
+ if (flow.runId) {
232
+ console.log(`${c.dim}Run: ${flow.runId}${c.reset}`);
233
+ }
228
234
  console.log(`${c.dim}Created: ${fmtDateTime(flow.createdAt)}${c.reset}`);
229
235
  if (flow.finishedAt !== 0n) {
230
236
  console.log(`${c.dim}Duration: ${fmtDuration(flow.createdAt, flow.finishedAt)}${c.reset}`);
@@ -346,12 +352,15 @@ async function cmdTail(keyHash: string) {
346
352
  .onApplied(() => {
347
353
  // Seed known flows and seen logs
348
354
  for (const f of c.db.flow.iter()) {
349
- if (f.keyHash === keyHash) flowNames.set(f.id, f.name === "/" ? "root" : f.name);
355
+ flowNames.set(f.id, f.name === "/" ? "root" : f.name);
350
356
  }
351
357
  for (const l of c.db.logEntry.iter()) seen.add(l.id);
352
358
  resolve(c);
353
359
  })
354
- .subscribeToAllTables();
360
+ .subscribe([
361
+ `SELECT * FROM flow WHERE key_hash = '${keyHash}'`,
362
+ `SELECT * FROM log_entry WHERE flow_id IN (SELECT id FROM flow WHERE key_hash = '${keyHash}')`,
363
+ ]);
355
364
  })
356
365
  .onConnectError((_ctx, err) => reject(new Error(`Connection failed: ${err}`)))
357
366
  .build();
@@ -359,9 +368,9 @@ async function cmdTail(keyHash: string) {
359
368
 
360
369
  // Poll for new logs
361
370
  setInterval(() => {
362
- // Update flow names
371
+ // Update flow names (subscription already filters by key)
363
372
  for (const f of conn.db.flow.iter()) {
364
- if (f.keyHash === keyHash && !flowNames.has(f.id)) {
373
+ if (!flowNames.has(f.id)) {
365
374
  flowNames.set(f.id, f.name === "/" ? "root" : f.name);
366
375
  }
367
376
  }
@@ -369,11 +378,8 @@ async function cmdTail(keyHash: string) {
369
378
  const newLogs: any[] = [];
370
379
  for (const l of conn.db.logEntry.iter()) {
371
380
  if (!seen.has(l.id) && l.level !== "flow" && l.level !== "action") {
372
- // Only show logs belonging to this key's flows
373
- if (flowNames.has(l.flowId)) {
374
- seen.add(l.id);
375
- newLogs.push(l);
376
- }
381
+ seen.add(l.id);
382
+ newLogs.push(l);
377
383
  }
378
384
  }
379
385
 
package/src/connection.ts CHANGED
@@ -2,14 +2,13 @@
2
2
  * SpacetimeDB connection wrapper for the Radish SDK.
3
3
  * Uses the generated bindings + SpacetimeDB WebSocket client.
4
4
  */
5
- import { DbConnection as StdbConnection } from './module_bindings';
5
+ import { DbConnection as StdbConnection } from "./module_bindings";
6
6
 
7
7
  export class SdkConnection {
8
8
  private _host: string;
9
9
  private _dbName: string;
10
10
  private _conn: StdbConnection | null = null;
11
11
  private _connectPromise: Promise<StdbConnection> | null = null;
12
- private _timeoutInterval: ReturnType<typeof setInterval> | null = null;
13
12
  private _keyHash: string | null = null;
14
13
 
15
14
  // Callbacks waiting for flow creation by exportToken
@@ -35,19 +34,12 @@ export class SdkConnection {
35
34
  .onConnect((c, _identity, _token) => {
36
35
  this._conn = c;
37
36
 
38
- // Subscribe to flow table to detect newly created flows
37
+ // Only subscribe to flows for this key (needed to resolve flow IDs)
39
38
  c.subscriptionBuilder()
40
39
  .onApplied(() => {
41
40
  resolve(c);
42
41
  })
43
- .subscribeToAllTables();
44
-
45
- // Periodic timeout check (every 30s)
46
- this._timeoutInterval = setInterval(() => {
47
- if (this._keyHash) {
48
- c.reducers.checkTimeouts({ keyHash: this._keyHash });
49
- }
50
- }, 30_000);
42
+ .subscribe([`SELECT * FROM flow WHERE key_hash = '${this._keyHash}'`]);
51
43
  })
52
44
  .onConnectError((_ctx, err) => {
53
45
  reject(new Error(`SpacetimeDB connection failed: ${err}`));
@@ -59,7 +51,7 @@ export class SdkConnection {
59
51
  }
60
52
 
61
53
  get conn(): StdbConnection {
62
- if (!this._conn) throw new Error('Not connected');
54
+ if (!this._conn) throw new Error("Not connected");
63
55
  return this._conn;
64
56
  }
65
57
 
@@ -67,10 +59,7 @@ export class SdkConnection {
67
59
  * Call a reducer and wait for the flow to appear in the subscription.
68
60
  * Returns the server-assigned flow ID.
69
61
  */
70
- async createFlowAndResolveId(
71
- reducerCall: () => void,
72
- exportToken: string
73
- ): Promise<bigint> {
62
+ async createFlowAndResolveId(reducerCall: () => void, exportToken: string): Promise<bigint> {
74
63
  const conn = this.conn;
75
64
 
76
65
  // Check if flow already exists (from a previous subscription update)
@@ -112,10 +101,6 @@ export class SdkConnection {
112
101
  }
113
102
 
114
103
  disconnect(): void {
115
- if (this._timeoutInterval) {
116
- clearInterval(this._timeoutInterval);
117
- this._timeoutInterval = null;
118
- }
119
104
  if (this._conn) {
120
105
  this._conn.disconnect();
121
106
  this._conn = null;
package/src/index.ts CHANGED
@@ -97,6 +97,7 @@ export class Flow {
97
97
  private _name: string;
98
98
  private _timeoutSeconds: bigint;
99
99
  private _release: string;
100
+ private _runId: string;
100
101
 
101
102
  /** @internal */
102
103
  constructor(
@@ -106,6 +107,7 @@ export class Flow {
106
107
  name: string,
107
108
  timeoutSeconds: number,
108
109
  release: string = "",
110
+ runId: string = "",
109
111
  ) {
110
112
  this._sdk = sdk;
111
113
  this._keyHash = keyHash;
@@ -113,6 +115,7 @@ export class Flow {
113
115
  this._name = name;
114
116
  this._timeoutSeconds = !timeoutSeconds || timeoutSeconds === Infinity ? 0n : BigInt(timeoutSeconds);
115
117
  this._release = release;
118
+ this._runId = runId;
116
119
  this._exportToken = generateToken();
117
120
  this._ready = new Promise<void>((resolve) => {
118
121
  this._resolveReady = resolve;
@@ -128,7 +131,14 @@ export class Flow {
128
131
 
129
132
  if (this._parentId === 0n) {
130
133
  const id = await this._sdk.createFlowAndResolveId(
131
- () => conn.reducers.createRootFlow({ keyHash, timeoutSeconds, exportToken, release: this._release }),
134
+ () =>
135
+ conn.reducers.createRootFlow({
136
+ keyHash,
137
+ runId: this._runId,
138
+ timeoutSeconds,
139
+ exportToken,
140
+ release: this._release,
141
+ }),
132
142
  exportToken,
133
143
  );
134
144
  this._id = id;
@@ -191,7 +201,7 @@ export class Flow {
191
201
 
192
202
  /** Create a sub-action. Returns immediately — creation runs in background. */
193
203
  action(name: string, timeoutSeconds = 100): Flow {
194
- const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release);
204
+ const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release, this._runId);
195
205
  this._ready.then(() => {
196
206
  (child as any)._parentId = this._id!;
197
207
  child._create();
@@ -252,6 +262,11 @@ export class Flow {
252
262
  });
253
263
  }
254
264
 
265
+ /** Get the run ID for this flow (pass to subprocesses via env, args, etc.) */
266
+ get runId(): string {
267
+ return this._runId;
268
+ }
269
+
255
270
  /** Export this flow's handle for restoration in another context */
256
271
  async exportID(): Promise<string> {
257
272
  await this._ready;
@@ -259,6 +274,7 @@ export class Flow {
259
274
  flowId: this._id!.toString(),
260
275
  exportToken: this._exportToken,
261
276
  keyHash: this._keyHash,
277
+ runId: this._runId,
262
278
  });
263
279
  }
264
280
 
@@ -323,6 +339,8 @@ export interface RLOptions {
323
339
  defaultTimeout?: number;
324
340
  release?: string;
325
341
  retention?: string;
342
+ /** Run ID — groups flows across processes. Auto-generated if not provided. */
343
+ runId?: string;
326
344
  }
327
345
 
328
346
  function parseRetention(retention: string): number {
@@ -360,6 +378,7 @@ export async function RL(secretKey: string, options: RLOptions = {}): Promise<Fl
360
378
  defaultTimeout = 100,
361
379
  release = "",
362
380
  retention = "30d",
381
+ runId = generateRunId(),
363
382
  } = options;
364
383
 
365
384
  const retentionDays = parseRetention(retention);
@@ -379,15 +398,8 @@ export async function RL(secretKey: string, options: RLOptions = {}): Promise<Fl
379
398
  // Already registered
380
399
  }
381
400
 
382
- // Check for timed-out flows (fire-and-forget)
383
- try {
384
- sdk.conn.reducers.checkTimeouts({ keyHash });
385
- } catch {
386
- // Non-critical
387
- }
388
-
389
401
  // Root flow — creation runs in background, logs queue until ready
390
- const root = new Flow(sdk, keyHash, 0n, "/", 0, release);
402
+ const root = new Flow(sdk, keyHash, 0n, "/", 0, release, runId);
391
403
  root._create(); // fire-and-forget — resolves _ready when server assigns ID
392
404
  return root;
393
405
  }
@@ -405,7 +417,7 @@ export async function restoreFlow(secretKey: string, exportedId: string, options
405
417
  const sdk = new SdkConnection(host, dbName);
406
418
  await sdk.connect();
407
419
 
408
- const flow = new Flow(sdk, keyHash, 0n, "restored", 100);
420
+ const flow = new Flow(sdk, keyHash, 0n, "restored", 100, "", parsed.runId || "");
409
421
  (flow as any)._id = BigInt(parsed.flowId);
410
422
  (flow as any)._exportToken = parsed.exportToken;
411
423
  (flow as any)._resolveReady();
@@ -423,3 +435,12 @@ export function generateKey(): string {
423
435
  .join("")
424
436
  );
425
437
  }
438
+
439
+ /** Generate a random run ID (pass to subprocesses to link flows) */
440
+ export function generateRunId(): string {
441
+ const bytes = new Uint8Array(8);
442
+ crypto.getRandomValues(bytes);
443
+ return Array.from(bytes)
444
+ .map((b) => b.toString(16).padStart(2, "0"))
445
+ .join("");
446
+ }
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  export default {
14
14
  keyHash: __t.string(),
15
+ runId: __t.string(),
15
16
  timeoutSeconds: __t.u64(),
16
17
  exportToken: __t.string(),
17
18
  release: __t.string(),
@@ -13,6 +13,7 @@ import {
13
13
  export default __t.row({
14
14
  id: __t.u64().primaryKey(),
15
15
  keyHash: __t.string().name("key_hash"),
16
+ runId: __t.string().name("run_id"),
16
17
  parentFlowId: __t.u64().name("parent_flow_id"),
17
18
  name: __t.string(),
18
19
  path: __t.string(),
@@ -36,7 +36,6 @@ import {
36
36
  // Import all reducer arg schemas
37
37
  import AddLogReducer from "./add_log_reducer";
38
38
  import AddLogsBatchReducer from "./add_logs_batch_reducer";
39
- import CheckTimeoutsReducer from "./check_timeouts_reducer";
40
39
  import CreateRootFlowReducer from "./create_root_flow_reducer";
41
40
  import CreateSubFlowReducer from "./create_sub_flow_reducer";
42
41
  import FinishActionReducer from "./finish_action_reducer";
@@ -127,7 +126,6 @@ const tablesSchema = __schema({
127
126
  const reducersSchema = __reducers(
128
127
  __reducerSchema("add_log", AddLogReducer),
129
128
  __reducerSchema("add_logs_batch", AddLogsBatchReducer),
130
- __reducerSchema("check_timeouts", CheckTimeoutsReducer),
131
129
  __reducerSchema("create_root_flow", CreateRootFlowReducer),
132
130
  __reducerSchema("create_sub_flow", CreateSubFlowReducer),
133
131
  __reducerSchema("finish_action", FinishActionReducer),
@@ -8,7 +8,6 @@ import { type Infer as __Infer } from "spacetimedb";
8
8
  // Import all reducer arg schemas
9
9
  import AddLogReducer from "../add_log_reducer";
10
10
  import AddLogsBatchReducer from "../add_logs_batch_reducer";
11
- import CheckTimeoutsReducer from "../check_timeouts_reducer";
12
11
  import CreateRootFlowReducer from "../create_root_flow_reducer";
13
12
  import CreateSubFlowReducer from "../create_sub_flow_reducer";
14
13
  import FinishActionReducer from "../finish_action_reducer";
@@ -19,7 +18,6 @@ import UpdateErrorGroupStatusReducer from "../update_error_group_status_reducer"
19
18
 
20
19
  export type AddLogParams = __Infer<typeof AddLogReducer>;
21
20
  export type AddLogsBatchParams = __Infer<typeof AddLogsBatchReducer>;
22
- export type CheckTimeoutsParams = __Infer<typeof CheckTimeoutsReducer>;
23
21
  export type CreateRootFlowParams = __Infer<typeof CreateRootFlowReducer>;
24
22
  export type CreateSubFlowParams = __Infer<typeof CreateSubFlowReducer>;
25
23
  export type FinishActionParams = __Infer<typeof FinishActionReducer>;
@@ -46,6 +46,7 @@ export type ErrorGroup = __Infer<typeof ErrorGroup>;
46
46
  export const Flow = __t.object("Flow", {
47
47
  id: __t.u64(),
48
48
  keyHash: __t.string(),
49
+ runId: __t.string(),
49
50
  parentFlowId: __t.u64(),
50
51
  name: __t.string(),
51
52
  path: __t.string(),
@@ -74,3 +75,9 @@ export const LogEntry = __t.object("LogEntry", {
74
75
  });
75
76
  export type LogEntry = __Infer<typeof LogEntry>;
76
77
 
78
+ export const TimeoutJob = __t.object("TimeoutJob", {
79
+ scheduledId: __t.u64(),
80
+ scheduledAt: __t.scheduleAt(),
81
+ });
82
+ export type TimeoutJob = __Infer<typeof TimeoutJob>;
83
+
@@ -1,15 +0,0 @@
1
- // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
2
- // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
3
-
4
- /* eslint-disable */
5
- /* tslint:disable */
6
- import {
7
- TypeBuilder as __TypeBuilder,
8
- t as __t,
9
- type AlgebraicTypeType as __AlgebraicTypeType,
10
- type Infer as __Infer,
11
- } from "spacetimedb";
12
-
13
- export default {
14
- keyHash: __t.string(),
15
- };