@ironflow/node 0.20.2 → 0.21.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/dist/agent/agent.d.ts +60 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/agent.js +133 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/approve.d.ts +23 -0
- package/dist/agent/approve.d.ts.map +1 -0
- package/dist/agent/approve.js +42 -0
- package/dist/agent/approve.js.map +1 -0
- package/dist/agent/dispatch.d.ts +63 -0
- package/dist/agent/dispatch.d.ts.map +1 -0
- package/dist/agent/dispatch.js +130 -0
- package/dist/agent/dispatch.js.map +1 -0
- package/dist/agent/errors.d.ts +90 -0
- package/dist/agent/errors.d.ts.map +1 -0
- package/dist/agent/errors.js +136 -0
- package/dist/agent/errors.js.map +1 -0
- package/dist/agent/index.d.ts +35 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +32 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/internal-registry.d.ts +27 -0
- package/dist/agent/internal-registry.d.ts.map +1 -0
- package/dist/agent/internal-registry.js +36 -0
- package/dist/agent/internal-registry.js.map +1 -0
- package/dist/agent/internal.d.ts +24 -0
- package/dist/agent/internal.d.ts.map +1 -0
- package/dist/agent/internal.js +29 -0
- package/dist/agent/internal.js.map +1 -0
- package/dist/agent/llm.d.ts +39 -0
- package/dist/agent/llm.d.ts.map +1 -0
- package/dist/agent/llm.js +59 -0
- package/dist/agent/llm.js.map +1 -0
- package/dist/agent/mcp.d.ts +51 -0
- package/dist/agent/mcp.d.ts.map +1 -0
- package/dist/agent/mcp.js +155 -0
- package/dist/agent/mcp.js.map +1 -0
- package/dist/agent/memory.d.ts +74 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +130 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/spawn.d.ts +20 -0
- package/dist/agent/spawn.d.ts.map +1 -0
- package/dist/agent/spawn.js +29 -0
- package/dist/agent/spawn.js.map +1 -0
- package/dist/agent/tool.d.ts +39 -0
- package/dist/agent/tool.d.ts.map +1 -0
- package/dist/agent/tool.js +103 -0
- package/dist/agent/tool.js.map +1 -0
- package/dist/agent/types.d.ts +363 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +9 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/client.d.ts +942 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1557 -0
- package/dist/client.js.map +1 -0
- package/dist/command-dedup.d.ts +61 -0
- package/dist/command-dedup.d.ts.map +1 -0
- package/dist/command-dedup.js +129 -0
- package/dist/command-dedup.js.map +1 -0
- package/dist/config-client.d.ts +58 -0
- package/dist/config-client.d.ts.map +1 -0
- package/dist/config-client.js +171 -0
- package/dist/config-client.js.map +1 -0
- package/dist/function.d.ts +53 -0
- package/dist/function.d.ts.map +1 -0
- package/dist/function.js +72 -0
- package/dist/function.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/assert-defined.d.ts +10 -0
- package/dist/internal/assert-defined.d.ts.map +1 -0
- package/dist/internal/assert-defined.js +15 -0
- package/dist/internal/assert-defined.js.map +1 -0
- package/dist/internal/context.d.ts +142 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/context.js +306 -0
- package/dist/internal/context.js.map +1 -0
- package/dist/internal/errors.d.ts +66 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +29 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/run-context.d.ts +10 -0
- package/dist/internal/run-context.d.ts.map +1 -0
- package/dist/internal/run-context.js +23 -0
- package/dist/internal/run-context.js.map +1 -0
- package/dist/kv.d.ts +86 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +261 -0
- package/dist/kv.js.map +1 -0
- package/dist/projection-runner.d.ts +83 -0
- package/dist/projection-runner.d.ts.map +1 -0
- package/dist/projection-runner.js +498 -0
- package/dist/projection-runner.js.map +1 -0
- package/dist/projection.d.ts +36 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +55 -0
- package/dist/projection.js.map +1 -0
- package/dist/secrets.d.ts +6 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +19 -0
- package/dist/secrets.js.map +1 -0
- package/dist/serve.d.ts +71 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +460 -0
- package/dist/serve.js.map +1 -0
- package/dist/step.d.ts +18 -0
- package/dist/step.d.ts.map +1 -0
- package/dist/step.js +581 -0
- package/dist/step.js.map +1 -0
- package/dist/subscribe.d.ts +164 -0
- package/dist/subscribe.d.ts.map +1 -0
- package/dist/subscribe.js +487 -0
- package/dist/subscribe.js.map +1 -0
- package/dist/test/index.d.ts +22 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +112 -0
- package/dist/test/index.js.map +1 -0
- package/dist/test/test-step.d.ts +21 -0
- package/dist/test/test-step.d.ts.map +1 -0
- package/dist/test/test-step.js +83 -0
- package/dist/test/test-step.js.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/webhook.d.ts +22 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +23 -0
- package/dist/webhook.js.map +1 -0
- package/dist/worker-streaming.d.ts +17 -0
- package/dist/worker-streaming.d.ts.map +1 -0
- package/dist/worker-streaming.js +510 -0
- package/dist/worker-streaming.js.map +1 -0
- package/dist/worker.d.ts +28 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +559 -0
- package/dist/worker.js.map +1 -0
- package/package.json +3 -3
package/dist/client.js
ADDED
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ironflow Node.js Client
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for interacting with the Ironflow server.
|
|
5
|
+
* Provides methods for registering functions, triggering events, and managing runs.
|
|
6
|
+
*/
|
|
7
|
+
import { getCurrentRunId } from "./internal/run-context.js";
|
|
8
|
+
import { API_ENDPOINTS, DEFAULT_SERVER_URL, getServerUrl, IronflowError, RunFailedError, RunCancelledError, UnauthenticatedError, EnterpriseRequiredError, UnauthorizedError, peelProjectionEnvelope, } from "@ironflow/core";
|
|
9
|
+
import { KVClient } from "./kv.js";
|
|
10
|
+
import { CommandDedup } from "./command-dedup.js";
|
|
11
|
+
import { ConfigClient } from "./config-client.js";
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Client Implementation
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Ironflow client for server-side operations
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { createClient } from "@ironflow/node";
|
|
21
|
+
*
|
|
22
|
+
* const client = createClient({
|
|
23
|
+
* serverUrl: "http://localhost:9123",
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Register a function
|
|
27
|
+
* await client.registerFunction({
|
|
28
|
+
* id: "my-function",
|
|
29
|
+
* name: "My Function",
|
|
30
|
+
* triggers: [{ event: "my.event" }],
|
|
31
|
+
* endpointUrl: "http://localhost:3000/api/ironflow",
|
|
32
|
+
* preferredMode: "push",
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Emit an event
|
|
36
|
+
* const result = await client.emit("my.event", { data: "value" });
|
|
37
|
+
* console.log("Created runs:", result.runIds);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class IronflowClient {
|
|
41
|
+
serverUrl;
|
|
42
|
+
apiKey;
|
|
43
|
+
timeout;
|
|
44
|
+
onErrorHandler;
|
|
45
|
+
constructor(config = {}) {
|
|
46
|
+
this.serverUrl = config.serverUrl || getServerUrl() || DEFAULT_SERVER_URL;
|
|
47
|
+
this.apiKey = config.apiKey;
|
|
48
|
+
this.timeout = config.timeout ?? 30000;
|
|
49
|
+
this.onErrorHandler = config.onError;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Register a function with the Ironflow server
|
|
53
|
+
*/
|
|
54
|
+
async registerFunction(request) {
|
|
55
|
+
const body = {
|
|
56
|
+
id: request.id,
|
|
57
|
+
};
|
|
58
|
+
if (request.name)
|
|
59
|
+
body.name = request.name;
|
|
60
|
+
if (request.description)
|
|
61
|
+
body.description = request.description;
|
|
62
|
+
if (request.triggers)
|
|
63
|
+
body.triggers = request.triggers;
|
|
64
|
+
if (request.retry)
|
|
65
|
+
body.retry = request.retry;
|
|
66
|
+
if (request.timeoutMs)
|
|
67
|
+
body.timeoutMs = request.timeoutMs;
|
|
68
|
+
if (request.concurrency)
|
|
69
|
+
body.concurrency = request.concurrency;
|
|
70
|
+
if (request.debounce) {
|
|
71
|
+
// Server expects snake_case period_ms / max_wait_ms;
|
|
72
|
+
// SDK uses camelCase for parity with the rest of the TS surface.
|
|
73
|
+
body.debounce = {
|
|
74
|
+
period_ms: request.debounce.periodMs,
|
|
75
|
+
key: request.debounce.key ?? "",
|
|
76
|
+
...(request.debounce.maxWaitMs != null
|
|
77
|
+
? { max_wait_ms: request.debounce.maxWaitMs }
|
|
78
|
+
: {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (request.preferredMode)
|
|
82
|
+
body.preferredMode = request.preferredMode;
|
|
83
|
+
if (request.endpointUrl)
|
|
84
|
+
body.endpointUrl = request.endpointUrl;
|
|
85
|
+
if (request.actorKey)
|
|
86
|
+
body.actorKey = request.actorKey;
|
|
87
|
+
if (request.pauseBehavior)
|
|
88
|
+
body.pauseBehavior = request.pauseBehavior;
|
|
89
|
+
if (request.compensateOnCancel)
|
|
90
|
+
body.compensateOnCancel = true;
|
|
91
|
+
if (request.cancelOn?.length)
|
|
92
|
+
body.cancelOn = request.cancelOn;
|
|
93
|
+
const response = await this.request(API_ENDPOINTS.REGISTER_FUNCTION, body, "registerFunction");
|
|
94
|
+
return { created: response.created };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Emit an event to trigger workflows
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const result = await client.emit("order.placed", {
|
|
102
|
+
* orderId: "123",
|
|
103
|
+
* total: 99.99,
|
|
104
|
+
* });
|
|
105
|
+
* console.log("Created runs:", result.runIds);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
async emit(eventName, data, options) {
|
|
109
|
+
const body = {
|
|
110
|
+
event: eventName,
|
|
111
|
+
data,
|
|
112
|
+
};
|
|
113
|
+
if (options?.version)
|
|
114
|
+
body.version = options.version;
|
|
115
|
+
if (options?.idempotencyKey)
|
|
116
|
+
body.idempotencyKey = options.idempotencyKey;
|
|
117
|
+
if (options?.metadata)
|
|
118
|
+
body.metadata = options.metadata;
|
|
119
|
+
const response = await this.request(API_ENDPOINTS.TRIGGER, body, "emit");
|
|
120
|
+
return {
|
|
121
|
+
runIds: response.runIds || [],
|
|
122
|
+
eventId: response.eventId,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Emit an event synchronously — waits for the triggered run to complete and returns the result.
|
|
127
|
+
*
|
|
128
|
+
* Calls the TriggerSync endpoint, which blocks until the run finishes or the timeout elapses.
|
|
129
|
+
* Throws RunFailedError if the run fails, RunCancelledError if it is cancelled.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const result = await client.emitSync("order.placed", { orderId: "123" });
|
|
134
|
+
* console.log("Output:", result.output);
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
async emitSync(eventName, data, options) {
|
|
138
|
+
const timeout = options?.timeout ?? 30000;
|
|
139
|
+
const fetchTimeout = timeout + 5000;
|
|
140
|
+
const url = `${this.serverUrl}/ironflow.v1.IronflowService/TriggerSync`;
|
|
141
|
+
const headers = {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
};
|
|
144
|
+
if (this.apiKey) {
|
|
145
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
146
|
+
}
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const timeoutId = setTimeout(() => controller.abort(), fetchTimeout);
|
|
149
|
+
let status;
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers,
|
|
154
|
+
body: JSON.stringify({ event: eventName, data, timeout_ms: timeout }),
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
});
|
|
157
|
+
status = response.status;
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const errorBody = await response.text();
|
|
160
|
+
let errorMessage = `Request failed with status ${response.status}`;
|
|
161
|
+
try {
|
|
162
|
+
const errorJson = JSON.parse(errorBody);
|
|
163
|
+
if (errorJson.message)
|
|
164
|
+
errorMessage = errorJson.message;
|
|
165
|
+
else if (errorJson.code)
|
|
166
|
+
errorMessage = `Error code: ${errorJson.code}`;
|
|
167
|
+
else
|
|
168
|
+
errorMessage = errorBody;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
errorMessage = errorBody;
|
|
172
|
+
}
|
|
173
|
+
this.throwTypedError(response.status, errorMessage);
|
|
174
|
+
}
|
|
175
|
+
const body = await response.json();
|
|
176
|
+
if (!body.results?.length) {
|
|
177
|
+
throw new IronflowError("No results returned from TriggerSync", { code: "NO_RESULTS", retryable: false });
|
|
178
|
+
}
|
|
179
|
+
const result = body.results[0];
|
|
180
|
+
if (!result) {
|
|
181
|
+
throw new IronflowError("No results returned from TriggerSync", { code: "NO_RESULTS", retryable: false });
|
|
182
|
+
}
|
|
183
|
+
if (result.status === "failed") {
|
|
184
|
+
throw new RunFailedError(result.runId, result.error);
|
|
185
|
+
}
|
|
186
|
+
if (result.status === "cancelled") {
|
|
187
|
+
throw new RunCancelledError(result.runId);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
runId: result.runId,
|
|
191
|
+
functionId: result.functionId,
|
|
192
|
+
status: result.status,
|
|
193
|
+
output: result.output,
|
|
194
|
+
durationMs: result.durationMs,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
await this.callOnError(error, { method: "emitSync", endpoint: "/ironflow.v1.IronflowService/TriggerSync", statusCode: status });
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
clearTimeout(timeoutId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Publish a message to a developer pub/sub topic.
|
|
207
|
+
* Unlike emit(), this does NOT trigger workflow functions.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* const result = await client.publish("notifications", {
|
|
212
|
+
* userId: "123",
|
|
213
|
+
* message: "Hello!",
|
|
214
|
+
* });
|
|
215
|
+
* console.log("Published:", result.eventId, result.sequence);
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
async publish(topic, data, options) {
|
|
219
|
+
const body = {
|
|
220
|
+
topic,
|
|
221
|
+
data: data ?? {},
|
|
222
|
+
};
|
|
223
|
+
if (options?.idempotencyKey) {
|
|
224
|
+
body.idempotencyKey = options.idempotencyKey;
|
|
225
|
+
}
|
|
226
|
+
const response = await this.request("/ironflow.v1.PubSubService/Publish", body, "publish");
|
|
227
|
+
return {
|
|
228
|
+
eventId: response.eventId,
|
|
229
|
+
sequence: parseInt(response.sequence, 10) || 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* List all active developer pub/sub topics.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const topics = await client.listTopics();
|
|
238
|
+
* for (const t of topics) {
|
|
239
|
+
* console.log(t.name, t.messageCount);
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
async listTopics() {
|
|
244
|
+
const response = await this.request("/ironflow.v1.PubSubService/ListTopics", {}, "listTopics");
|
|
245
|
+
return (response.topics ?? []).map((t) => ({
|
|
246
|
+
name: String(t.name ?? ""),
|
|
247
|
+
messageCount: Number(t.messageCount ?? 0),
|
|
248
|
+
consumerCount: Number(t.consumerCount ?? 0),
|
|
249
|
+
firstMessageAt: t.firstMessageAt ? String(t.firstMessageAt) : undefined,
|
|
250
|
+
lastMessageAt: t.lastMessageAt ? String(t.lastMessageAt) : undefined,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get detailed statistics for a topic.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const stats = await client.getTopicStats("notifications");
|
|
259
|
+
* console.log("Messages:", stats.messageCount, "Lag:", stats.lag);
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
async getTopicStats(topic) {
|
|
263
|
+
const response = await this.request("/ironflow.v1.PubSubService/GetTopicStats", { topic }, "getTopicStats");
|
|
264
|
+
return {
|
|
265
|
+
name: String(response.name ?? ""),
|
|
266
|
+
messageCount: Number(response.messageCount ?? 0),
|
|
267
|
+
consumerCount: Number(response.consumerCount ?? 0),
|
|
268
|
+
lag: Number(response.lag ?? 0),
|
|
269
|
+
firstSeq: Number(response.firstSeq ?? 0),
|
|
270
|
+
lastSeq: Number(response.lastSeq ?? 0),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get a run by ID
|
|
275
|
+
*/
|
|
276
|
+
async getRun(runId) {
|
|
277
|
+
return this.request(API_ENDPOINTS.GET_RUN, { id: runId }, "getRun");
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* List runs with optional filtering
|
|
281
|
+
*/
|
|
282
|
+
async listRuns(options) {
|
|
283
|
+
const body = {};
|
|
284
|
+
if (options?.functionId)
|
|
285
|
+
body.functionId = options.functionId;
|
|
286
|
+
if (options?.status)
|
|
287
|
+
body.status = options.status;
|
|
288
|
+
if (options?.limit)
|
|
289
|
+
body.limit = options.limit;
|
|
290
|
+
if (options?.cursor)
|
|
291
|
+
body.cursor = options.cursor;
|
|
292
|
+
return this.request(API_ENDPOINTS.LIST_RUNS, body, "listRuns");
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Cancel a running workflow
|
|
296
|
+
*/
|
|
297
|
+
async cancelRun(runId, reason) {
|
|
298
|
+
return this.request(API_ENDPOINTS.CANCEL_RUN, {
|
|
299
|
+
id: runId,
|
|
300
|
+
reason: reason || "",
|
|
301
|
+
}, "cancelRun");
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Retry a failed run
|
|
305
|
+
*/
|
|
306
|
+
async retryRun(runId, fromStep) {
|
|
307
|
+
const body = { id: runId };
|
|
308
|
+
if (fromStep)
|
|
309
|
+
body.fromStep = fromStep;
|
|
310
|
+
return this.request(API_ENDPOINTS.RETRY_RUN, body, "retryRun");
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Health check
|
|
314
|
+
*/
|
|
315
|
+
async health() {
|
|
316
|
+
const response = await this.request(API_ENDPOINTS.HEALTH, {}, "health");
|
|
317
|
+
return response.status;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Entity stream operations
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* // Append an event to a stream
|
|
325
|
+
* const result = await client.streams.append("order-123", {
|
|
326
|
+
* name: "order.created",
|
|
327
|
+
* data: { total: 100 },
|
|
328
|
+
* entityType: "order",
|
|
329
|
+
* });
|
|
330
|
+
*
|
|
331
|
+
* // Read events from a stream
|
|
332
|
+
* const { events } = await client.streams.read("order-123", { limit: 10 });
|
|
333
|
+
*
|
|
334
|
+
* // Get stream info
|
|
335
|
+
* const info = await client.streams.getInfo("order-123");
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
streams = {
|
|
339
|
+
/**
|
|
340
|
+
* Append an event to an entity stream
|
|
341
|
+
*/
|
|
342
|
+
append: async (entityId, input, options) => {
|
|
343
|
+
const body = {
|
|
344
|
+
entity_id: entityId,
|
|
345
|
+
entity_type: input.entityType,
|
|
346
|
+
event_name: input.name,
|
|
347
|
+
data: input.data,
|
|
348
|
+
expected_version: options?.expectedVersion ?? -1,
|
|
349
|
+
idempotency_key: options?.idempotencyKey ?? "",
|
|
350
|
+
version: options?.version ?? 1,
|
|
351
|
+
};
|
|
352
|
+
if (options?.metadata !== undefined) {
|
|
353
|
+
body.metadata = options.metadata;
|
|
354
|
+
}
|
|
355
|
+
const response = await this.request("/ironflow.v1.EntityStreamService/AppendEvent", body, "streams.append");
|
|
356
|
+
return {
|
|
357
|
+
entityVersion: Number(response.entityVersion ?? 0),
|
|
358
|
+
eventId: response.eventId,
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
/**
|
|
362
|
+
* Read events from an entity stream
|
|
363
|
+
*/
|
|
364
|
+
read: async (entityId, options) => {
|
|
365
|
+
const response = await this.request("/ironflow.v1.EntityStreamService/ReadStream", {
|
|
366
|
+
entity_id: entityId,
|
|
367
|
+
from_version: options?.fromVersion ?? 0,
|
|
368
|
+
limit: options?.limit ?? 0,
|
|
369
|
+
direction: options?.direction ?? "forward",
|
|
370
|
+
}, "streams.read");
|
|
371
|
+
return {
|
|
372
|
+
events: (response.events ?? []).map((e) => ({
|
|
373
|
+
id: e.id,
|
|
374
|
+
name: e.name,
|
|
375
|
+
data: e.data ?? {},
|
|
376
|
+
entityVersion: Number(e.entityVersion ?? 0),
|
|
377
|
+
version: e.version,
|
|
378
|
+
timestamp: e.timestamp,
|
|
379
|
+
source: e.source,
|
|
380
|
+
metadata: e.metadata,
|
|
381
|
+
})),
|
|
382
|
+
totalCount: response.totalCount ?? 0,
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
/**
|
|
386
|
+
* Get information about an entity stream.
|
|
387
|
+
*
|
|
388
|
+
* Returns `null` if no events have been written to this stream yet — safe to
|
|
389
|
+
* pass `expectedVersion: 0` to `append()` in that case to create the first event.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* const info = await client.streams.getInfo("order-123");
|
|
394
|
+
* await client.streams.append("order-123", event, {
|
|
395
|
+
* expectedVersion: info ? info.version : 0,
|
|
396
|
+
* });
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
getInfo: async (entityId) => {
|
|
400
|
+
try {
|
|
401
|
+
const response = await this.request("/ironflow.v1.EntityStreamService/GetStreamInfo", {
|
|
402
|
+
entity_id: entityId,
|
|
403
|
+
}, "streams.getInfo");
|
|
404
|
+
return {
|
|
405
|
+
entityId: response.entityId,
|
|
406
|
+
entityType: response.entityType,
|
|
407
|
+
version: Number(response.version ?? 0),
|
|
408
|
+
eventCount: Number(response.eventCount ?? 0),
|
|
409
|
+
createdAt: response.createdAt,
|
|
410
|
+
updatedAt: response.updatedAt,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
if (err instanceof IronflowError && err.message === "stream not found") {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
/**
|
|
421
|
+
* Create a snapshot of the materialized state at a specific stream version.
|
|
422
|
+
* Use snapshots to speed up state reconstruction for long-lived entity streams.
|
|
423
|
+
*/
|
|
424
|
+
createSnapshot: async (entityId, input) => {
|
|
425
|
+
const response = await this.request("/ironflow.v1.EntityStreamService/CreateSnapshot", {
|
|
426
|
+
entity_id: entityId,
|
|
427
|
+
entity_type: input.entityType,
|
|
428
|
+
entity_version: input.entityVersion,
|
|
429
|
+
state: input.state,
|
|
430
|
+
}, "streams.createSnapshot");
|
|
431
|
+
return { snapshotId: response.snapshotId };
|
|
432
|
+
},
|
|
433
|
+
/**
|
|
434
|
+
* Get the latest snapshot at or before a given version.
|
|
435
|
+
* Returns the snapshot closest to the requested version without exceeding it.
|
|
436
|
+
*/
|
|
437
|
+
getSnapshot: async (entityId, options) => {
|
|
438
|
+
const response = await this.request("/ironflow.v1.EntityStreamService/GetSnapshot", {
|
|
439
|
+
entity_id: entityId,
|
|
440
|
+
before_version: options?.beforeVersion ?? 0,
|
|
441
|
+
}, "streams.getSnapshot");
|
|
442
|
+
return {
|
|
443
|
+
snapshotId: response.snapshotId,
|
|
444
|
+
entityId: response.entityId,
|
|
445
|
+
entityType: response.entityType,
|
|
446
|
+
entityVersion: Number(response.entityVersion ?? 0),
|
|
447
|
+
state: response.state,
|
|
448
|
+
createdAt: response.createdAt,
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
/**
|
|
452
|
+
* List all entity streams.
|
|
453
|
+
*/
|
|
454
|
+
listStreams: async () => {
|
|
455
|
+
const resp = await this.restRequest("GET", "/api/v1/streams", undefined, "streams.listStreams");
|
|
456
|
+
return resp.streams ?? [];
|
|
457
|
+
},
|
|
458
|
+
/**
|
|
459
|
+
* Get the full event history for an entity.
|
|
460
|
+
*/
|
|
461
|
+
getEntityHistory: async (entityId) => {
|
|
462
|
+
const resp = await this.restRequest("GET", `/api/v1/streams/${encodeURIComponent(entityId)}/history`, undefined, "streams.getEntityHistory");
|
|
463
|
+
return resp.events ?? [];
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* SQL-backed projections
|
|
468
|
+
*
|
|
469
|
+
* Create materialized SQL tables from event streams. Events are processed
|
|
470
|
+
* server-side using parameterized SQL handlers.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* // Create a SQL projection
|
|
475
|
+
* await client.sqlProjections.create({
|
|
476
|
+
* name: "board",
|
|
477
|
+
* tableSql: "CREATE TABLE proj_board (id TEXT PRIMARY KEY, title TEXT, status TEXT)",
|
|
478
|
+
* eventHandlers: {
|
|
479
|
+
* "issue.created": "INSERT INTO proj_board (id, title, status) VALUES (:entity_id, :data.title, 'OPEN')",
|
|
480
|
+
* "issue.status_changed": "UPDATE proj_board SET status = :data.to WHERE id = :entity_id",
|
|
481
|
+
* },
|
|
482
|
+
* events: ["issue.created", "issue.status_changed"],
|
|
483
|
+
* });
|
|
484
|
+
*
|
|
485
|
+
* // Query the projection
|
|
486
|
+
* const result = await client.sqlProjections.query("board", {
|
|
487
|
+
* where: "status = 'OPEN'",
|
|
488
|
+
* orderBy: "title ASC",
|
|
489
|
+
* limit: 50,
|
|
490
|
+
* });
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
sqlProjections = {
|
|
494
|
+
/**
|
|
495
|
+
* Create a SQL-backed projection with a materialized table and event handlers.
|
|
496
|
+
*/
|
|
497
|
+
create: async (input) => {
|
|
498
|
+
const response = await this.request("/ironflow.v1.ProjectionService/CreateSQLProjection", {
|
|
499
|
+
name: input.name,
|
|
500
|
+
table_sql: input.tableSql,
|
|
501
|
+
event_handlers: input.eventHandlers,
|
|
502
|
+
events: input.events,
|
|
503
|
+
description: input.description ?? "",
|
|
504
|
+
}, "sqlProjections.create");
|
|
505
|
+
return { name: response.name, status: response.status };
|
|
506
|
+
},
|
|
507
|
+
/**
|
|
508
|
+
* Query a SQL-backed projection table with optional filtering, ordering, and pagination.
|
|
509
|
+
*/
|
|
510
|
+
query: async (name, options) => {
|
|
511
|
+
const response = await this.request("/ironflow.v1.ProjectionService/QuerySQLProjection", {
|
|
512
|
+
name,
|
|
513
|
+
where: options?.where ?? "",
|
|
514
|
+
order_by: options?.orderBy ?? "",
|
|
515
|
+
limit: options?.limit ?? 100,
|
|
516
|
+
offset: options?.offset ?? 0,
|
|
517
|
+
}, "sqlProjections.query");
|
|
518
|
+
return {
|
|
519
|
+
columns: response.columns ?? [],
|
|
520
|
+
rows: (response.rows ?? []).map((r) => r.values),
|
|
521
|
+
totalCount: response.totalCount ?? 0,
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* API key management
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* // Create an API key
|
|
531
|
+
* const { key } = await client.apiKeys.create({ name: "ci-key" });
|
|
532
|
+
*
|
|
533
|
+
* // List all API keys
|
|
534
|
+
* const keys = await client.apiKeys.list();
|
|
535
|
+
*
|
|
536
|
+
* // Rotate a key
|
|
537
|
+
* const rotated = await client.apiKeys.rotate(keys[0].id);
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
apiKeys = {
|
|
541
|
+
/** Create a new API key */
|
|
542
|
+
create: async (input) => {
|
|
543
|
+
return this.restRequest("POST", "/api/v1/apikeys", input, "apiKeys.create");
|
|
544
|
+
},
|
|
545
|
+
/** List all API keys */
|
|
546
|
+
list: async () => {
|
|
547
|
+
return this.restRequest("GET", "/api/v1/apikeys", undefined, "apiKeys.list");
|
|
548
|
+
},
|
|
549
|
+
/** Get an API key by ID */
|
|
550
|
+
get: async (id) => {
|
|
551
|
+
return this.restRequest("GET", `/api/v1/apikeys/${id}`, undefined, "apiKeys.get");
|
|
552
|
+
},
|
|
553
|
+
/** Delete an API key */
|
|
554
|
+
delete: async (id) => {
|
|
555
|
+
await this.restRequest("DELETE", `/api/v1/apikeys/${id}`, undefined, "apiKeys.delete");
|
|
556
|
+
},
|
|
557
|
+
/** Rotate an API key (generates a new secret) */
|
|
558
|
+
rotate: async (id) => {
|
|
559
|
+
return this.restRequest("POST", `/api/v1/apikeys/${id}/rotate`, undefined, "apiKeys.rotate");
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* Organization management (enterprise)
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```typescript
|
|
567
|
+
* const org = await client.orgs.create({ name: "Acme Corp" });
|
|
568
|
+
* const orgs = await client.orgs.list();
|
|
569
|
+
* await client.orgs.update(org.id, { name: "Acme Inc" });
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
orgs = {
|
|
573
|
+
/** Create a new organization */
|
|
574
|
+
create: async (input) => {
|
|
575
|
+
return this.restRequest("POST", "/api/v1/orgs", input, "orgs.create");
|
|
576
|
+
},
|
|
577
|
+
/** List all organizations */
|
|
578
|
+
list: async () => {
|
|
579
|
+
return this.restRequest("GET", "/api/v1/orgs", undefined, "orgs.list");
|
|
580
|
+
},
|
|
581
|
+
/** Get an organization by ID */
|
|
582
|
+
get: async (id) => {
|
|
583
|
+
return this.restRequest("GET", `/api/v1/orgs/${id}`, undefined, "orgs.get");
|
|
584
|
+
},
|
|
585
|
+
/** Update an organization */
|
|
586
|
+
update: async (id, input) => {
|
|
587
|
+
return this.restRequest("PATCH", `/api/v1/orgs/${id}`, input, "orgs.update");
|
|
588
|
+
},
|
|
589
|
+
/** Delete an organization */
|
|
590
|
+
delete: async (id) => {
|
|
591
|
+
await this.restRequest("DELETE", `/api/v1/orgs/${id}`, undefined, "orgs.delete");
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Role management (enterprise)
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* ```typescript
|
|
599
|
+
* const role = await client.roles.create({ name: "deployer", org_id: orgId });
|
|
600
|
+
* await client.roles.assignPolicy(role.id, policyId);
|
|
601
|
+
* const roles = await client.roles.list(orgId);
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
604
|
+
roles = {
|
|
605
|
+
/** Create a new role */
|
|
606
|
+
create: async (input) => {
|
|
607
|
+
return this.restRequest("POST", "/api/v1/roles", input, "roles.create");
|
|
608
|
+
},
|
|
609
|
+
/** List roles, optionally filtered by organization */
|
|
610
|
+
list: async (orgId) => {
|
|
611
|
+
const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
|
|
612
|
+
return this.restRequest("GET", `/api/v1/roles${query}`, undefined, "roles.list");
|
|
613
|
+
},
|
|
614
|
+
/** Get a role by ID */
|
|
615
|
+
get: async (id) => {
|
|
616
|
+
return this.restRequest("GET", `/api/v1/roles/${id}`, undefined, "roles.get");
|
|
617
|
+
},
|
|
618
|
+
/** Update a role */
|
|
619
|
+
update: async (id, input) => {
|
|
620
|
+
return this.restRequest("PATCH", `/api/v1/roles/${id}`, input, "roles.update");
|
|
621
|
+
},
|
|
622
|
+
/** Delete a role */
|
|
623
|
+
delete: async (id) => {
|
|
624
|
+
await this.restRequest("DELETE", `/api/v1/roles/${id}`, undefined, "roles.delete");
|
|
625
|
+
},
|
|
626
|
+
/** Assign a policy to a role */
|
|
627
|
+
assignPolicy: async (roleId, policyId) => {
|
|
628
|
+
await this.restRequest("POST", `/api/v1/roles/${roleId}/policies`, {
|
|
629
|
+
policy_id: policyId,
|
|
630
|
+
}, "roles.assignPolicy");
|
|
631
|
+
},
|
|
632
|
+
/** Remove a policy from a role */
|
|
633
|
+
removePolicy: async (roleId, policyId) => {
|
|
634
|
+
await this.restRequest("DELETE", `/api/v1/roles/${roleId}/policies/${policyId}`, undefined, "roles.removePolicy");
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Policy management (enterprise)
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```typescript
|
|
642
|
+
* const policy = await client.policies.create({
|
|
643
|
+
* name: "allow-emit",
|
|
644
|
+
* effect: "allow",
|
|
645
|
+
* actions: "emit:*",
|
|
646
|
+
* resources: "*",
|
|
647
|
+
* org_id: orgId,
|
|
648
|
+
* });
|
|
649
|
+
* const policies = await client.policies.list(orgId);
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
652
|
+
policies = {
|
|
653
|
+
/** Create a new policy */
|
|
654
|
+
create: async (input) => {
|
|
655
|
+
return this.restRequest("POST", "/api/v1/policies", input, "policies.create");
|
|
656
|
+
},
|
|
657
|
+
/** List policies, optionally filtered by organization */
|
|
658
|
+
list: async (orgId) => {
|
|
659
|
+
const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
|
|
660
|
+
return this.restRequest("GET", `/api/v1/policies${query}`, undefined, "policies.list");
|
|
661
|
+
},
|
|
662
|
+
/** Get a policy by ID */
|
|
663
|
+
get: async (id) => {
|
|
664
|
+
return this.restRequest("GET", `/api/v1/policies/${id}`, undefined, "policies.get");
|
|
665
|
+
},
|
|
666
|
+
/** Update a policy */
|
|
667
|
+
update: async (id, input) => {
|
|
668
|
+
return this.restRequest("PATCH", `/api/v1/policies/${id}`, input, "policies.update");
|
|
669
|
+
},
|
|
670
|
+
/** Delete a policy */
|
|
671
|
+
delete: async (id) => {
|
|
672
|
+
await this.restRequest("DELETE", `/api/v1/policies/${id}`, undefined, "policies.delete");
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
/**
|
|
676
|
+
* Projection management
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```typescript
|
|
680
|
+
* const state = await client.projections.get("order-summary");
|
|
681
|
+
* const statuses = await client.projections.list();
|
|
682
|
+
* await client.projections.rebuild("order-summary");
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
projections = {
|
|
686
|
+
/**
|
|
687
|
+
* Get the current materialized state of a projection.
|
|
688
|
+
*
|
|
689
|
+
* Returns a flat `ProjectionStateResult<TState>` (see `@ironflow/core`).
|
|
690
|
+
* The server returns a wrapped envelope and this method peels it via
|
|
691
|
+
* `peelProjectionEnvelope`. See issue #610 / CHANGELOG 0.20.0.
|
|
692
|
+
*
|
|
693
|
+
* For a freshly registered projection with no events applied, returns
|
|
694
|
+
* empty `state`, `lastEventTime: undefined`, `version: 0`.
|
|
695
|
+
*/
|
|
696
|
+
get: async (name, options) => {
|
|
697
|
+
// Normalize empty-string partition to undefined so the helper falls back
|
|
698
|
+
// to "__global__" instead of returning empty-string partition.
|
|
699
|
+
const partition = options?.partition ? options.partition : undefined;
|
|
700
|
+
const path = partition
|
|
701
|
+
? `/api/v1/projections/${encodeURIComponent(name)}?partition=${encodeURIComponent(partition)}`
|
|
702
|
+
: `/api/v1/projections/${encodeURIComponent(name)}`;
|
|
703
|
+
const raw = await this.restRequest("GET", path, undefined, "projections.get");
|
|
704
|
+
return peelProjectionEnvelope(raw, partition);
|
|
705
|
+
},
|
|
706
|
+
/** List all projection statuses */
|
|
707
|
+
list: async () => {
|
|
708
|
+
return this.restRequest("GET", "/api/v1/projections", undefined, "projections.list");
|
|
709
|
+
},
|
|
710
|
+
/** Get operational status for a projection */
|
|
711
|
+
getStatus: async (name) => {
|
|
712
|
+
return this.restRequest("GET", `/api/v1/projections/${encodeURIComponent(name)}/status`, undefined, "projections.getStatus");
|
|
713
|
+
},
|
|
714
|
+
/** Trigger a full rebuild of a projection */
|
|
715
|
+
rebuild: async (name) => {
|
|
716
|
+
return this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/rebuild`, undefined, "projections.rebuild");
|
|
717
|
+
},
|
|
718
|
+
/** Get the status of an in-progress or completed rebuild job */
|
|
719
|
+
getRebuildJob: async (name) => {
|
|
720
|
+
return this.restRequest("GET", `/api/v1/projections/${encodeURIComponent(name)}/rebuild`, undefined, "projections.getRebuildJob");
|
|
721
|
+
},
|
|
722
|
+
/** Delete a projection */
|
|
723
|
+
delete: async (name) => {
|
|
724
|
+
await this.restRequest("DELETE", `/api/v1/projections/${encodeURIComponent(name)}`, undefined, "projections.delete");
|
|
725
|
+
},
|
|
726
|
+
/** Pause a projection (stop consuming new events) */
|
|
727
|
+
pause: async (name) => {
|
|
728
|
+
await this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/pause`, undefined, "projections.pause");
|
|
729
|
+
},
|
|
730
|
+
/** Resume a paused projection */
|
|
731
|
+
resume: async (name) => {
|
|
732
|
+
await this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/resume`, undefined, "projections.resume");
|
|
733
|
+
},
|
|
734
|
+
/** Cancel an in-progress rebuild */
|
|
735
|
+
cancelRebuild: async (name) => {
|
|
736
|
+
await this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/cancel`, undefined, "projections.cancelRebuild");
|
|
737
|
+
},
|
|
738
|
+
/**
|
|
739
|
+
* Wait until the named projection has processed events up to `minSeq`,
|
|
740
|
+
* or the timeout elapses. Read-your-writes primitive for CQRS: pair
|
|
741
|
+
* with `sequence` from a `streams.append` response.
|
|
742
|
+
*
|
|
743
|
+
* ```typescript
|
|
744
|
+
* const { sequence } = await client.streams.append(orderId, event);
|
|
745
|
+
* await client.projections.waitForCatchup("order-detail-view", {
|
|
746
|
+
* minSeq: sequence,
|
|
747
|
+
* partition: orderId,
|
|
748
|
+
* timeoutMs: 5000,
|
|
749
|
+
* });
|
|
750
|
+
* ```
|
|
751
|
+
*
|
|
752
|
+
* Errors: 404 (projection not found), 409 (paused/rebuilding/partition
|
|
753
|
+
* unsupported for external), 429 (wait capacity exceeded).
|
|
754
|
+
*
|
|
755
|
+
* Issue #473.
|
|
756
|
+
*/
|
|
757
|
+
waitForCatchup: async (name, opts) => {
|
|
758
|
+
const params = new URLSearchParams();
|
|
759
|
+
params.set("minSeq", String(opts.minSeq));
|
|
760
|
+
if (opts.timeoutMs !== undefined) {
|
|
761
|
+
params.set("timeout", String(opts.timeoutMs));
|
|
762
|
+
}
|
|
763
|
+
if (opts.partition) {
|
|
764
|
+
params.set("partition", opts.partition);
|
|
765
|
+
}
|
|
766
|
+
return this.restRequest("GET", `/api/v1/projections/${encodeURIComponent(name)}/catchup?${params.toString()}`, undefined, "projections.waitForCatchup");
|
|
767
|
+
},
|
|
768
|
+
/**
|
|
769
|
+
* Wait on multiple projections in a single request. All items share
|
|
770
|
+
* a single timeout deadline and a single atomic slot reservation on
|
|
771
|
+
* the server — if the server's cap cannot absorb N items, the whole
|
|
772
|
+
* batch is rejected with 429. Per-item failures are returned per
|
|
773
|
+
* element via `error` fields.
|
|
774
|
+
*
|
|
775
|
+
* Max 16 items. Issue #473.
|
|
776
|
+
*/
|
|
777
|
+
waitForCatchupBatch: async (items, opts = {}) => {
|
|
778
|
+
// Always send minSeq as a string. uint64 sequences can exceed JS's
|
|
779
|
+
// safe-integer range (2^53-1); stringifying keeps the value exact
|
|
780
|
+
// across the JSON boundary and matches protojson's convention for
|
|
781
|
+
// 64-bit ints.
|
|
782
|
+
const body = {
|
|
783
|
+
items: items.map((i) => ({
|
|
784
|
+
name: i.name,
|
|
785
|
+
minSeq: String(i.minSeq),
|
|
786
|
+
...(i.partition ? { partition: i.partition } : {}),
|
|
787
|
+
})),
|
|
788
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
789
|
+
};
|
|
790
|
+
const resp = await this.restRequest("POST", "/api/v1/projections/catchup/batch", body, "projections.waitForCatchupBatch");
|
|
791
|
+
return resp.results ?? [];
|
|
792
|
+
},
|
|
793
|
+
/**
|
|
794
|
+
* Wait for a specific event (identified by `eventId` from a
|
|
795
|
+
* `streams.append` response) to be processed by the given projection.
|
|
796
|
+
* The server resolves eventId → NATS seq internally.
|
|
797
|
+
*
|
|
798
|
+
* Errors: 404 (event not found), 409 (event predates sequence
|
|
799
|
+
* tracking — fall back to waitForCatchup with minSeq from a
|
|
800
|
+
* fresh write), plus the standard wait errors.
|
|
801
|
+
*
|
|
802
|
+
* Issue #473.
|
|
803
|
+
*/
|
|
804
|
+
waitForEvent: async (eventId, projection, opts = {}) => {
|
|
805
|
+
const body = {
|
|
806
|
+
eventId,
|
|
807
|
+
projection,
|
|
808
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
809
|
+
...(opts.partition ? { partition: opts.partition } : {}),
|
|
810
|
+
};
|
|
811
|
+
return this.restRequest("POST", "/api/v1/projections/wait-for-event", body, "projections.waitForEvent");
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* Secrets management
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* ```typescript
|
|
819
|
+
* await client.secrets.set("stripe-key", "sk_live_...");
|
|
820
|
+
* const secret = await client.secrets.get("stripe-key");
|
|
821
|
+
* const all = await client.secrets.list();
|
|
822
|
+
* await client.secrets.delete("stripe-key");
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
secrets = {
|
|
826
|
+
/** Get a secret by name (returns value) */
|
|
827
|
+
get: async (name) => {
|
|
828
|
+
return this.restRequest("GET", `/api/v1/secrets/${encodeURIComponent(name)}`, undefined, "secrets.get");
|
|
829
|
+
},
|
|
830
|
+
/** Create a new secret */
|
|
831
|
+
set: async (name, value) => {
|
|
832
|
+
return this.restRequest("POST", "/api/v1/secrets", { name, value }, "secrets.set");
|
|
833
|
+
},
|
|
834
|
+
/** Update an existing secret's value */
|
|
835
|
+
update: async (name, value) => {
|
|
836
|
+
return this.restRequest("PUT", `/api/v1/secrets/${encodeURIComponent(name)}`, { value }, "secrets.update");
|
|
837
|
+
},
|
|
838
|
+
/** List all secrets (names only, no values) */
|
|
839
|
+
list: async () => {
|
|
840
|
+
return this.restRequest("GET", "/api/v1/secrets", undefined, "secrets.list");
|
|
841
|
+
},
|
|
842
|
+
/** Delete a secret */
|
|
843
|
+
delete: async (name) => {
|
|
844
|
+
await this.restRequest("DELETE", `/api/v1/secrets/${encodeURIComponent(name)}`, undefined, "secrets.delete");
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* Project management
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```typescript
|
|
852
|
+
* const project = await client.projects.create({ name: "my-service" });
|
|
853
|
+
* const projects = await client.projects.list();
|
|
854
|
+
* await client.projects.update(project.id, { name: "renamed-service" });
|
|
855
|
+
* await client.projects.delete(project.id);
|
|
856
|
+
* ```
|
|
857
|
+
*/
|
|
858
|
+
projects = {
|
|
859
|
+
/** List all projects */
|
|
860
|
+
list: async () => {
|
|
861
|
+
return this.restRequest("GET", "/api/v1/projects", undefined, "projects.list");
|
|
862
|
+
},
|
|
863
|
+
/** Create a new project */
|
|
864
|
+
create: async (input) => {
|
|
865
|
+
return this.restRequest("POST", "/api/v1/projects", input, "projects.create");
|
|
866
|
+
},
|
|
867
|
+
/** Update a project */
|
|
868
|
+
update: async (id, input) => {
|
|
869
|
+
return this.restRequest("PUT", `/api/v1/projects/${encodeURIComponent(id)}`, input, "projects.update");
|
|
870
|
+
},
|
|
871
|
+
/** Delete a project */
|
|
872
|
+
delete: async (id) => {
|
|
873
|
+
await this.restRequest("DELETE", `/api/v1/projects/${encodeURIComponent(id)}`, undefined, "projects.delete");
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
/**
|
|
877
|
+
* Environment management
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* ```typescript
|
|
881
|
+
* const env = await client.environments.create({ name: "staging", projectId: "proj_..." });
|
|
882
|
+
* const envs = await client.environments.list();
|
|
883
|
+
* await client.environments.update(env.id, { name: "staging-v2" });
|
|
884
|
+
* await client.environments.delete(env.id);
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
environments = {
|
|
888
|
+
/** List all environments */
|
|
889
|
+
list: async () => {
|
|
890
|
+
return this.restRequest("GET", "/api/v1/environments", undefined, "environments.list");
|
|
891
|
+
},
|
|
892
|
+
/** Create a new environment */
|
|
893
|
+
create: async (input) => {
|
|
894
|
+
return this.restRequest("POST", "/api/v1/environments", input, "environments.create");
|
|
895
|
+
},
|
|
896
|
+
/** Update an environment */
|
|
897
|
+
update: async (id, input) => {
|
|
898
|
+
return this.restRequest("PUT", `/api/v1/environments/${encodeURIComponent(id)}`, input, "environments.update");
|
|
899
|
+
},
|
|
900
|
+
/** Delete an environment */
|
|
901
|
+
delete: async (id) => {
|
|
902
|
+
await this.restRequest("DELETE", `/api/v1/environments/${encodeURIComponent(id)}`, undefined, "environments.delete");
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
/**
|
|
906
|
+
* Event schema registry operations
|
|
907
|
+
*
|
|
908
|
+
* @example
|
|
909
|
+
* ```typescript
|
|
910
|
+
* // Register a schema
|
|
911
|
+
* const schema = await client.schemas.register({
|
|
912
|
+
* name: "order.placed",
|
|
913
|
+
* version: 1,
|
|
914
|
+
* schema: { type: "object", properties: { orderId: { type: "string" } } },
|
|
915
|
+
* });
|
|
916
|
+
*
|
|
917
|
+
* // List all schemas
|
|
918
|
+
* const schemas = await client.schemas.list();
|
|
919
|
+
*
|
|
920
|
+
* // Get latest version of a schema
|
|
921
|
+
* const latest = await client.schemas.get("order.placed");
|
|
922
|
+
*
|
|
923
|
+
* // Get a specific version
|
|
924
|
+
* const v1 = await client.schemas.getVersion("order.placed", 1);
|
|
925
|
+
*
|
|
926
|
+
* // Test an upcast transformation
|
|
927
|
+
* const result = await client.schemas.testUpcast({
|
|
928
|
+
* eventName: "order.placed",
|
|
929
|
+
* fromVersion: 1,
|
|
930
|
+
* toVersion: 2,
|
|
931
|
+
* data: { orderId: "123" },
|
|
932
|
+
* });
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
schemas = {
|
|
936
|
+
/** Register a new event schema (or a new version of an existing schema) */
|
|
937
|
+
register: async (input) => {
|
|
938
|
+
return this.restRequest("POST", "/api/v1/events/schemas", {
|
|
939
|
+
event_name: input.name,
|
|
940
|
+
version: input.version,
|
|
941
|
+
schema_json: JSON.stringify(input.schema),
|
|
942
|
+
}, "schemas.register");
|
|
943
|
+
},
|
|
944
|
+
/** List all registered event schemas */
|
|
945
|
+
list: async () => {
|
|
946
|
+
const resp = await this.restRequest("GET", "/api/v1/events/schemas", undefined, "schemas.list");
|
|
947
|
+
return resp.schemas ?? [];
|
|
948
|
+
},
|
|
949
|
+
/** Get the latest version of an event schema by name */
|
|
950
|
+
get: async (name) => {
|
|
951
|
+
return this.restRequest("GET", `/api/v1/events/schemas/${encodeURIComponent(name)}`, undefined, "schemas.get");
|
|
952
|
+
},
|
|
953
|
+
/** Get a specific version of an event schema */
|
|
954
|
+
getVersion: async (name, version) => {
|
|
955
|
+
return this.restRequest("GET", `/api/v1/events/schemas/${encodeURIComponent(name)}/${version}`, undefined, "schemas.getVersion");
|
|
956
|
+
},
|
|
957
|
+
/** Delete a specific version of an event schema */
|
|
958
|
+
delete: async (name, version) => {
|
|
959
|
+
await this.restRequest("DELETE", `/api/v1/events/schemas/${encodeURIComponent(name)}/${version}`, undefined, "schemas.delete");
|
|
960
|
+
},
|
|
961
|
+
/** Test an upcast transformation between two schema versions */
|
|
962
|
+
testUpcast: async (input) => {
|
|
963
|
+
return this.restRequest("POST", "/api/v1/events/upcast", input, "schemas.testUpcast");
|
|
964
|
+
},
|
|
965
|
+
};
|
|
966
|
+
/**
|
|
967
|
+
* Get the reconstructed state of a run at a specific point in time.
|
|
968
|
+
*
|
|
969
|
+
* @param runId The run ID to query
|
|
970
|
+
* @param timestamp The point in time to reconstruct state at
|
|
971
|
+
*/
|
|
972
|
+
async getRunStateAt(runId, timestamp) {
|
|
973
|
+
return this.request("/ironflow.v1.TimeTravelService/GetRunStateAt", { run_id: runId, timestamp: timestamp.toISOString() }, "getRunStateAt");
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Get the timeline of events for a run (for time-travel debugging).
|
|
977
|
+
*
|
|
978
|
+
* @param runId The run ID to query
|
|
979
|
+
*/
|
|
980
|
+
async getRunTimeline(runId) {
|
|
981
|
+
const response = await this.request("/ironflow.v1.TimeTravelService/GetRunTimeline", { run_id: runId }, "getRunTimeline");
|
|
982
|
+
return response.events ?? [];
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Get the output of a specific step at a point in time.
|
|
986
|
+
*
|
|
987
|
+
* @param runId The run ID
|
|
988
|
+
* @param stepId The step ID
|
|
989
|
+
* @param timestamp The point in time to query
|
|
990
|
+
*/
|
|
991
|
+
async getStepOutputAt(runId, stepId, timestamp) {
|
|
992
|
+
return this.request("/ironflow.v1.TimeTravelService/GetStepOutputAt", { run_id: runId, step_id: stepId, timestamp: timestamp.toISOString() }, "getStepOutputAt");
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Get the audit trail for a run.
|
|
996
|
+
*
|
|
997
|
+
* @param runId The run ID to retrieve the audit trail for
|
|
998
|
+
*/
|
|
999
|
+
async getAuditTrail(runId) {
|
|
1000
|
+
const response = await this.request("/ironflow.v1.AuditService/GetAuditTrail", { run_id: runId }, "getAuditTrail");
|
|
1001
|
+
return response.entries ?? [];
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Webhook management operations
|
|
1005
|
+
*
|
|
1006
|
+
* @example
|
|
1007
|
+
* ```typescript
|
|
1008
|
+
* // List all webhook sources
|
|
1009
|
+
* const sources = await client.webhooks.listSources();
|
|
1010
|
+
*
|
|
1011
|
+
* // Delete a webhook source
|
|
1012
|
+
* await client.webhooks.deleteSource("my-webhook");
|
|
1013
|
+
*
|
|
1014
|
+
* // List deliveries for a source
|
|
1015
|
+
* const { deliveries } = await client.webhooks.listDeliveries({ sourceId: "my-webhook" });
|
|
1016
|
+
* ```
|
|
1017
|
+
*/
|
|
1018
|
+
webhooks = {
|
|
1019
|
+
/** Create a new webhook source */
|
|
1020
|
+
create: async (input) => {
|
|
1021
|
+
const response = await this.request("/ironflow.v1.WebhookService/CreateWebhookSource", {
|
|
1022
|
+
id: input.id,
|
|
1023
|
+
event_prefix: input.eventPrefix,
|
|
1024
|
+
verify_header: input.verifyHeader ?? "",
|
|
1025
|
+
verify_algorithm: input.verifyAlgorithm ?? "",
|
|
1026
|
+
verify_secret: input.verifySecret ?? "",
|
|
1027
|
+
metadata: input.metadata,
|
|
1028
|
+
}, "webhooks.create");
|
|
1029
|
+
return {
|
|
1030
|
+
id: response.id,
|
|
1031
|
+
eventPrefix: response.eventPrefix,
|
|
1032
|
+
verifyHeader: response.verifyHeader,
|
|
1033
|
+
verifyAlgorithm: response.verifyAlgorithm,
|
|
1034
|
+
sourceType: response.sourceType,
|
|
1035
|
+
metadata: response.metadata,
|
|
1036
|
+
createdAt: response.createdAt,
|
|
1037
|
+
updatedAt: response.updatedAt,
|
|
1038
|
+
};
|
|
1039
|
+
},
|
|
1040
|
+
/** List all registered webhook sources */
|
|
1041
|
+
listSources: async () => {
|
|
1042
|
+
const response = await this.request("/ironflow.v1.WebhookService/ListWebhookSources", { limit: 0, offset: 0 }, "webhooks.listSources");
|
|
1043
|
+
return (response.sources ?? []).map((s) => ({
|
|
1044
|
+
id: s.id,
|
|
1045
|
+
eventPrefix: s.eventPrefix,
|
|
1046
|
+
verifyHeader: s.verifyHeader,
|
|
1047
|
+
verifyAlgorithm: s.verifyAlgorithm,
|
|
1048
|
+
sourceType: s.sourceType,
|
|
1049
|
+
metadata: s.metadata,
|
|
1050
|
+
createdAt: s.createdAt,
|
|
1051
|
+
updatedAt: s.updatedAt,
|
|
1052
|
+
}));
|
|
1053
|
+
},
|
|
1054
|
+
/** Delete a webhook source by ID */
|
|
1055
|
+
deleteSource: async (id) => {
|
|
1056
|
+
await this.request("/ironflow.v1.WebhookService/DeleteWebhookSource", { id }, "webhooks.deleteSource");
|
|
1057
|
+
},
|
|
1058
|
+
/** List webhook deliveries with optional filtering */
|
|
1059
|
+
listDeliveries: async (opts) => {
|
|
1060
|
+
const response = await this.request("/ironflow.v1.WebhookService/ListWebhookDeliveries", {
|
|
1061
|
+
source_id: opts?.sourceId ?? "",
|
|
1062
|
+
status: opts?.status ?? "",
|
|
1063
|
+
limit: opts?.limit ?? 0,
|
|
1064
|
+
offset: opts?.offset ?? 0,
|
|
1065
|
+
}, "webhooks.listDeliveries");
|
|
1066
|
+
return {
|
|
1067
|
+
deliveries: (response.deliveries ?? []).map((d) => ({
|
|
1068
|
+
id: d.id,
|
|
1069
|
+
sourceId: d.sourceId,
|
|
1070
|
+
externalId: d.externalId,
|
|
1071
|
+
status: d.status,
|
|
1072
|
+
eventId: d.eventId,
|
|
1073
|
+
error: d.error,
|
|
1074
|
+
createdAt: d.createdAt,
|
|
1075
|
+
})),
|
|
1076
|
+
totalCount: response.totalCount ?? 0,
|
|
1077
|
+
};
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
/**
|
|
1081
|
+
* User management operations
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* ```typescript
|
|
1085
|
+
* // Create a user
|
|
1086
|
+
* const user = await client.users.create({ email: "alice@example.com", password: "secret", roles: ["admin"] });
|
|
1087
|
+
*
|
|
1088
|
+
* // List users
|
|
1089
|
+
* const users = await client.users.list();
|
|
1090
|
+
*
|
|
1091
|
+
* // Update a user
|
|
1092
|
+
* await client.users.update(user.id, { name: "Alice" });
|
|
1093
|
+
*
|
|
1094
|
+
* // Delete a user
|
|
1095
|
+
* await client.users.delete(user.id);
|
|
1096
|
+
* ```
|
|
1097
|
+
*/
|
|
1098
|
+
users = {
|
|
1099
|
+
/** Create a new user (admin only) */
|
|
1100
|
+
create: async (input) => {
|
|
1101
|
+
return this.restRequest("POST", "/api/v1/users", input, "users.create");
|
|
1102
|
+
},
|
|
1103
|
+
/** List all users in the current organization (admin only) */
|
|
1104
|
+
list: async () => {
|
|
1105
|
+
return this.restRequest("GET", "/api/v1/users", undefined, "users.list");
|
|
1106
|
+
},
|
|
1107
|
+
/** Get a user by ID */
|
|
1108
|
+
get: async (id) => {
|
|
1109
|
+
return this.restRequest("GET", `/api/v1/users/${encodeURIComponent(id)}`, undefined, "users.get");
|
|
1110
|
+
},
|
|
1111
|
+
/** Update a user's profile (admin only) */
|
|
1112
|
+
update: async (id, input) => {
|
|
1113
|
+
return this.restRequest("PATCH", `/api/v1/users/${encodeURIComponent(id)}`, input, "users.update");
|
|
1114
|
+
},
|
|
1115
|
+
/** Delete a user (admin only) */
|
|
1116
|
+
delete: async (id) => {
|
|
1117
|
+
await this.restRequest("DELETE", `/api/v1/users/${encodeURIComponent(id)}`, undefined, "users.delete");
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
/**
|
|
1121
|
+
* Tenant management operations (enterprise-only)
|
|
1122
|
+
*
|
|
1123
|
+
* @example
|
|
1124
|
+
* ```typescript
|
|
1125
|
+
* // List all tenants
|
|
1126
|
+
* const tenants = await client.tenants.list();
|
|
1127
|
+
* console.log(tenants.map(t => t.name));
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
tenants = {
|
|
1131
|
+
/** List all tenants (enterprise-only) */
|
|
1132
|
+
list: async () => {
|
|
1133
|
+
return this.restRequest("GET", "/api/v1/tenants", undefined, "tenants.list");
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
/**
|
|
1137
|
+
* KV store operations
|
|
1138
|
+
*
|
|
1139
|
+
* @example
|
|
1140
|
+
* ```typescript
|
|
1141
|
+
* const kv = client.kv();
|
|
1142
|
+
* const bucket = await kv.createBucket({ name: "sessions", ttlSeconds: 3600 });
|
|
1143
|
+
* const handle = kv.bucket("sessions");
|
|
1144
|
+
* const { revision } = await handle.put("user.123", { token: "abc" });
|
|
1145
|
+
* const entry = await handle.get("user.123");
|
|
1146
|
+
* ```
|
|
1147
|
+
*/
|
|
1148
|
+
kv() {
|
|
1149
|
+
return new KVClient({
|
|
1150
|
+
serverUrl: this.serverUrl,
|
|
1151
|
+
apiKey: this.apiKey,
|
|
1152
|
+
timeout: this.timeout,
|
|
1153
|
+
onError: this.onErrorHandler,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Create a CommandDedup instance for atomic command-level idempotency.
|
|
1158
|
+
*
|
|
1159
|
+
* Uses the claim-first pattern backed by NATS KV. The KV bucket is created
|
|
1160
|
+
* lazily on the first operation. Store the returned instance and reuse it
|
|
1161
|
+
* across requests — do not call commandDedup() per request.
|
|
1162
|
+
*
|
|
1163
|
+
* @example
|
|
1164
|
+
* ```typescript
|
|
1165
|
+
* const dedup = client.commandDedup<OrderResult>("order-commands");
|
|
1166
|
+
* const prior = await dedup.tryClaim(commandId, { orderId, claimedAt: new Date().toISOString() });
|
|
1167
|
+
* if (prior !== null) return prior;
|
|
1168
|
+
* try {
|
|
1169
|
+
* const result = await runOrderHandler();
|
|
1170
|
+
* await dedup.finalize(commandId, result);
|
|
1171
|
+
* return result;
|
|
1172
|
+
* } catch (err) {
|
|
1173
|
+
* await dedup.release(commandId).catch(() => {}); // swallow — don't mask the original error
|
|
1174
|
+
* throw err;
|
|
1175
|
+
* }
|
|
1176
|
+
* ```
|
|
1177
|
+
*/
|
|
1178
|
+
commandDedup(bucketName, options) {
|
|
1179
|
+
return new CommandDedup(this.kv(), bucketName, options?.ttlSeconds);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Config management operations
|
|
1183
|
+
*
|
|
1184
|
+
* @example
|
|
1185
|
+
* ```typescript
|
|
1186
|
+
* const config = client.config();
|
|
1187
|
+
* await config.set("app", { featureX: true });
|
|
1188
|
+
* const { data } = await config.get("app");
|
|
1189
|
+
* await config.patch("app", { maxRetries: 5 });
|
|
1190
|
+
* const configs = await config.list();
|
|
1191
|
+
* await config.delete("app");
|
|
1192
|
+
* ```
|
|
1193
|
+
*/
|
|
1194
|
+
config() {
|
|
1195
|
+
return new ConfigClient({
|
|
1196
|
+
serverUrl: this.serverUrl,
|
|
1197
|
+
apiKey: this.apiKey,
|
|
1198
|
+
timeout: this.timeout,
|
|
1199
|
+
onError: this.onErrorHandler,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Patch a step's output (hot patching)
|
|
1204
|
+
*/
|
|
1205
|
+
async patchStep(stepId, output, reason) {
|
|
1206
|
+
const endpoint = "/api/v1/steps/patch";
|
|
1207
|
+
const url = `${this.serverUrl}${endpoint}`;
|
|
1208
|
+
const headers = {
|
|
1209
|
+
"Content-Type": "application/json",
|
|
1210
|
+
};
|
|
1211
|
+
if (this.apiKey) {
|
|
1212
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1213
|
+
}
|
|
1214
|
+
const controller = new AbortController();
|
|
1215
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1216
|
+
let status;
|
|
1217
|
+
try {
|
|
1218
|
+
const response = await fetch(url, {
|
|
1219
|
+
method: "POST",
|
|
1220
|
+
headers,
|
|
1221
|
+
body: JSON.stringify({ step_id: stepId, output, reason: reason || "" }),
|
|
1222
|
+
signal: controller.signal,
|
|
1223
|
+
});
|
|
1224
|
+
status = response.status;
|
|
1225
|
+
if (!response.ok) {
|
|
1226
|
+
const errorBody = await response.text();
|
|
1227
|
+
throw new Error(errorBody || `Patch step failed: ${response.status}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
await this.callOnError(error, { method: "patchStep", endpoint, statusCode: status });
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
finally {
|
|
1235
|
+
clearTimeout(timeoutId);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Resume a paused or failed run
|
|
1240
|
+
*/
|
|
1241
|
+
async resumeRun(runId, fromStep) {
|
|
1242
|
+
const endpoint = "/api/v1/runs/resume";
|
|
1243
|
+
const url = `${this.serverUrl}${endpoint}`;
|
|
1244
|
+
const headers = {
|
|
1245
|
+
"Content-Type": "application/json",
|
|
1246
|
+
};
|
|
1247
|
+
if (this.apiKey) {
|
|
1248
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1249
|
+
}
|
|
1250
|
+
const controller = new AbortController();
|
|
1251
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1252
|
+
let status;
|
|
1253
|
+
try {
|
|
1254
|
+
const response = await fetch(url, {
|
|
1255
|
+
method: "POST",
|
|
1256
|
+
headers,
|
|
1257
|
+
body: JSON.stringify({ run_id: runId, from_step: fromStep || "" }),
|
|
1258
|
+
signal: controller.signal,
|
|
1259
|
+
});
|
|
1260
|
+
status = response.status;
|
|
1261
|
+
if (!response.ok) {
|
|
1262
|
+
const errorBody = await response.text();
|
|
1263
|
+
throw new Error(errorBody || `Resume run failed: ${response.status}`);
|
|
1264
|
+
}
|
|
1265
|
+
return response.json();
|
|
1266
|
+
}
|
|
1267
|
+
catch (error) {
|
|
1268
|
+
await this.callOnError(error, { method: "resumeRun", endpoint, statusCode: status });
|
|
1269
|
+
throw error;
|
|
1270
|
+
}
|
|
1271
|
+
finally {
|
|
1272
|
+
clearTimeout(timeoutId);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Pause a running workflow run (scoped injection).
|
|
1277
|
+
*
|
|
1278
|
+
* @example
|
|
1279
|
+
* ```typescript
|
|
1280
|
+
* const result = await client.pauseRun("run_abc123");
|
|
1281
|
+
* console.log(result.status); // "paused"
|
|
1282
|
+
* ```
|
|
1283
|
+
*/
|
|
1284
|
+
async pauseRun(runId) {
|
|
1285
|
+
return this.request("/ironflow.v1.IronflowService/PauseRun", { run_id: runId }, "pauseRun");
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Get the paused state of a run, including completed steps and next step hint.
|
|
1289
|
+
*
|
|
1290
|
+
* @example
|
|
1291
|
+
* ```typescript
|
|
1292
|
+
* const state = await client.getPausedState("run_abc123");
|
|
1293
|
+
* for (const step of state.steps) {
|
|
1294
|
+
* console.log(step.name, step.output, step.injected);
|
|
1295
|
+
* }
|
|
1296
|
+
* console.log("Next step:", state.nextStepHint);
|
|
1297
|
+
* ```
|
|
1298
|
+
*/
|
|
1299
|
+
async getPausedState(runId) {
|
|
1300
|
+
const response = await this.request("/ironflow.v1.IronflowService/GetPausedState", { run_id: runId }, "getPausedState");
|
|
1301
|
+
return {
|
|
1302
|
+
steps: (response.steps || []).map((s) => ({
|
|
1303
|
+
id: s.id,
|
|
1304
|
+
name: s.name,
|
|
1305
|
+
output: s.output ? JSON.parse(s.output) : null,
|
|
1306
|
+
injected: s.injected,
|
|
1307
|
+
completedAt: s.completedAt,
|
|
1308
|
+
})),
|
|
1309
|
+
nextStepHint: response.nextStepHint,
|
|
1310
|
+
pauseReason: response.pauseReason,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Inject new output for a step in a paused run (scoped injection).
|
|
1315
|
+
*
|
|
1316
|
+
* @example
|
|
1317
|
+
* ```typescript
|
|
1318
|
+
* const result = await client.injectStepOutput(
|
|
1319
|
+
* "run_abc123",
|
|
1320
|
+
* "step_xyz",
|
|
1321
|
+
* { corrected: true },
|
|
1322
|
+
* "Manual correction"
|
|
1323
|
+
* );
|
|
1324
|
+
* console.log("Previous output:", result.previousOutput);
|
|
1325
|
+
* ```
|
|
1326
|
+
*/
|
|
1327
|
+
async injectStepOutput(runId, stepId, newOutput, reason) {
|
|
1328
|
+
const response = await this.request("/ironflow.v1.IronflowService/InjectStepOutput", {
|
|
1329
|
+
run_id: runId,
|
|
1330
|
+
step_id: stepId,
|
|
1331
|
+
new_output: JSON.stringify(newOutput),
|
|
1332
|
+
reason: reason ?? "",
|
|
1333
|
+
}, "injectStepOutput");
|
|
1334
|
+
return {
|
|
1335
|
+
stepId: response.stepId,
|
|
1336
|
+
previousOutput: response.previousOutput
|
|
1337
|
+
? JSON.parse(response.previousOutput)
|
|
1338
|
+
: null,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* List registered functions
|
|
1343
|
+
*/
|
|
1344
|
+
async listFunctions() {
|
|
1345
|
+
const endpoint = "/api/v1/functions";
|
|
1346
|
+
const url = `${this.serverUrl}${endpoint}`;
|
|
1347
|
+
const headers = {};
|
|
1348
|
+
if (this.apiKey) {
|
|
1349
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1350
|
+
}
|
|
1351
|
+
const controller = new AbortController();
|
|
1352
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1353
|
+
let status;
|
|
1354
|
+
try {
|
|
1355
|
+
const response = await fetch(url, {
|
|
1356
|
+
method: "GET",
|
|
1357
|
+
headers,
|
|
1358
|
+
signal: controller.signal,
|
|
1359
|
+
});
|
|
1360
|
+
status = response.status;
|
|
1361
|
+
if (!response.ok) {
|
|
1362
|
+
throw new Error(`List functions failed: ${response.status}`);
|
|
1363
|
+
}
|
|
1364
|
+
const data = (await response.json());
|
|
1365
|
+
return data.functions || [];
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
await this.callOnError(error, { method: "listFunctions", endpoint, statusCode: status });
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
finally {
|
|
1372
|
+
clearTimeout(timeoutId);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* List connected workers
|
|
1377
|
+
*/
|
|
1378
|
+
async listWorkers() {
|
|
1379
|
+
const endpoint = "/api/v1/workers";
|
|
1380
|
+
const url = `${this.serverUrl}${endpoint}`;
|
|
1381
|
+
const headers = {};
|
|
1382
|
+
if (this.apiKey) {
|
|
1383
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1384
|
+
}
|
|
1385
|
+
const controller = new AbortController();
|
|
1386
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1387
|
+
let status;
|
|
1388
|
+
try {
|
|
1389
|
+
const response = await fetch(url, {
|
|
1390
|
+
method: "GET",
|
|
1391
|
+
headers,
|
|
1392
|
+
signal: controller.signal,
|
|
1393
|
+
});
|
|
1394
|
+
status = response.status;
|
|
1395
|
+
if (!response.ok) {
|
|
1396
|
+
throw new Error(`List workers failed: ${response.status}`);
|
|
1397
|
+
}
|
|
1398
|
+
const data = (await response.json());
|
|
1399
|
+
return data.workers || [];
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) {
|
|
1402
|
+
await this.callOnError(error, { method: "listWorkers", endpoint, statusCode: status });
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
finally {
|
|
1406
|
+
clearTimeout(timeoutId);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Make an HTTP request to the server
|
|
1411
|
+
*/
|
|
1412
|
+
async request(endpoint, body, method) {
|
|
1413
|
+
const url = `${this.serverUrl}${endpoint}`;
|
|
1414
|
+
const headers = {
|
|
1415
|
+
"Content-Type": "application/json",
|
|
1416
|
+
};
|
|
1417
|
+
if (this.apiKey) {
|
|
1418
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1419
|
+
}
|
|
1420
|
+
const runId = getCurrentRunId();
|
|
1421
|
+
if (runId) {
|
|
1422
|
+
headers["X-Ironflow-Run-ID"] = runId;
|
|
1423
|
+
}
|
|
1424
|
+
const controller = new AbortController();
|
|
1425
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1426
|
+
let status;
|
|
1427
|
+
try {
|
|
1428
|
+
const response = await fetch(url, {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
headers,
|
|
1431
|
+
body: JSON.stringify(body),
|
|
1432
|
+
signal: controller.signal,
|
|
1433
|
+
});
|
|
1434
|
+
status = response.status;
|
|
1435
|
+
if (!response.ok) {
|
|
1436
|
+
const errorBody = await response.text();
|
|
1437
|
+
let errorMessage = `Request failed with status ${response.status}`;
|
|
1438
|
+
if (errorBody) {
|
|
1439
|
+
try {
|
|
1440
|
+
const errorJson = JSON.parse(errorBody);
|
|
1441
|
+
if (errorJson.message) {
|
|
1442
|
+
errorMessage = errorJson.message;
|
|
1443
|
+
}
|
|
1444
|
+
else if (errorJson.code) {
|
|
1445
|
+
errorMessage = `Error code: ${errorJson.code}`;
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
errorMessage = errorBody;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
// Not a JSON response, use raw text.
|
|
1453
|
+
errorMessage = errorBody;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
this.throwTypedError(response.status, errorMessage);
|
|
1457
|
+
}
|
|
1458
|
+
return response.json();
|
|
1459
|
+
}
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
if (method) {
|
|
1462
|
+
await this.callOnError(error, { method, endpoint, statusCode: status });
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
1466
|
+
finally {
|
|
1467
|
+
clearTimeout(timeoutId);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Throw a typed error based on HTTP status code.
|
|
1472
|
+
*/
|
|
1473
|
+
throwTypedError(status, message) {
|
|
1474
|
+
switch (status) {
|
|
1475
|
+
case 401:
|
|
1476
|
+
throw new UnauthenticatedError(message);
|
|
1477
|
+
case 402:
|
|
1478
|
+
throw new EnterpriseRequiredError(message);
|
|
1479
|
+
case 403:
|
|
1480
|
+
throw new UnauthorizedError(message);
|
|
1481
|
+
default:
|
|
1482
|
+
throw new IronflowError(message);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Make a REST HTTP request to the server (supports GET, POST, PATCH, DELETE)
|
|
1487
|
+
*/
|
|
1488
|
+
async restRequest(httpMethod, path, body, method) {
|
|
1489
|
+
const url = `${this.serverUrl}${path}`;
|
|
1490
|
+
const headers = {};
|
|
1491
|
+
if (this.apiKey) {
|
|
1492
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1493
|
+
}
|
|
1494
|
+
const options = { method: httpMethod, headers };
|
|
1495
|
+
if (body && (httpMethod === "POST" || httpMethod === "PATCH" || httpMethod === "PUT")) {
|
|
1496
|
+
headers["Content-Type"] = "application/json";
|
|
1497
|
+
options.body = JSON.stringify(body);
|
|
1498
|
+
}
|
|
1499
|
+
const controller = new AbortController();
|
|
1500
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1501
|
+
let status;
|
|
1502
|
+
try {
|
|
1503
|
+
const response = await fetch(url, {
|
|
1504
|
+
...options,
|
|
1505
|
+
signal: controller.signal,
|
|
1506
|
+
});
|
|
1507
|
+
status = response.status;
|
|
1508
|
+
if (!response.ok) {
|
|
1509
|
+
const errBody = await response
|
|
1510
|
+
.json()
|
|
1511
|
+
.catch(() => ({ error: response.statusText }));
|
|
1512
|
+
const message = errBody.error ||
|
|
1513
|
+
errBody.message ||
|
|
1514
|
+
response.statusText;
|
|
1515
|
+
this.throwTypedError(response.status, message);
|
|
1516
|
+
}
|
|
1517
|
+
if (response.status === 204)
|
|
1518
|
+
return undefined;
|
|
1519
|
+
return response.json();
|
|
1520
|
+
}
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
if (method) {
|
|
1523
|
+
await this.callOnError(error, { method, endpoint: path, statusCode: status });
|
|
1524
|
+
}
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
finally {
|
|
1528
|
+
clearTimeout(timeoutId);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Call the global onError handler if registered.
|
|
1533
|
+
* Swallows any errors thrown by the callback.
|
|
1534
|
+
*/
|
|
1535
|
+
async callOnError(error, context) {
|
|
1536
|
+
if (!this.onErrorHandler)
|
|
1537
|
+
return;
|
|
1538
|
+
try {
|
|
1539
|
+
await this.onErrorHandler(error, context);
|
|
1540
|
+
}
|
|
1541
|
+
catch (callbackError) {
|
|
1542
|
+
console.error("[ironflow] onError callback threw:", callbackError);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Create a new Ironflow client
|
|
1548
|
+
*
|
|
1549
|
+
* @example
|
|
1550
|
+
* ```typescript
|
|
1551
|
+
* const client = createClient({ serverUrl: "http://localhost:9123" });
|
|
1552
|
+
* ```
|
|
1553
|
+
*/
|
|
1554
|
+
export function createClient(config) {
|
|
1555
|
+
return new IronflowClient(config);
|
|
1556
|
+
}
|
|
1557
|
+
//# sourceMappingURL=client.js.map
|