@radishbot/sdk 0.4.0 → 0.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
@@ -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
@@ -48,6 +48,7 @@ import {
48
48
  } from "spacetimedb";
49
49
  var create_root_flow_reducer_default = {
50
50
  keyHash: __t4.string(),
51
+ runId: __t4.string(),
51
52
  timeoutSeconds: __t4.u64(),
52
53
  exportToken: __t4.string(),
53
54
  release: __t4.string()
@@ -165,6 +166,7 @@ import {
165
166
  var flow_table_default = __t14.row({
166
167
  id: __t14.u64().primaryKey(),
167
168
  keyHash: __t14.string().name("key_hash"),
169
+ runId: __t14.string().name("run_id"),
168
170
  parentFlowId: __t14.u64().name("parent_flow_id"),
169
171
  name: __t14.string(),
170
172
  path: __t14.string(),
@@ -454,13 +456,15 @@ class Flow {
454
456
  _name;
455
457
  _timeoutSeconds;
456
458
  _release;
457
- constructor(sdk, keyHash, parentId, name, timeoutSeconds, release = "") {
459
+ _runId;
460
+ constructor(sdk, keyHash, parentId, name, timeoutSeconds, release = "", runId = "") {
458
461
  this._sdk = sdk;
459
462
  this._keyHash = keyHash;
460
463
  this._parentId = parentId;
461
464
  this._name = name;
462
465
  this._timeoutSeconds = !timeoutSeconds || timeoutSeconds === Infinity ? 0n : BigInt(timeoutSeconds);
463
466
  this._release = release;
467
+ this._runId = runId;
464
468
  this._exportToken = generateToken();
465
469
  this._ready = new Promise((resolve) => {
466
470
  this._resolveReady = resolve;
@@ -472,7 +476,13 @@ class Flow {
472
476
  const keyHash = this._keyHash;
473
477
  const timeoutSeconds = this._timeoutSeconds;
474
478
  if (this._parentId === 0n) {
475
- const id = await this._sdk.createFlowAndResolveId(() => conn.reducers.createRootFlow({ keyHash, timeoutSeconds, exportToken, release: this._release }), exportToken);
479
+ const id = await this._sdk.createFlowAndResolveId(() => conn.reducers.createRootFlow({
480
+ keyHash,
481
+ runId: this._runId,
482
+ timeoutSeconds,
483
+ exportToken,
484
+ release: this._release
485
+ }), exportToken);
476
486
  this._id = id;
477
487
  } else {
478
488
  const parentFlowId = this._parentId;
@@ -521,7 +531,7 @@ class Flow {
521
531
  return this.log(message, data, "debug");
522
532
  }
523
533
  action(name, timeoutSeconds = 100) {
524
- const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release);
534
+ const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release, this._runId);
525
535
  this._ready.then(() => {
526
536
  child._parentId = this._id;
527
537
  child._create();
@@ -574,12 +584,16 @@ class Flow {
574
584
  errorMessage
575
585
  });
576
586
  }
587
+ get runId() {
588
+ return this._runId;
589
+ }
577
590
  async exportID() {
578
591
  await this._ready;
579
592
  return JSON.stringify({
580
593
  flowId: this._id.toString(),
581
594
  exportToken: this._exportToken,
582
- keyHash: this._keyHash
595
+ keyHash: this._keyHash,
596
+ runId: this._runId
583
597
  });
584
598
  }
585
599
  async getId() {
@@ -639,7 +653,8 @@ async function RL(secretKey, options = {}) {
639
653
  label = "",
640
654
  defaultTimeout = 100,
641
655
  release = "",
642
- retention = "30d"
656
+ retention = "30d",
657
+ runId = generateRunId()
643
658
  } = options;
644
659
  const retentionDays = parseRetention(retention);
645
660
  const keyHash = await hashKey(secretKey);
@@ -656,7 +671,7 @@ async function RL(secretKey, options = {}) {
656
671
  try {
657
672
  sdk.conn.reducers.checkTimeouts({ keyHash });
658
673
  } catch {}
659
- const root = new Flow(sdk, keyHash, 0n, "/", 0, release);
674
+ const root = new Flow(sdk, keyHash, 0n, "/", 0, release, runId);
660
675
  root._create();
661
676
  return root;
662
677
  }
@@ -669,7 +684,7 @@ async function restoreFlow(secretKey, exportedId, options = {}) {
669
684
  }
670
685
  const sdk = new SdkConnection(host, dbName);
671
686
  await sdk.connect();
672
- const flow = new Flow(sdk, keyHash, 0n, "restored", 100);
687
+ const flow = new Flow(sdk, keyHash, 0n, "restored", 100, "", parsed.runId || "");
673
688
  flow._id = BigInt(parsed.flowId);
674
689
  flow._exportToken = parsed.exportToken;
675
690
  flow._resolveReady();
@@ -680,8 +695,14 @@ function generateKey() {
680
695
  crypto.getRandomValues(bytes);
681
696
  return "rl_" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
682
697
  }
698
+ function generateRunId() {
699
+ const bytes = new Uint8Array(8);
700
+ crypto.getRandomValues(bytes);
701
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
702
+ }
683
703
  export {
684
704
  restoreFlow,
705
+ generateRunId,
685
706
  generateKey,
686
707
  RL,
687
708
  Flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radishbot/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/cli.ts CHANGED
@@ -185,19 +185,27 @@ function cmdList(flows: any[]) {
185
185
  }
186
186
 
187
187
  const hasRelease = roots.some((f) => f.release && f.release !== "");
188
+ const hasRunId = roots.some((f) => f.runId && f.runId !== "");
188
189
 
189
190
  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
- }
191
+ const cols = [
192
+ "ID".padEnd(8),
193
+ "STATUS".padEnd(10),
194
+ ...(hasRunId ? ["RUN".padEnd(18)] : []),
195
+ ...(hasRelease ? ["RELEASE".padEnd(10)] : []),
196
+ "DURATION".padEnd(10),
197
+ "CREATED",
198
+ ];
199
+ const divs = [
200
+ "─".repeat(8),
201
+ "─".repeat(10),
202
+ ...(hasRunId ? ["─".repeat(18)] : []),
203
+ ...(hasRelease ? ["─".repeat(10)] : []),
204
+ "─".repeat(10),
205
+ "─".repeat(24),
206
+ ];
207
+ console.log(`${c.dim} ${cols.join(" ")}${c.reset}`);
208
+ console.log(`${c.dim} ${divs.join(" ")}${c.reset}`);
201
209
 
202
210
  for (const f of roots.slice(0, 30)) {
203
211
  const id = f.id.toString().padEnd(8);
@@ -205,8 +213,11 @@ function cmdList(flows: any[]) {
205
213
  const status = padRight(f.status, 10);
206
214
  const dur = f.finishedAt !== 0n ? padRight(fmtDuration(f.createdAt, f.finishedAt), 10) : padRight("—", 10);
207
215
  const time = fmtDateTime(f.createdAt);
216
+ const runIdCol = hasRunId ? padRight(f.runId ? f.runId.slice(0, 16) : "—", 18) : "";
208
217
  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}`);
218
+ console.log(
219
+ ` ${c.dim}${id}${c.reset} ${sc}${status}${c.reset} ${hasRunId ? runIdCol + " " : ""}${release}${dur} ${c.dim}${time}${c.reset}`,
220
+ );
210
221
  }
211
222
 
212
223
  if (roots.length > 30) {
@@ -225,6 +236,9 @@ function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
225
236
  const sc = statusColor(flow.status);
226
237
  const releaseTag = flow.release ? ` ${c.dim}(${flow.release})${c.reset}` : "";
227
238
  console.log(`${c.bold}Flow #${flow.id}${c.reset} ${sc}${flow.status}${c.reset}${releaseTag}`);
239
+ if (flow.runId) {
240
+ console.log(`${c.dim}Run: ${flow.runId}${c.reset}`);
241
+ }
228
242
  console.log(`${c.dim}Created: ${fmtDateTime(flow.createdAt)}${c.reset}`);
229
243
  if (flow.finishedAt !== 0n) {
230
244
  console.log(`${c.dim}Duration: ${fmtDuration(flow.createdAt, flow.finishedAt)}${c.reset}`);
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);
@@ -387,7 +406,7 @@ export async function RL(secretKey: string, options: RLOptions = {}): Promise<Fl
387
406
  }
388
407
 
389
408
  // Root flow — creation runs in background, logs queue until ready
390
- const root = new Flow(sdk, keyHash, 0n, "/", 0, release);
409
+ const root = new Flow(sdk, keyHash, 0n, "/", 0, release, runId);
391
410
  root._create(); // fire-and-forget — resolves _ready when server assigns ID
392
411
  return root;
393
412
  }
@@ -405,7 +424,7 @@ export async function restoreFlow(secretKey: string, exportedId: string, options
405
424
  const sdk = new SdkConnection(host, dbName);
406
425
  await sdk.connect();
407
426
 
408
- const flow = new Flow(sdk, keyHash, 0n, "restored", 100);
427
+ const flow = new Flow(sdk, keyHash, 0n, "restored", 100, "", parsed.runId || "");
409
428
  (flow as any)._id = BigInt(parsed.flowId);
410
429
  (flow as any)._exportToken = parsed.exportToken;
411
430
  (flow as any)._resolveReady();
@@ -423,3 +442,12 @@ export function generateKey(): string {
423
442
  .join("")
424
443
  );
425
444
  }
445
+
446
+ /** Generate a random run ID (pass to subprocesses to link flows) */
447
+ export function generateRunId(): string {
448
+ const bytes = new Uint8Array(8);
449
+ crypto.getRandomValues(bytes);
450
+ return Array.from(bytes)
451
+ .map((b) => b.toString(16).padStart(2, "0"))
452
+ .join("");
453
+ }
@@ -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(),
@@ -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(),