@radishbot/sdk 0.1.0 → 0.2.1
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 +96 -57
- package/package.json +1 -1
- package/src/cli.ts +0 -0
- package/src/index.ts +79 -62
- package/src/module_bindings/api_key_table.ts +1 -0
- package/src/module_bindings/create_root_flow_reducer.ts +1 -0
- package/src/module_bindings/error_group_table.ts +25 -0
- package/src/module_bindings/finish_flow_reducer.ts +1 -0
- package/src/module_bindings/flow_table.ts +1 -0
- package/src/module_bindings/index.ts +18 -0
- package/src/module_bindings/register_key_reducer.ts +1 -0
- package/src/module_bindings/types/reducers.ts +2 -0
- package/src/module_bindings/types.ts +23 -0
- package/src/module_bindings/update_error_group_status_reducer.ts +17 -0
package/README.md
CHANGED
|
@@ -13,20 +13,20 @@ npm install @radishbot/sdk
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import { RL, generateKey } from
|
|
16
|
+
import { RL, generateKey } from "@radishbot/sdk";
|
|
17
17
|
|
|
18
18
|
const key = generateKey(); // save this — it's your dashboard login
|
|
19
19
|
|
|
20
|
-
const root = await RL(key);
|
|
20
|
+
const root = await RL(key, { release: "v1.0.0", retention: "30d" });
|
|
21
21
|
|
|
22
|
-
await root.a(
|
|
23
|
-
console.log(
|
|
22
|
+
await root.a("handle-request", async (req) => {
|
|
23
|
+
console.log("GET /api/users"); // automatically captured
|
|
24
24
|
|
|
25
|
-
const users = await req.a(
|
|
26
|
-
return await db.query(
|
|
25
|
+
const users = await req.a("db-query", async () => {
|
|
26
|
+
return await db.query("SELECT * FROM users");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
console.log(
|
|
29
|
+
console.log("Done", { count: users.length });
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
await root.finish();
|
|
@@ -39,12 +39,12 @@ Open the dashboard, paste your key, see everything.
|
|
|
39
39
|
Inside `.a()` callbacks, `console.log/warn/error/debug` are automatically captured as action logs. Output still prints to the terminal — but it also gets sent to the dashboard with full context.
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
await root.a(
|
|
43
|
-
console.log(
|
|
44
|
-
console.warn(
|
|
45
|
-
console.error(
|
|
46
|
-
console.debug(
|
|
47
|
-
console.log(
|
|
42
|
+
await root.a("migrate", async () => {
|
|
43
|
+
console.log("Starting migration"); // → info log
|
|
44
|
+
console.warn("Deprecated column found"); // → warn log
|
|
45
|
+
console.error("Failed to migrate users"); // → error log
|
|
46
|
+
console.debug("SQL: ALTER TABLE ..."); // → debug log
|
|
47
|
+
console.log("Done", { tables: 5 }); // → info log with data
|
|
48
48
|
});
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -57,15 +57,15 @@ Actions are nested scopes. Every `RL()` call creates a root action at `/`. Sub-a
|
|
|
57
57
|
```ts
|
|
58
58
|
const root = await RL(key);
|
|
59
59
|
|
|
60
|
-
await root.a(
|
|
61
|
-
console.log(
|
|
60
|
+
await root.a("request", async (req) => {
|
|
61
|
+
console.log("handling request");
|
|
62
62
|
|
|
63
|
-
await req.a(
|
|
64
|
-
console.log(
|
|
63
|
+
await req.a("database", async () => {
|
|
64
|
+
console.log("querying users");
|
|
65
65
|
}); // auto-finished
|
|
66
66
|
|
|
67
|
-
await req.a(
|
|
68
|
-
console.log(
|
|
67
|
+
await req.a("response", async () => {
|
|
68
|
+
console.log("sending 200");
|
|
69
69
|
}); // auto-finished
|
|
70
70
|
}); // auto-finished
|
|
71
71
|
|
|
@@ -75,16 +75,16 @@ await root.finish();
|
|
|
75
75
|
If your function throws, the action is marked as errored and the exception propagates:
|
|
76
76
|
|
|
77
77
|
```ts
|
|
78
|
-
await root.a(
|
|
79
|
-
throw new Error(
|
|
78
|
+
await root.a("risky-op", async () => {
|
|
79
|
+
throw new Error("something broke");
|
|
80
80
|
}); // action → error, rethrows
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Return values pass through:
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
|
-
const users = await flow.a(
|
|
87
|
-
return await db.query(
|
|
86
|
+
const users = await flow.a("db-query", async () => {
|
|
87
|
+
return await db.query("SELECT * FROM users");
|
|
88
88
|
});
|
|
89
89
|
// users is the query result
|
|
90
90
|
```
|
|
@@ -92,9 +92,9 @@ const users = await flow.a('db-query', async () => {
|
|
|
92
92
|
Duplicate names are allowed — the dashboard shows them as `request:1`, `request:2`, etc:
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
|
-
await root.a(
|
|
95
|
+
await root.a("batch", async (batch) => {
|
|
96
96
|
for (const item of items) {
|
|
97
|
-
await batch.a(
|
|
97
|
+
await batch.a("request", async () => {
|
|
98
98
|
await processItem(item);
|
|
99
99
|
});
|
|
100
100
|
}
|
|
@@ -104,10 +104,10 @@ await root.a('batch', async (batch) => {
|
|
|
104
104
|
For long-lived actions (websockets, streams), use the manual API:
|
|
105
105
|
|
|
106
106
|
```ts
|
|
107
|
-
const stream = root.action(
|
|
108
|
-
stream.info(
|
|
107
|
+
const stream = root.action("websocket");
|
|
108
|
+
stream.info("connected");
|
|
109
109
|
// ... hours later ...
|
|
110
|
-
stream.info(
|
|
110
|
+
stream.info("disconnected");
|
|
111
111
|
await stream.finish();
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -116,10 +116,10 @@ await stream.finish();
|
|
|
116
116
|
Outside of `.a()` callbacks, or when you want to log to a specific action:
|
|
117
117
|
|
|
118
118
|
```ts
|
|
119
|
-
action.info(
|
|
120
|
-
action.warn(
|
|
121
|
-
action.error(
|
|
122
|
-
action.debug(
|
|
119
|
+
action.info("request received", { method: "POST", path: "/api/users" });
|
|
120
|
+
action.warn("rate limit approaching", { remaining: 3 });
|
|
121
|
+
action.error("query failed", new Error("connection refused"));
|
|
122
|
+
action.debug("cache hit", { key: "user:123", ttl: 300 });
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
## Cross-Context Actions
|
|
@@ -128,35 +128,74 @@ Export an action's handle to continue logging from another process, worker, or s
|
|
|
128
128
|
|
|
129
129
|
```ts
|
|
130
130
|
// Service A
|
|
131
|
-
const action = root.action(
|
|
131
|
+
const action = root.action("job");
|
|
132
132
|
const handle = await action.exportID();
|
|
133
133
|
// pass handle to service B via queue, HTTP, etc.
|
|
134
134
|
|
|
135
135
|
// Service B
|
|
136
|
-
import { restoreFlow } from
|
|
136
|
+
import { restoreFlow } from "@radishbot/sdk";
|
|
137
137
|
const action = await restoreFlow(key, handle);
|
|
138
|
-
action.info(
|
|
138
|
+
action.info("continuing from service B");
|
|
139
139
|
await action.finish();
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
## Release Tracking
|
|
143
|
+
|
|
144
|
+
Tag every flow with a version or commit SHA. Errors are tracked per-release, and regressions (errors reappearing after being resolved) are detected automatically.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const root = await RL(key, { release: "v1.2.3" });
|
|
148
|
+
// or
|
|
149
|
+
const root = await RL(key, { release: process.env.GIT_SHA });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Sub-flows inherit the release from the root automatically.
|
|
153
|
+
|
|
154
|
+
## Retention & Garbage Collection
|
|
155
|
+
|
|
156
|
+
Each key has a retention period. Flows older than the retention window are automatically deleted (along with their logs and actions) by a GC job that runs every 3 hours.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const root = await RL(key, { retention: "30d" }); // default
|
|
160
|
+
const root = await RL(key, { retention: "7d" }); // keep 1 week
|
|
161
|
+
const root = await RL(key, { retention: "90d" }); // keep 3 months
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Calling `RL()` with a new retention value updates the stored retention for that key.
|
|
165
|
+
|
|
166
|
+
## Error Grouping
|
|
167
|
+
|
|
168
|
+
Errors are automatically deduplicated by normalizing the error message (stripping numbers, UUIDs) and combining it with the flow path. This means:
|
|
169
|
+
|
|
170
|
+
- `"User 123 not found"` and `"User 456 not found"` at `/request/db-query` group together
|
|
171
|
+
- Each group tracks: count, first/last seen, latest flow, and release
|
|
172
|
+
- Resolving an error group and seeing it again marks it as **regressed**
|
|
173
|
+
- Groups can be marked as `resolved` or `ignored` from the dashboard
|
|
174
|
+
|
|
142
175
|
## Configuration
|
|
143
176
|
|
|
144
177
|
```ts
|
|
145
178
|
const root = await RL(key, {
|
|
146
|
-
host:
|
|
147
|
-
dbName:
|
|
148
|
-
defaultTimeout: 100,
|
|
179
|
+
host: "wss://maincloud.spacetimedb.com", // default
|
|
180
|
+
dbName: "radish-log", // default
|
|
181
|
+
defaultTimeout: 100, // seconds, default 100
|
|
182
|
+
release: "v1.0.0", // version or commit SHA
|
|
183
|
+
retention: "30d", // data retention period
|
|
149
184
|
});
|
|
150
185
|
```
|
|
151
186
|
|
|
152
187
|
Sub-actions can have their own timeout:
|
|
153
188
|
|
|
154
189
|
```ts
|
|
155
|
-
await root.a(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
await root.a(
|
|
191
|
+
"quick-task",
|
|
192
|
+
async () => {
|
|
193
|
+
// ...
|
|
194
|
+
},
|
|
195
|
+
10,
|
|
196
|
+
); // 10 second timeout
|
|
197
|
+
|
|
198
|
+
const slow = root.action("batch-job", 3600); // 1 hour
|
|
160
199
|
```
|
|
161
200
|
|
|
162
201
|
Actions that exceed their timeout are automatically marked as timed out.
|
|
@@ -165,7 +204,7 @@ Actions that exceed their timeout are automatically marked as timed out.
|
|
|
165
204
|
|
|
166
205
|
### `RL(secretKey, options?) → Promise<Flow>`
|
|
167
206
|
|
|
168
|
-
Connect and create a root action.
|
|
207
|
+
Connect and create a root action. Options: `host`, `dbName`, `defaultTimeout`, `release`, `retention`.
|
|
169
208
|
|
|
170
209
|
### `generateKey() → string`
|
|
171
210
|
|
|
@@ -177,16 +216,16 @@ Restore an action from an exported handle string.
|
|
|
177
216
|
|
|
178
217
|
### Flow (Action)
|
|
179
218
|
|
|
180
|
-
| Method
|
|
181
|
-
|
|
182
|
-
| `.a(name, fn, timeout?)`
|
|
183
|
-
| `.action(name, timeout?)`
|
|
184
|
-
| `.finish()`
|
|
185
|
-
| `.finishWithError(err?)`
|
|
186
|
-
| `.info(msg, data?)`
|
|
187
|
-
| `.warn(msg, data?)`
|
|
188
|
-
| `.error(msg, data?)`
|
|
189
|
-
| `.debug(msg, data?)`
|
|
190
|
-
| `.log(msg, data?, level?)` | Log at any level.
|
|
191
|
-
| `.exportID()`
|
|
192
|
-
| `.getId()`
|
|
219
|
+
| Method | Description |
|
|
220
|
+
| -------------------------- | ------------------------------------------------------------------ |
|
|
221
|
+
| `.a(name, fn, timeout?)` | Run a sub-action. Auto-finish, console capture. Returns fn result. |
|
|
222
|
+
| `.action(name, timeout?)` | Create a sub-action manually. |
|
|
223
|
+
| `.finish()` | Finish the action. |
|
|
224
|
+
| `.finishWithError(err?)` | Finish the action as errored. |
|
|
225
|
+
| `.info(msg, data?)` | Log at info level. |
|
|
226
|
+
| `.warn(msg, data?)` | Log at warn level. |
|
|
227
|
+
| `.error(msg, data?)` | Log at error level. |
|
|
228
|
+
| `.debug(msg, data?)` | Log at debug level. |
|
|
229
|
+
| `.log(msg, data?, level?)` | Log at any level. |
|
|
230
|
+
| `.exportID()` | Export handle for cross-context restore. |
|
|
231
|
+
| `.getId()` | Get server-assigned action ID. |
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SdkConnection } from
|
|
1
|
+
import { SdkConnection } from "./connection";
|
|
2
2
|
|
|
3
|
-
export type LogLevel =
|
|
3
|
+
export type LogLevel = "info" | "warn" | "error" | "debug";
|
|
4
4
|
|
|
5
5
|
// ── Console capture ──────────────────────────────────────────
|
|
6
6
|
|
|
@@ -15,26 +15,32 @@ const _origConsole = {
|
|
|
15
15
|
const _flowStack: Flow[] = [];
|
|
16
16
|
|
|
17
17
|
function _patchConsole() {
|
|
18
|
-
const capture =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
const capture =
|
|
19
|
+
(level: LogLevel) =>
|
|
20
|
+
(...args: unknown[]) => {
|
|
21
|
+
const flow = _flowStack[_flowStack.length - 1];
|
|
22
|
+
if (flow) {
|
|
23
|
+
const msg = args
|
|
24
|
+
.map((a) => {
|
|
25
|
+
if (typeof a === "string") return a;
|
|
26
|
+
if (a instanceof Error) return a.message;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(a);
|
|
29
|
+
} catch {
|
|
30
|
+
return String(a);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.join(" ");
|
|
34
|
+
const data = args.length <= 1 ? undefined : args.length === 2 ? args[1] : args.slice(1);
|
|
35
|
+
flow.log(msg, data, level);
|
|
36
|
+
}
|
|
37
|
+
_origConsole[level === "info" ? "log" : level](...args);
|
|
38
|
+
};
|
|
39
|
+
console.log = capture("info");
|
|
40
|
+
console.info = capture("info");
|
|
41
|
+
console.warn = capture("warn");
|
|
42
|
+
console.error = capture("error");
|
|
43
|
+
console.debug = capture("debug");
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
function _restoreConsole() {
|
|
@@ -46,7 +52,7 @@ function _restoreConsole() {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
function serialize(value: unknown): string {
|
|
49
|
-
if (value === undefined || value === null) return
|
|
55
|
+
if (value === undefined || value === null) return "{}";
|
|
50
56
|
if (value instanceof Error)
|
|
51
57
|
return JSON.stringify({
|
|
52
58
|
name: value.name,
|
|
@@ -54,9 +60,7 @@ function serialize(value: unknown): string {
|
|
|
54
60
|
stack: value.stack,
|
|
55
61
|
});
|
|
56
62
|
try {
|
|
57
|
-
return JSON.stringify(value, (_key, v) =>
|
|
58
|
-
typeof v === 'bigint' ? v.toString() : v
|
|
59
|
-
);
|
|
63
|
+
return JSON.stringify(value, (_key, v) => (typeof v === "bigint" ? v.toString() : v));
|
|
60
64
|
} catch {
|
|
61
65
|
return JSON.stringify({ value: String(value) });
|
|
62
66
|
}
|
|
@@ -65,17 +69,17 @@ function serialize(value: unknown): string {
|
|
|
65
69
|
async function hashKey(secretKey: string): Promise<string> {
|
|
66
70
|
const encoder = new TextEncoder();
|
|
67
71
|
const data = encoder.encode(secretKey);
|
|
68
|
-
const hashBuffer = await crypto.subtle.digest(
|
|
72
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
69
73
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
70
|
-
return hashArray.map((b) => b.toString(16).padStart(2,
|
|
74
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
function generateToken(): string {
|
|
74
78
|
const bytes = new Uint8Array(16);
|
|
75
79
|
crypto.getRandomValues(bytes);
|
|
76
80
|
return Array.from(bytes)
|
|
77
|
-
.map((b) => b.toString(16).padStart(2,
|
|
78
|
-
.join(
|
|
81
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
82
|
+
.join("");
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export class Flow {
|
|
@@ -92,6 +96,7 @@ export class Flow {
|
|
|
92
96
|
private _parentId: bigint;
|
|
93
97
|
private _name: string;
|
|
94
98
|
private _timeoutSeconds: bigint;
|
|
99
|
+
private _release: string;
|
|
95
100
|
|
|
96
101
|
/** @internal */
|
|
97
102
|
constructor(
|
|
@@ -99,13 +104,15 @@ export class Flow {
|
|
|
99
104
|
keyHash: string,
|
|
100
105
|
parentId: bigint,
|
|
101
106
|
name: string,
|
|
102
|
-
timeoutSeconds: number
|
|
107
|
+
timeoutSeconds: number,
|
|
108
|
+
release: string = "",
|
|
103
109
|
) {
|
|
104
110
|
this._sdk = sdk;
|
|
105
111
|
this._keyHash = keyHash;
|
|
106
112
|
this._parentId = parentId;
|
|
107
113
|
this._name = name;
|
|
108
|
-
this._timeoutSeconds =
|
|
114
|
+
this._timeoutSeconds = !timeoutSeconds || timeoutSeconds === Infinity ? 0n : BigInt(timeoutSeconds);
|
|
115
|
+
this._release = release;
|
|
109
116
|
this._exportToken = generateToken();
|
|
110
117
|
this._ready = new Promise<void>((resolve) => {
|
|
111
118
|
this._resolveReady = resolve;
|
|
@@ -121,8 +128,8 @@ export class Flow {
|
|
|
121
128
|
|
|
122
129
|
if (this._parentId === 0n) {
|
|
123
130
|
const id = await this._sdk.createFlowAndResolveId(
|
|
124
|
-
() => conn.reducers.createRootFlow({ keyHash, timeoutSeconds, exportToken }),
|
|
125
|
-
exportToken
|
|
131
|
+
() => conn.reducers.createRootFlow({ keyHash, timeoutSeconds, exportToken, release: this._release }),
|
|
132
|
+
exportToken,
|
|
126
133
|
);
|
|
127
134
|
this._id = id;
|
|
128
135
|
} else {
|
|
@@ -137,7 +144,7 @@ export class Flow {
|
|
|
137
144
|
timeoutSeconds,
|
|
138
145
|
exportToken,
|
|
139
146
|
}),
|
|
140
|
-
exportToken
|
|
147
|
+
exportToken,
|
|
141
148
|
);
|
|
142
149
|
this._id = id;
|
|
143
150
|
}
|
|
@@ -147,7 +154,7 @@ export class Flow {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
/** Log a message with optional data */
|
|
150
|
-
log(message: string, data?: unknown, level: LogLevel =
|
|
157
|
+
log(message: string, data?: unknown, level: LogLevel = "info"): this {
|
|
151
158
|
if (this._finished) {
|
|
152
159
|
console.warn(`[radish] Cannot log to finished flow`);
|
|
153
160
|
return this;
|
|
@@ -155,28 +162,28 @@ export class Flow {
|
|
|
155
162
|
this._pendingLogs.push({
|
|
156
163
|
level,
|
|
157
164
|
message,
|
|
158
|
-
data: data !== undefined ? serialize(data) :
|
|
165
|
+
data: data !== undefined ? serialize(data) : "{}",
|
|
159
166
|
});
|
|
160
167
|
this._scheduleFlush();
|
|
161
168
|
return this;
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
info(message: string, data?: unknown): this {
|
|
165
|
-
return this.log(message, data,
|
|
172
|
+
return this.log(message, data, "info");
|
|
166
173
|
}
|
|
167
174
|
warn(message: string, data?: unknown): this {
|
|
168
|
-
return this.log(message, data,
|
|
175
|
+
return this.log(message, data, "warn");
|
|
169
176
|
}
|
|
170
177
|
error(message: string, data?: unknown): this {
|
|
171
|
-
return this.log(message, data,
|
|
178
|
+
return this.log(message, data, "error");
|
|
172
179
|
}
|
|
173
180
|
debug(message: string, data?: unknown): this {
|
|
174
|
-
return this.log(message, data,
|
|
181
|
+
return this.log(message, data, "debug");
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
/** Create a sub-action. Returns immediately — creation runs in background. */
|
|
178
185
|
action(name: string, timeoutSeconds = 100): Flow {
|
|
179
|
-
const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds);
|
|
186
|
+
const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds, this._release);
|
|
180
187
|
this._ready.then(() => {
|
|
181
188
|
(child as any)._parentId = this._id!;
|
|
182
189
|
child._create();
|
|
@@ -215,14 +222,16 @@ export class Flow {
|
|
|
215
222
|
this._sdk.conn.reducers.finishFlow({
|
|
216
223
|
keyHash: this._keyHash,
|
|
217
224
|
flowId: this._id!,
|
|
218
|
-
status:
|
|
225
|
+
status: "finished",
|
|
226
|
+
errorMessage: "",
|
|
219
227
|
});
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
/** Finish this flow with error status */
|
|
223
231
|
async finishWithError(err?: Error | string): Promise<void> {
|
|
232
|
+
const errorMessage = err ? (typeof err === "string" ? err : err.message) : "";
|
|
224
233
|
if (err) {
|
|
225
|
-
this.error(
|
|
234
|
+
this.error(errorMessage, err);
|
|
226
235
|
}
|
|
227
236
|
await this._ready;
|
|
228
237
|
this._drain();
|
|
@@ -230,7 +239,8 @@ export class Flow {
|
|
|
230
239
|
this._sdk.conn.reducers.finishFlow({
|
|
231
240
|
keyHash: this._keyHash,
|
|
232
241
|
flowId: this._id!,
|
|
233
|
-
status:
|
|
242
|
+
status: "error",
|
|
243
|
+
errorMessage,
|
|
234
244
|
});
|
|
235
245
|
}
|
|
236
246
|
|
|
@@ -290,6 +300,14 @@ export interface RLOptions {
|
|
|
290
300
|
dbName?: string;
|
|
291
301
|
label?: string;
|
|
292
302
|
defaultTimeout?: number;
|
|
303
|
+
release?: string;
|
|
304
|
+
retention?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseRetention(retention: string): number {
|
|
308
|
+
const match = retention.match(/^(\d+)d$/);
|
|
309
|
+
if (!match) throw new Error('Invalid retention format. Use e.g. "30d"');
|
|
310
|
+
return parseInt(match[1], 10);
|
|
293
311
|
}
|
|
294
312
|
|
|
295
313
|
/**
|
|
@@ -315,20 +333,23 @@ export interface RLOptions {
|
|
|
315
333
|
*/
|
|
316
334
|
export async function RL(secretKey: string, options: RLOptions = {}): Promise<Flow> {
|
|
317
335
|
const {
|
|
318
|
-
host =
|
|
319
|
-
dbName =
|
|
320
|
-
label =
|
|
336
|
+
host = "wss://maincloud.spacetimedb.com",
|
|
337
|
+
dbName = "radish-log",
|
|
338
|
+
label = "",
|
|
321
339
|
defaultTimeout = 100,
|
|
340
|
+
release = "",
|
|
341
|
+
retention = "30d",
|
|
322
342
|
} = options;
|
|
323
343
|
|
|
344
|
+
const retentionDays = parseRetention(retention);
|
|
324
345
|
const keyHash = await hashKey(secretKey);
|
|
325
346
|
const sdk = new SdkConnection(host, dbName);
|
|
326
347
|
sdk.setKeyHash(keyHash);
|
|
327
348
|
await sdk.connect();
|
|
328
349
|
|
|
329
|
-
// Register key (idempotent —
|
|
350
|
+
// Register key (idempotent — updates retention if changed)
|
|
330
351
|
try {
|
|
331
|
-
sdk.conn.reducers.registerKey({ keyHash, label });
|
|
352
|
+
sdk.conn.reducers.registerKey({ keyHash, label, retentionDays: BigInt(retentionDays) });
|
|
332
353
|
} catch {
|
|
333
354
|
// Already registered
|
|
334
355
|
}
|
|
@@ -341,29 +362,25 @@ export async function RL(secretKey: string, options: RLOptions = {}): Promise<Fl
|
|
|
341
362
|
}
|
|
342
363
|
|
|
343
364
|
// Root flow — creation runs in background, logs queue until ready
|
|
344
|
-
const root = new Flow(sdk, keyHash, 0n,
|
|
365
|
+
const root = new Flow(sdk, keyHash, 0n, "/", 0, release);
|
|
345
366
|
root._create(); // fire-and-forget — resolves _ready when server assigns ID
|
|
346
367
|
return root;
|
|
347
368
|
}
|
|
348
369
|
|
|
349
370
|
/** Restore a flow from an exported ID string */
|
|
350
|
-
export async function restoreFlow(
|
|
351
|
-
|
|
352
|
-
exportedId: string,
|
|
353
|
-
options: RLOptions = {}
|
|
354
|
-
): Promise<Flow> {
|
|
355
|
-
const { host = 'wss://maincloud.spacetimedb.com', dbName = 'radish-log' } = options;
|
|
371
|
+
export async function restoreFlow(secretKey: string, exportedId: string, options: RLOptions = {}): Promise<Flow> {
|
|
372
|
+
const { host = "wss://maincloud.spacetimedb.com", dbName = "radish-log" } = options;
|
|
356
373
|
|
|
357
374
|
const parsed = JSON.parse(exportedId);
|
|
358
375
|
const keyHash = await hashKey(secretKey);
|
|
359
376
|
if (keyHash !== parsed.keyHash) {
|
|
360
|
-
throw new Error(
|
|
377
|
+
throw new Error("Secret key does not match the flow owner");
|
|
361
378
|
}
|
|
362
379
|
|
|
363
380
|
const sdk = new SdkConnection(host, dbName);
|
|
364
381
|
await sdk.connect();
|
|
365
382
|
|
|
366
|
-
const flow = new Flow(sdk, keyHash, 0n,
|
|
383
|
+
const flow = new Flow(sdk, keyHash, 0n, "restored", 100);
|
|
367
384
|
(flow as any)._id = BigInt(parsed.flowId);
|
|
368
385
|
(flow as any)._exportToken = parsed.exportToken;
|
|
369
386
|
(flow as any)._resolveReady();
|
|
@@ -375,9 +392,9 @@ export function generateKey(): string {
|
|
|
375
392
|
const bytes = new Uint8Array(32);
|
|
376
393
|
crypto.getRandomValues(bytes);
|
|
377
394
|
return (
|
|
378
|
-
|
|
395
|
+
"rl_" +
|
|
379
396
|
Array.from(bytes)
|
|
380
|
-
.map((b) => b.toString(16).padStart(2,
|
|
381
|
-
.join(
|
|
397
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
398
|
+
.join("")
|
|
382
399
|
);
|
|
383
400
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 __t.row({
|
|
14
|
+
id: __t.u64().primaryKey(),
|
|
15
|
+
keyHash: __t.string().name("key_hash"),
|
|
16
|
+
fingerprint: __t.string(),
|
|
17
|
+
message: __t.string(),
|
|
18
|
+
path: __t.string(),
|
|
19
|
+
release: __t.string(),
|
|
20
|
+
count: __t.u64(),
|
|
21
|
+
status: __t.string(),
|
|
22
|
+
firstSeenAt: __t.timestamp().name("first_seen_at"),
|
|
23
|
+
lastSeenAt: __t.u64().name("last_seen_at"),
|
|
24
|
+
lastFlowId: __t.u64().name("last_flow_id"),
|
|
25
|
+
});
|
|
@@ -17,6 +17,7 @@ export default __t.row({
|
|
|
17
17
|
name: __t.string(),
|
|
18
18
|
path: __t.string(),
|
|
19
19
|
status: __t.string(),
|
|
20
|
+
release: __t.string(),
|
|
20
21
|
timeoutSeconds: __t.u64().name("timeout_seconds"),
|
|
21
22
|
createdAt: __t.timestamp().name("created_at"),
|
|
22
23
|
finishedAt: __t.u64().name("finished_at"),
|
|
@@ -43,12 +43,14 @@ import FinishActionReducer from "./finish_action_reducer";
|
|
|
43
43
|
import FinishFlowReducer from "./finish_flow_reducer";
|
|
44
44
|
import RegisterKeyReducer from "./register_key_reducer";
|
|
45
45
|
import StartActionReducer from "./start_action_reducer";
|
|
46
|
+
import UpdateErrorGroupStatusReducer from "./update_error_group_status_reducer";
|
|
46
47
|
|
|
47
48
|
// Import all procedure arg schemas
|
|
48
49
|
|
|
49
50
|
// Import all table schema definitions
|
|
50
51
|
import ActionRow from "./action_table";
|
|
51
52
|
import ApiKeyRow from "./api_key_table";
|
|
53
|
+
import ErrorGroupRow from "./error_group_table";
|
|
52
54
|
import FlowRow from "./flow_table";
|
|
53
55
|
import LogEntryRow from "./log_entry_table";
|
|
54
56
|
|
|
@@ -78,6 +80,21 @@ const tablesSchema = __schema({
|
|
|
78
80
|
{ name: 'api_key_key_hash_key', constraint: 'unique', columns: ['keyHash'] },
|
|
79
81
|
],
|
|
80
82
|
}, ApiKeyRow),
|
|
83
|
+
errorGroup: __table({
|
|
84
|
+
name: 'error_group',
|
|
85
|
+
indexes: [
|
|
86
|
+
{ name: 'fingerprint', algorithm: 'btree', columns: [
|
|
87
|
+
'fingerprint',
|
|
88
|
+
] },
|
|
89
|
+
{ name: 'id', algorithm: 'btree', columns: [
|
|
90
|
+
'id',
|
|
91
|
+
] },
|
|
92
|
+
],
|
|
93
|
+
constraints: [
|
|
94
|
+
{ name: 'error_group_fingerprint_key', constraint: 'unique', columns: ['fingerprint'] },
|
|
95
|
+
{ name: 'error_group_id_key', constraint: 'unique', columns: ['id'] },
|
|
96
|
+
],
|
|
97
|
+
}, ErrorGroupRow),
|
|
81
98
|
flow: __table({
|
|
82
99
|
name: 'flow',
|
|
83
100
|
indexes: [
|
|
@@ -117,6 +134,7 @@ const reducersSchema = __reducers(
|
|
|
117
134
|
__reducerSchema("finish_flow", FinishFlowReducer),
|
|
118
135
|
__reducerSchema("register_key", RegisterKeyReducer),
|
|
119
136
|
__reducerSchema("start_action", StartActionReducer),
|
|
137
|
+
__reducerSchema("update_error_group_status", UpdateErrorGroupStatusReducer),
|
|
120
138
|
);
|
|
121
139
|
|
|
122
140
|
/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */
|
|
@@ -15,6 +15,7 @@ import FinishActionReducer from "../finish_action_reducer";
|
|
|
15
15
|
import FinishFlowReducer from "../finish_flow_reducer";
|
|
16
16
|
import RegisterKeyReducer from "../register_key_reducer";
|
|
17
17
|
import StartActionReducer from "../start_action_reducer";
|
|
18
|
+
import UpdateErrorGroupStatusReducer from "../update_error_group_status_reducer";
|
|
18
19
|
|
|
19
20
|
export type AddLogParams = __Infer<typeof AddLogReducer>;
|
|
20
21
|
export type AddLogsBatchParams = __Infer<typeof AddLogsBatchReducer>;
|
|
@@ -25,4 +26,5 @@ export type FinishActionParams = __Infer<typeof FinishActionReducer>;
|
|
|
25
26
|
export type FinishFlowParams = __Infer<typeof FinishFlowReducer>;
|
|
26
27
|
export type RegisterKeyParams = __Infer<typeof RegisterKeyReducer>;
|
|
27
28
|
export type StartActionParams = __Infer<typeof StartActionReducer>;
|
|
29
|
+
export type UpdateErrorGroupStatusParams = __Infer<typeof UpdateErrorGroupStatusReducer>;
|
|
28
30
|
|
|
@@ -23,10 +23,26 @@ export type Action = __Infer<typeof Action>;
|
|
|
23
23
|
export const ApiKey = __t.object("ApiKey", {
|
|
24
24
|
keyHash: __t.string(),
|
|
25
25
|
label: __t.string(),
|
|
26
|
+
retentionDays: __t.u64(),
|
|
26
27
|
createdAt: __t.timestamp(),
|
|
27
28
|
});
|
|
28
29
|
export type ApiKey = __Infer<typeof ApiKey>;
|
|
29
30
|
|
|
31
|
+
export const ErrorGroup = __t.object("ErrorGroup", {
|
|
32
|
+
id: __t.u64(),
|
|
33
|
+
keyHash: __t.string(),
|
|
34
|
+
fingerprint: __t.string(),
|
|
35
|
+
message: __t.string(),
|
|
36
|
+
path: __t.string(),
|
|
37
|
+
release: __t.string(),
|
|
38
|
+
count: __t.u64(),
|
|
39
|
+
status: __t.string(),
|
|
40
|
+
firstSeenAt: __t.timestamp(),
|
|
41
|
+
lastSeenAt: __t.u64(),
|
|
42
|
+
lastFlowId: __t.u64(),
|
|
43
|
+
});
|
|
44
|
+
export type ErrorGroup = __Infer<typeof ErrorGroup>;
|
|
45
|
+
|
|
30
46
|
export const Flow = __t.object("Flow", {
|
|
31
47
|
id: __t.u64(),
|
|
32
48
|
keyHash: __t.string(),
|
|
@@ -34,6 +50,7 @@ export const Flow = __t.object("Flow", {
|
|
|
34
50
|
name: __t.string(),
|
|
35
51
|
path: __t.string(),
|
|
36
52
|
status: __t.string(),
|
|
53
|
+
release: __t.string(),
|
|
37
54
|
timeoutSeconds: __t.u64(),
|
|
38
55
|
createdAt: __t.timestamp(),
|
|
39
56
|
finishedAt: __t.u64(),
|
|
@@ -41,6 +58,12 @@ export const Flow = __t.object("Flow", {
|
|
|
41
58
|
});
|
|
42
59
|
export type Flow = __Infer<typeof Flow>;
|
|
43
60
|
|
|
61
|
+
export const GcJob = __t.object("GcJob", {
|
|
62
|
+
scheduledId: __t.u64(),
|
|
63
|
+
scheduledAt: __t.scheduleAt(),
|
|
64
|
+
});
|
|
65
|
+
export type GcJob = __Infer<typeof GcJob>;
|
|
66
|
+
|
|
44
67
|
export const LogEntry = __t.object("LogEntry", {
|
|
45
68
|
id: __t.u64(),
|
|
46
69
|
flowId: __t.u64(),
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
errorGroupId: __t.u64(),
|
|
16
|
+
status: __t.string(),
|
|
17
|
+
};
|