@radishbot/sdk 0.1.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 +192 -0
- package/package.json +24 -0
- package/src/cli.ts +383 -0
- package/src/connection.ts +124 -0
- package/src/index.ts +383 -0
- package/src/module_bindings/action_table.ts +20 -0
- package/src/module_bindings/add_log_reducer.ts +19 -0
- package/src/module_bindings/add_logs_batch_reducer.ts +17 -0
- package/src/module_bindings/api_key_table.ts +17 -0
- package/src/module_bindings/check_timeouts_reducer.ts +15 -0
- package/src/module_bindings/create_root_flow_reducer.ts +17 -0
- package/src/module_bindings/create_sub_flow_reducer.ts +19 -0
- package/src/module_bindings/finish_action_reducer.ts +17 -0
- package/src/module_bindings/finish_flow_reducer.ts +17 -0
- package/src/module_bindings/flow_table.ts +24 -0
- package/src/module_bindings/index.ts +175 -0
- package/src/module_bindings/log_entry_table.ts +20 -0
- package/src/module_bindings/register_key_reducer.ts +16 -0
- package/src/module_bindings/start_action_reducer.ts +17 -0
- package/src/module_bindings/types/procedures.ts +10 -0
- package/src/module_bindings/types/reducers.ts +28 -0
- package/src/module_bindings/types.ts +53 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpacetimeDB connection wrapper for the Radish SDK.
|
|
3
|
+
* Uses the generated bindings + SpacetimeDB WebSocket client.
|
|
4
|
+
*/
|
|
5
|
+
import { DbConnection as StdbConnection } from './module_bindings';
|
|
6
|
+
|
|
7
|
+
export class SdkConnection {
|
|
8
|
+
private _host: string;
|
|
9
|
+
private _dbName: string;
|
|
10
|
+
private _conn: StdbConnection | null = null;
|
|
11
|
+
private _connectPromise: Promise<StdbConnection> | null = null;
|
|
12
|
+
private _timeoutInterval: ReturnType<typeof setInterval> | null = null;
|
|
13
|
+
private _keyHash: string | null = null;
|
|
14
|
+
|
|
15
|
+
// Callbacks waiting for flow creation by exportToken
|
|
16
|
+
private _flowWaiters = new Map<string, (id: bigint) => void>();
|
|
17
|
+
|
|
18
|
+
constructor(host: string, dbName: string) {
|
|
19
|
+
this._host = host;
|
|
20
|
+
this._dbName = dbName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setKeyHash(keyHash: string): void {
|
|
24
|
+
this._keyHash = keyHash;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async connect(): Promise<StdbConnection> {
|
|
28
|
+
if (this._conn) return this._conn;
|
|
29
|
+
if (this._connectPromise) return this._connectPromise;
|
|
30
|
+
|
|
31
|
+
this._connectPromise = new Promise<StdbConnection>((resolve, reject) => {
|
|
32
|
+
const conn = StdbConnection.builder()
|
|
33
|
+
.withUri(this._host)
|
|
34
|
+
.withDatabaseName(this._dbName)
|
|
35
|
+
.onConnect((c, _identity, _token) => {
|
|
36
|
+
this._conn = c;
|
|
37
|
+
|
|
38
|
+
// Subscribe to flow table to detect newly created flows
|
|
39
|
+
c.subscriptionBuilder()
|
|
40
|
+
.onApplied(() => {
|
|
41
|
+
resolve(c);
|
|
42
|
+
})
|
|
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);
|
|
51
|
+
})
|
|
52
|
+
.onConnectError((_ctx, err) => {
|
|
53
|
+
reject(new Error(`SpacetimeDB connection failed: ${err}`));
|
|
54
|
+
})
|
|
55
|
+
.build();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return this._connectPromise;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get conn(): StdbConnection {
|
|
62
|
+
if (!this._conn) throw new Error('Not connected');
|
|
63
|
+
return this._conn;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Call a reducer and wait for the flow to appear in the subscription.
|
|
68
|
+
* Returns the server-assigned flow ID.
|
|
69
|
+
*/
|
|
70
|
+
async createFlowAndResolveId(
|
|
71
|
+
reducerCall: () => void,
|
|
72
|
+
exportToken: string
|
|
73
|
+
): Promise<bigint> {
|
|
74
|
+
const conn = this.conn;
|
|
75
|
+
|
|
76
|
+
// Check if flow already exists (from a previous subscription update)
|
|
77
|
+
for (const f of conn.db.flow.iter()) {
|
|
78
|
+
if (f.exportToken === exportToken) return f.id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Promise<bigint>((resolve) => {
|
|
82
|
+
// Set up watcher
|
|
83
|
+
this._flowWaiters.set(exportToken, resolve);
|
|
84
|
+
|
|
85
|
+
// Call the reducer
|
|
86
|
+
reducerCall();
|
|
87
|
+
|
|
88
|
+
// Also poll in case subscription update was missed
|
|
89
|
+
const check = () => {
|
|
90
|
+
for (const f of conn.db.flow.iter()) {
|
|
91
|
+
if (f.exportToken === exportToken) {
|
|
92
|
+
this._flowWaiters.delete(exportToken);
|
|
93
|
+
resolve(f.id);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Poll every 100ms for up to 10 seconds
|
|
101
|
+
let attempts = 0;
|
|
102
|
+
const interval = setInterval(() => {
|
|
103
|
+
if (check() || attempts++ > 100) {
|
|
104
|
+
clearInterval(interval);
|
|
105
|
+
if (attempts > 100) {
|
|
106
|
+
this._flowWaiters.delete(exportToken);
|
|
107
|
+
throw new Error(`Flow creation timed out for token ${exportToken}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, 100);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disconnect(): void {
|
|
115
|
+
if (this._timeoutInterval) {
|
|
116
|
+
clearInterval(this._timeoutInterval);
|
|
117
|
+
this._timeoutInterval = null;
|
|
118
|
+
}
|
|
119
|
+
if (this._conn) {
|
|
120
|
+
this._conn.disconnect();
|
|
121
|
+
this._conn = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { SdkConnection } from './connection';
|
|
2
|
+
|
|
3
|
+
export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
|
4
|
+
|
|
5
|
+
// ── Console capture ──────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const _origConsole = {
|
|
8
|
+
log: console.log,
|
|
9
|
+
info: console.info,
|
|
10
|
+
warn: console.warn,
|
|
11
|
+
error: console.error,
|
|
12
|
+
debug: console.debug,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const _flowStack: Flow[] = [];
|
|
16
|
+
|
|
17
|
+
function _patchConsole() {
|
|
18
|
+
const capture = (level: LogLevel) => (...args: unknown[]) => {
|
|
19
|
+
const flow = _flowStack[_flowStack.length - 1];
|
|
20
|
+
if (flow) {
|
|
21
|
+
const msg = args.map(a => {
|
|
22
|
+
if (typeof a === 'string') return a;
|
|
23
|
+
if (a instanceof Error) return a.message;
|
|
24
|
+
try { return JSON.stringify(a); } catch { return String(a); }
|
|
25
|
+
}).join(' ');
|
|
26
|
+
const data = args.length <= 1 ? undefined
|
|
27
|
+
: args.length === 2 ? args[1]
|
|
28
|
+
: args.slice(1);
|
|
29
|
+
flow.log(msg, data, level);
|
|
30
|
+
}
|
|
31
|
+
_origConsole[level === 'info' ? 'log' : level](...args);
|
|
32
|
+
};
|
|
33
|
+
console.log = capture('info');
|
|
34
|
+
console.info = capture('info');
|
|
35
|
+
console.warn = capture('warn');
|
|
36
|
+
console.error = capture('error');
|
|
37
|
+
console.debug = capture('debug');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _restoreConsole() {
|
|
41
|
+
console.log = _origConsole.log;
|
|
42
|
+
console.info = _origConsole.info;
|
|
43
|
+
console.warn = _origConsole.warn;
|
|
44
|
+
console.error = _origConsole.error;
|
|
45
|
+
console.debug = _origConsole.debug;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function serialize(value: unknown): string {
|
|
49
|
+
if (value === undefined || value === null) return '{}';
|
|
50
|
+
if (value instanceof Error)
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
name: value.name,
|
|
53
|
+
message: value.message,
|
|
54
|
+
stack: value.stack,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
return JSON.stringify(value, (_key, v) =>
|
|
58
|
+
typeof v === 'bigint' ? v.toString() : v
|
|
59
|
+
);
|
|
60
|
+
} catch {
|
|
61
|
+
return JSON.stringify({ value: String(value) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function hashKey(secretKey: string): Promise<string> {
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
const data = encoder.encode(secretKey);
|
|
68
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
69
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
70
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateToken(): string {
|
|
74
|
+
const bytes = new Uint8Array(16);
|
|
75
|
+
crypto.getRandomValues(bytes);
|
|
76
|
+
return Array.from(bytes)
|
|
77
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
78
|
+
.join('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class Flow {
|
|
82
|
+
private _sdk: SdkConnection;
|
|
83
|
+
private _keyHash: string;
|
|
84
|
+
private _id: bigint | null = null;
|
|
85
|
+
private _exportToken: string;
|
|
86
|
+
private _ready: Promise<void>;
|
|
87
|
+
private _resolveReady!: () => void;
|
|
88
|
+
private _pendingLogs: Array<{ level: string; message: string; data: string }> = [];
|
|
89
|
+
private _flushScheduled = false;
|
|
90
|
+
private _isReady = false;
|
|
91
|
+
private _finished = false;
|
|
92
|
+
private _parentId: bigint;
|
|
93
|
+
private _name: string;
|
|
94
|
+
private _timeoutSeconds: bigint;
|
|
95
|
+
|
|
96
|
+
/** @internal */
|
|
97
|
+
constructor(
|
|
98
|
+
sdk: SdkConnection,
|
|
99
|
+
keyHash: string,
|
|
100
|
+
parentId: bigint,
|
|
101
|
+
name: string,
|
|
102
|
+
timeoutSeconds: number
|
|
103
|
+
) {
|
|
104
|
+
this._sdk = sdk;
|
|
105
|
+
this._keyHash = keyHash;
|
|
106
|
+
this._parentId = parentId;
|
|
107
|
+
this._name = name;
|
|
108
|
+
this._timeoutSeconds = (!timeoutSeconds || timeoutSeconds === Infinity) ? 0n : BigInt(timeoutSeconds);
|
|
109
|
+
this._exportToken = generateToken();
|
|
110
|
+
this._ready = new Promise<void>((resolve) => {
|
|
111
|
+
this._resolveReady = resolve;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @internal */
|
|
116
|
+
async _create(): Promise<void> {
|
|
117
|
+
const conn = this._sdk.conn;
|
|
118
|
+
const exportToken = this._exportToken;
|
|
119
|
+
const keyHash = this._keyHash;
|
|
120
|
+
const timeoutSeconds = this._timeoutSeconds;
|
|
121
|
+
|
|
122
|
+
if (this._parentId === 0n) {
|
|
123
|
+
const id = await this._sdk.createFlowAndResolveId(
|
|
124
|
+
() => conn.reducers.createRootFlow({ keyHash, timeoutSeconds, exportToken }),
|
|
125
|
+
exportToken
|
|
126
|
+
);
|
|
127
|
+
this._id = id;
|
|
128
|
+
} else {
|
|
129
|
+
const parentFlowId = this._parentId;
|
|
130
|
+
const name = this._name;
|
|
131
|
+
const id = await this._sdk.createFlowAndResolveId(
|
|
132
|
+
() =>
|
|
133
|
+
conn.reducers.createSubFlow({
|
|
134
|
+
keyHash,
|
|
135
|
+
parentFlowId,
|
|
136
|
+
name,
|
|
137
|
+
timeoutSeconds,
|
|
138
|
+
exportToken,
|
|
139
|
+
}),
|
|
140
|
+
exportToken
|
|
141
|
+
);
|
|
142
|
+
this._id = id;
|
|
143
|
+
}
|
|
144
|
+
this._isReady = true;
|
|
145
|
+
this._resolveReady();
|
|
146
|
+
this._drain(); // flush any logs queued before ready
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Log a message with optional data */
|
|
150
|
+
log(message: string, data?: unknown, level: LogLevel = 'info'): this {
|
|
151
|
+
if (this._finished) {
|
|
152
|
+
console.warn(`[radish] Cannot log to finished flow`);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
this._pendingLogs.push({
|
|
156
|
+
level,
|
|
157
|
+
message,
|
|
158
|
+
data: data !== undefined ? serialize(data) : '{}',
|
|
159
|
+
});
|
|
160
|
+
this._scheduleFlush();
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
info(message: string, data?: unknown): this {
|
|
165
|
+
return this.log(message, data, 'info');
|
|
166
|
+
}
|
|
167
|
+
warn(message: string, data?: unknown): this {
|
|
168
|
+
return this.log(message, data, 'warn');
|
|
169
|
+
}
|
|
170
|
+
error(message: string, data?: unknown): this {
|
|
171
|
+
return this.log(message, data, 'error');
|
|
172
|
+
}
|
|
173
|
+
debug(message: string, data?: unknown): this {
|
|
174
|
+
return this.log(message, data, 'debug');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Create a sub-action. Returns immediately — creation runs in background. */
|
|
178
|
+
action(name: string, timeoutSeconds = 100): Flow {
|
|
179
|
+
const child = new Flow(this._sdk, this._keyHash, 0n, name, timeoutSeconds);
|
|
180
|
+
this._ready.then(() => {
|
|
181
|
+
(child as any)._parentId = this._id!;
|
|
182
|
+
child._create();
|
|
183
|
+
});
|
|
184
|
+
return child;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Alias for action() */
|
|
188
|
+
flow(name: string, timeoutSeconds = 100): Flow {
|
|
189
|
+
return this.action(name, timeoutSeconds);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Run a sub-action — auto-creates, runs fn, auto-finishes. console.* inside fn becomes action logs. */
|
|
193
|
+
async a<T>(name: string, fn: (action: Flow) => T | Promise<T>, timeoutSeconds = 100): Promise<T> {
|
|
194
|
+
const child = this.action(name, timeoutSeconds);
|
|
195
|
+
_flowStack.push(child);
|
|
196
|
+
_patchConsole();
|
|
197
|
+
try {
|
|
198
|
+
const result = await fn(child);
|
|
199
|
+
await child.finish();
|
|
200
|
+
return result;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
await child.finishWithError(e instanceof Error ? e : String(e));
|
|
203
|
+
throw e;
|
|
204
|
+
} finally {
|
|
205
|
+
_flowStack.pop();
|
|
206
|
+
if (_flowStack.length === 0) _restoreConsole();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Finish this flow successfully */
|
|
211
|
+
async finish(): Promise<void> {
|
|
212
|
+
await this._ready;
|
|
213
|
+
this._drain(); // sync flush — sends over same WS, ordering preserved
|
|
214
|
+
this._finished = true;
|
|
215
|
+
this._sdk.conn.reducers.finishFlow({
|
|
216
|
+
keyHash: this._keyHash,
|
|
217
|
+
flowId: this._id!,
|
|
218
|
+
status: 'finished',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Finish this flow with error status */
|
|
223
|
+
async finishWithError(err?: Error | string): Promise<void> {
|
|
224
|
+
if (err) {
|
|
225
|
+
this.error(typeof err === 'string' ? err : err.message, err);
|
|
226
|
+
}
|
|
227
|
+
await this._ready;
|
|
228
|
+
this._drain();
|
|
229
|
+
this._finished = true;
|
|
230
|
+
this._sdk.conn.reducers.finishFlow({
|
|
231
|
+
keyHash: this._keyHash,
|
|
232
|
+
flowId: this._id!,
|
|
233
|
+
status: 'error',
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Export this flow's handle for restoration in another context */
|
|
238
|
+
async exportID(): Promise<string> {
|
|
239
|
+
await this._ready;
|
|
240
|
+
return JSON.stringify({
|
|
241
|
+
flowId: this._id!.toString(),
|
|
242
|
+
exportToken: this._exportToken,
|
|
243
|
+
keyHash: this._keyHash,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Get the resolved server-assigned flow ID */
|
|
248
|
+
async getId(): Promise<bigint> {
|
|
249
|
+
await this._ready;
|
|
250
|
+
return this._id!;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Schedule a drain on the next microtask — batches sync logs with ~0ms delay */
|
|
254
|
+
private _scheduleFlush() {
|
|
255
|
+
if (this._flushScheduled) return;
|
|
256
|
+
this._flushScheduled = true;
|
|
257
|
+
queueMicrotask(() => {
|
|
258
|
+
this._flushScheduled = false;
|
|
259
|
+
this._drain();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Synchronously send all pending logs. No-op if flow not ready yet (they'll flush when ready). */
|
|
264
|
+
private _drain(): void {
|
|
265
|
+
if (!this._isReady || this._pendingLogs.length === 0) return;
|
|
266
|
+
const batch = this._pendingLogs.splice(0);
|
|
267
|
+
const conn = this._sdk.conn;
|
|
268
|
+
|
|
269
|
+
if (batch.length === 1) {
|
|
270
|
+
const e = batch[0];
|
|
271
|
+
conn.reducers.addLog({
|
|
272
|
+
keyHash: this._keyHash,
|
|
273
|
+
flowId: this._id!,
|
|
274
|
+
level: e.level,
|
|
275
|
+
message: e.message,
|
|
276
|
+
data: e.data,
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
conn.reducers.addLogsBatch({
|
|
280
|
+
keyHash: this._keyHash,
|
|
281
|
+
flowId: this._id!,
|
|
282
|
+
entries: JSON.stringify(batch),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface RLOptions {
|
|
289
|
+
host?: string;
|
|
290
|
+
dbName?: string;
|
|
291
|
+
label?: string;
|
|
292
|
+
defaultTimeout?: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create a Radish Logger instance. Returns the root action.
|
|
297
|
+
*
|
|
298
|
+
* ```ts
|
|
299
|
+
* const root = await RL('my-secret-key');
|
|
300
|
+
*
|
|
301
|
+
* // Auto-managed — auto-finish on return, auto-error on throw
|
|
302
|
+
* await root.a('request', async (req) => {
|
|
303
|
+
* console.log('GET /api/users');
|
|
304
|
+
* const rows = await req.a('db_query', () => db.query('SELECT ...'));
|
|
305
|
+
* console.log('Done', { count: rows.length });
|
|
306
|
+
* });
|
|
307
|
+
*
|
|
308
|
+
* // Manual — when you need more control
|
|
309
|
+
* const ws = root.action('websocket');
|
|
310
|
+
* ws.info('Connected');
|
|
311
|
+
* await ws.finish();
|
|
312
|
+
*
|
|
313
|
+
* await root.finish();
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
export async function RL(secretKey: string, options: RLOptions = {}): Promise<Flow> {
|
|
317
|
+
const {
|
|
318
|
+
host = 'wss://maincloud.spacetimedb.com',
|
|
319
|
+
dbName = 'radish-log',
|
|
320
|
+
label = '',
|
|
321
|
+
defaultTimeout = 100,
|
|
322
|
+
} = options;
|
|
323
|
+
|
|
324
|
+
const keyHash = await hashKey(secretKey);
|
|
325
|
+
const sdk = new SdkConnection(host, dbName);
|
|
326
|
+
sdk.setKeyHash(keyHash);
|
|
327
|
+
await sdk.connect();
|
|
328
|
+
|
|
329
|
+
// Register key (idempotent — server rejects dupes)
|
|
330
|
+
try {
|
|
331
|
+
sdk.conn.reducers.registerKey({ keyHash, label });
|
|
332
|
+
} catch {
|
|
333
|
+
// Already registered
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check for timed-out flows (fire-and-forget)
|
|
337
|
+
try {
|
|
338
|
+
sdk.conn.reducers.checkTimeouts({ keyHash });
|
|
339
|
+
} catch {
|
|
340
|
+
// Non-critical
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Root flow — creation runs in background, logs queue until ready
|
|
344
|
+
const root = new Flow(sdk, keyHash, 0n, '/', 0);
|
|
345
|
+
root._create(); // fire-and-forget — resolves _ready when server assigns ID
|
|
346
|
+
return root;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Restore a flow from an exported ID string */
|
|
350
|
+
export async function restoreFlow(
|
|
351
|
+
secretKey: string,
|
|
352
|
+
exportedId: string,
|
|
353
|
+
options: RLOptions = {}
|
|
354
|
+
): Promise<Flow> {
|
|
355
|
+
const { host = 'wss://maincloud.spacetimedb.com', dbName = 'radish-log' } = options;
|
|
356
|
+
|
|
357
|
+
const parsed = JSON.parse(exportedId);
|
|
358
|
+
const keyHash = await hashKey(secretKey);
|
|
359
|
+
if (keyHash !== parsed.keyHash) {
|
|
360
|
+
throw new Error('Secret key does not match the flow owner');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const sdk = new SdkConnection(host, dbName);
|
|
364
|
+
await sdk.connect();
|
|
365
|
+
|
|
366
|
+
const flow = new Flow(sdk, keyHash, 0n, 'restored', 100);
|
|
367
|
+
(flow as any)._id = BigInt(parsed.flowId);
|
|
368
|
+
(flow as any)._exportToken = parsed.exportToken;
|
|
369
|
+
(flow as any)._resolveReady();
|
|
370
|
+
return flow;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Generate a random secret key */
|
|
374
|
+
export function generateKey(): string {
|
|
375
|
+
const bytes = new Uint8Array(32);
|
|
376
|
+
crypto.getRandomValues(bytes);
|
|
377
|
+
return (
|
|
378
|
+
'rl_' +
|
|
379
|
+
Array.from(bytes)
|
|
380
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
381
|
+
.join('')
|
|
382
|
+
);
|
|
383
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
flowId: __t.u64().name("flow_id"),
|
|
16
|
+
name: __t.string(),
|
|
17
|
+
status: __t.string(),
|
|
18
|
+
createdAt: __t.timestamp().name("created_at"),
|
|
19
|
+
finishedAt: __t.u64().name("finished_at"),
|
|
20
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
flowId: __t.u64(),
|
|
16
|
+
level: __t.string(),
|
|
17
|
+
message: __t.string(),
|
|
18
|
+
data: __t.string(),
|
|
19
|
+
};
|
|
@@ -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
|
+
flowId: __t.u64(),
|
|
16
|
+
entries: __t.string(),
|
|
17
|
+
};
|
|
@@ -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 __t.row({
|
|
14
|
+
keyHash: __t.string().primaryKey().name("key_hash"),
|
|
15
|
+
label: __t.string(),
|
|
16
|
+
createdAt: __t.timestamp().name("created_at"),
|
|
17
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
};
|
|
@@ -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
|
+
timeoutSeconds: __t.u64(),
|
|
16
|
+
exportToken: __t.string(),
|
|
17
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
parentFlowId: __t.u64(),
|
|
16
|
+
name: __t.string(),
|
|
17
|
+
timeoutSeconds: __t.u64(),
|
|
18
|
+
exportToken: __t.string(),
|
|
19
|
+
};
|
|
@@ -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
|
+
actionId: __t.u64(),
|
|
16
|
+
status: __t.string(),
|
|
17
|
+
};
|
|
@@ -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
|
+
flowId: __t.u64(),
|
|
16
|
+
status: __t.string(),
|
|
17
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
parentFlowId: __t.u64().name("parent_flow_id"),
|
|
17
|
+
name: __t.string(),
|
|
18
|
+
path: __t.string(),
|
|
19
|
+
status: __t.string(),
|
|
20
|
+
timeoutSeconds: __t.u64().name("timeout_seconds"),
|
|
21
|
+
createdAt: __t.timestamp().name("created_at"),
|
|
22
|
+
finishedAt: __t.u64().name("finished_at"),
|
|
23
|
+
exportToken: __t.string().name("export_token"),
|
|
24
|
+
});
|