@ironflow/browser 0.19.3 → 0.20.2

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.
Files changed (47) hide show
  1. package/LICENSE +181 -0
  2. package/README.md +108 -6
  3. package/package.json +15 -15
  4. package/dist/client.d.ts +0 -822
  5. package/dist/client.d.ts.map +0 -1
  6. package/dist/client.js +0 -2220
  7. package/dist/client.js.map +0 -1
  8. package/dist/config-client.d.ts +0 -43
  9. package/dist/config-client.d.ts.map +0 -1
  10. package/dist/config-client.js +0 -134
  11. package/dist/config-client.js.map +0 -1
  12. package/dist/config.d.ts +0 -91
  13. package/dist/config.d.ts.map +0 -1
  14. package/dist/config.js +0 -55
  15. package/dist/config.js.map +0 -1
  16. package/dist/index.d.ts +0 -52
  17. package/dist/index.d.ts.map +0 -1
  18. package/dist/index.js +0 -53
  19. package/dist/index.js.map +0 -1
  20. package/dist/kv.d.ts +0 -79
  21. package/dist/kv.d.ts.map +0 -1
  22. package/dist/kv.js +0 -289
  23. package/dist/kv.js.map +0 -1
  24. package/dist/projection-stream.d.ts +0 -42
  25. package/dist/projection-stream.d.ts.map +0 -1
  26. package/dist/projection-stream.js +0 -48
  27. package/dist/projection-stream.js.map +0 -1
  28. package/dist/subscription.d.ts +0 -101
  29. package/dist/subscription.d.ts.map +0 -1
  30. package/dist/subscription.js +0 -442
  31. package/dist/subscription.js.map +0 -1
  32. package/dist/transport/connectrpc.d.ts +0 -74
  33. package/dist/transport/connectrpc.d.ts.map +0 -1
  34. package/dist/transport/connectrpc.js +0 -393
  35. package/dist/transport/connectrpc.js.map +0 -1
  36. package/dist/transport/index.d.ts +0 -7
  37. package/dist/transport/index.d.ts.map +0 -1
  38. package/dist/transport/index.js +0 -6
  39. package/dist/transport/index.js.map +0 -1
  40. package/dist/transport/types.d.ts +0 -69
  41. package/dist/transport/types.d.ts.map +0 -1
  42. package/dist/transport/types.js +0 -5
  43. package/dist/transport/types.js.map +0 -1
  44. package/dist/transport/websocket.d.ts +0 -38
  45. package/dist/transport/websocket.d.ts.map +0 -1
  46. package/dist/transport/websocket.js +0 -276
  47. package/dist/transport/websocket.js.map +0 -1
package/dist/client.js DELETED
@@ -1,2220 +0,0 @@
1
- /**
2
- * Ironflow Browser Client
3
- *
4
- * Singleton client for browser-based real-time interactions with Ironflow.
5
- */
6
- import { NotConfiguredError, IronflowError, RunFailedError, RunCancelledError, UnauthenticatedError, EnterpriseRequiredError, UnauthorizedError, ValidationError, createLogger, createNoopLogger, DEFAULT_TIMEOUTS, HEADERS, TriggerResponseSchema, TriggerSyncResponseSchema, RunResponseSchema, ListRunsResponseSchema, RunStatusSchema, ErrorResponseSchema, safeJsonParse, patterns, } from "@ironflow/core";
7
- import { mergeConfig } from "./config.js";
8
- import { SubscriptionManager, } from "./subscription.js";
9
- import { createWebSocketTransport } from "./transport/websocket.js";
10
- import { createConnectRPCTransport } from "./transport/connectrpc.js";
11
- import { filterWaitStreamFrames } from "./projection-stream.js";
12
- import { BrowserKVClient } from "./kv.js";
13
- import { BrowserConfigClient } from "./config-client.js";
14
- /**
15
- * Ironflow browser client singleton
16
- */
17
- class IronflowClient {
18
- config = null;
19
- logger = createNoopLogger();
20
- transport = null;
21
- subscriptionManager = null;
22
- visibilityHandler = null;
23
- /**
24
- * Configure the client
25
- *
26
- * Must be called before any other operations.
27
- */
28
- configure(options = {}) {
29
- this.config = mergeConfig(options);
30
- // Set up logger
31
- if (this.config.logger === false) {
32
- this.logger = createNoopLogger();
33
- }
34
- else if (this.config.logger) {
35
- this.logger = this.config.logger;
36
- }
37
- else {
38
- this.logger = createLogger({ prefix: "[ironflow]" });
39
- }
40
- // Clean up existing resources
41
- this.cleanup();
42
- // Create transport
43
- const transportOptions = {
44
- auth: this.config.auth,
45
- autoReconnect: this.config.reconnect.enabled,
46
- reconnectDelay: this.config.reconnect.backoff.initial,
47
- maxReconnectDelay: this.config.reconnect.backoff.max,
48
- reconnectBackoff: this.config.reconnect.backoff.multiplier,
49
- environment: this.config.environment,
50
- };
51
- // Create transport based on config (ConnectRPC by default)
52
- if (this.config.transport === "websocket") {
53
- this.transport = createWebSocketTransport(this.config.serverUrl, transportOptions);
54
- }
55
- else {
56
- // Default to ConnectRPC
57
- this.transport = createConnectRPCTransport(this.config.serverUrl, transportOptions);
58
- }
59
- // Create subscription manager
60
- this.subscriptionManager = new SubscriptionManager(this.transport, this.config.logger);
61
- // Set up visibility handling
62
- if (this.config.visibility.pauseOnHidden && typeof document !== "undefined") {
63
- this.setupVisibilityHandling();
64
- }
65
- this.logger.info("Ironflow client configured", {
66
- serverUrl: this.config.serverUrl,
67
- transport: this.config.transport,
68
- });
69
- }
70
- /**
71
- * Check if the client is configured
72
- */
73
- get isConfigured() {
74
- return this.config !== null;
75
- }
76
- /**
77
- * Detect which transport the server supports
78
- *
79
- * Returns the best available transport based on server capabilities.
80
- * ConnectRPC is preferred over WebSocket.
81
- *
82
- * @example
83
- * ```typescript
84
- * const transport = await ironflow.detectTransport();
85
- * // Returns 'connectrpc' | 'websocket'
86
- * ```
87
- */
88
- async detectTransport() {
89
- const serverUrl = this.config?.serverUrl ?? "http://localhost:9123";
90
- try {
91
- // Try to get server capabilities via ConnectRPC endpoint
92
- const detectHeaders = {
93
- "Content-Type": "application/json",
94
- };
95
- if (this.config?.environment) {
96
- detectHeaders[HEADERS.ENVIRONMENT] = this.config.environment;
97
- }
98
- const response = await fetch(`${serverUrl}/ironflow.v1.IronflowService/GetCapabilities`, {
99
- method: "POST",
100
- headers: detectHeaders,
101
- body: "{}",
102
- });
103
- if (response.ok) {
104
- const data = await response.json();
105
- const transports = data.transports;
106
- // Check if server explicitly supports connectrpc
107
- if (transports?.includes("connectrpc") || transports?.includes("grpc")) {
108
- return "connectrpc";
109
- }
110
- // Server responded, so ConnectRPC endpoint works
111
- return "connectrpc";
112
- }
113
- }
114
- catch {
115
- // ConnectRPC not available
116
- }
117
- // Fall back to WebSocket
118
- return "websocket";
119
- }
120
- /**
121
- * Get the current configuration
122
- */
123
- getConfig() {
124
- if (!this.config) {
125
- throw new NotConfiguredError();
126
- }
127
- return this.config;
128
- }
129
- // ============================================================================
130
- // Connection Management
131
- // ============================================================================
132
- /**
133
- * Connect to the Ironflow server
134
- */
135
- async connect() {
136
- this.ensureConfigured();
137
- await this.subscriptionManager.connect();
138
- }
139
- /**
140
- * Disconnect from the Ironflow server
141
- */
142
- disconnect() {
143
- if (this.subscriptionManager) {
144
- this.subscriptionManager.disconnect();
145
- }
146
- }
147
- /**
148
- * Register a callback for connection state changes
149
- */
150
- onConnectionChange(callback) {
151
- this.ensureConfigured();
152
- return this.subscriptionManager.onConnectionChange(callback);
153
- }
154
- /**
155
- * Get current connection state
156
- */
157
- get connectionState() {
158
- if (!this.subscriptionManager) {
159
- return "disconnected";
160
- }
161
- return this.subscriptionManager.connectionState;
162
- }
163
- // ============================================================================
164
- // Subscriptions
165
- // ============================================================================
166
- /**
167
- * Subscribe to events matching a pattern
168
- *
169
- * @example
170
- * ```typescript
171
- * // Basic subscription
172
- * const sub = await ironflow.subscribe('events:order.*', {
173
- * onEvent: (event) => console.log(event),
174
- * });
175
- *
176
- * // Multiple patterns
177
- * const sub = await ironflow.subscribe(['system.run.*', 'events:order.*'], {
178
- * onEvent: (event) => { ... }
179
- * });
180
- *
181
- * // With options
182
- * const sub = await ironflow.subscribe('events:*', {
183
- * onEvent: (e) => { ... },
184
- * replay: 10,
185
- * trackState: true,
186
- * ackMode: 'manual',
187
- * });
188
- * ```
189
- */
190
- subscribe(pattern, callbacksAndOptions) {
191
- this.ensureConfigured();
192
- return this.subscriptionManager.subscribe(pattern, callbacksAndOptions);
193
- }
194
- /**
195
- * Create a subscription group for batch management
196
- *
197
- * @example
198
- * ```typescript
199
- * const group = ironflow.subscriptionGroup();
200
- * await group.add('system.run.*', { onEvent: handleRun });
201
- * await group.add('events:payment.*', { onEvent: handlePayment });
202
- * // Later:
203
- * group.unsubscribeAll();
204
- * ```
205
- */
206
- subscriptionGroup() {
207
- this.ensureConfigured();
208
- return this.subscriptionManager.createGroup();
209
- }
210
- /**
211
- * Register a global error handler
212
- */
213
- onError(callback) {
214
- this.ensureConfigured();
215
- return this.subscriptionManager.onError(callback);
216
- }
217
- // ============================================================================
218
- // Workflow Operations
219
- // ============================================================================
220
- /**
221
- * Invoke a workflow function by ID
222
- *
223
- * @example
224
- * ```typescript
225
- * const run = await ironflow.invoke<OrderInput, OrderOutput>('process-order', {
226
- * data: { orderId: '123' }
227
- * });
228
- * ```
229
- */
230
- async invoke(functionId, options) {
231
- this.ensureConfigured();
232
- const response = await this.request(TriggerResponseSchema, "POST", "/ironflow.v1.IronflowService/Trigger", {
233
- event: functionId,
234
- data: options.data,
235
- });
236
- return {
237
- runIds: response.runIds ?? [],
238
- eventId: response.eventId,
239
- };
240
- }
241
- /**
242
- * Get run status
243
- */
244
- async getRun(runId) {
245
- this.ensureConfigured();
246
- const response = await this.request(RunResponseSchema, "POST", "/ironflow.v1.IronflowService/GetRun", { id: runId });
247
- return this.mapRunResponse(response);
248
- }
249
- /**
250
- * List runs with filtering
251
- */
252
- async listRuns(options) {
253
- this.ensureConfigured();
254
- const response = await this.request(ListRunsResponseSchema, "POST", "/ironflow.v1.IronflowService/ListRuns", {
255
- function_id: options?.functionId,
256
- status: options?.status?.toUpperCase(),
257
- limit: options?.limit,
258
- cursor: options?.cursor,
259
- });
260
- return {
261
- runs: (response.runs ?? []).map((r) => this.mapRunResponse(r)),
262
- nextCursor: response.nextCursor,
263
- totalCount: response.totalCount ?? 0,
264
- };
265
- }
266
- /**
267
- * Cancel a running run
268
- */
269
- async cancelRun(runId, reason) {
270
- this.ensureConfigured();
271
- const response = await this.request(RunResponseSchema, "POST", "/ironflow.v1.IronflowService/CancelRun", { id: runId, reason });
272
- return this.mapRunResponse(response);
273
- }
274
- /**
275
- * Retry a failed run
276
- */
277
- async retryRun(runId, fromStep) {
278
- this.ensureConfigured();
279
- const response = await this.request(RunResponseSchema, "POST", "/ironflow.v1.IronflowService/RetryRun", { id: runId, fromStep });
280
- return this.mapRunResponse(response);
281
- }
282
- /**
283
- * Patch a step's output (hot patching)
284
- */
285
- async patchStep(stepId, output, reason) {
286
- this.ensureConfigured();
287
- const url = `${this.config.serverUrl}/api/v1/steps/patch`;
288
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
289
- const controller = new AbortController();
290
- const timeoutId = setTimeout(() => controller.abort(), timeout);
291
- try {
292
- const headers = {
293
- "Content-Type": "application/json",
294
- [HEADERS.ENVIRONMENT]: this.config.environment,
295
- };
296
- if (this.config.auth?.apiKey) {
297
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
298
- }
299
- const response = await fetch(url, {
300
- method: "POST",
301
- headers,
302
- body: JSON.stringify({ step_id: stepId, output, reason: reason || "" }),
303
- signal: controller.signal,
304
- });
305
- if (!response.ok) {
306
- const error = safeJsonParse(await response.text());
307
- throw new IronflowError(error?.message || `Patch step failed: ${response.status}`, { code: error?.code || "PATCH_FAILED" });
308
- }
309
- }
310
- finally {
311
- clearTimeout(timeoutId);
312
- }
313
- }
314
- /**
315
- * Resume a paused or failed run
316
- */
317
- async resumeRun(runId, fromStep) {
318
- this.ensureConfigured();
319
- const url = `${this.config.serverUrl}/api/v1/runs/resume`;
320
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
321
- const controller = new AbortController();
322
- const timeoutId = setTimeout(() => controller.abort(), timeout);
323
- try {
324
- const headers = {
325
- "Content-Type": "application/json",
326
- [HEADERS.ENVIRONMENT]: this.config.environment,
327
- };
328
- if (this.config.auth?.apiKey) {
329
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
330
- }
331
- const response = await fetch(url, {
332
- method: "POST",
333
- headers,
334
- body: JSON.stringify({ run_id: runId, from_step: fromStep || "" }),
335
- signal: controller.signal,
336
- });
337
- if (!response.ok) {
338
- const error = safeJsonParse(await response.text());
339
- throw new IronflowError(error?.message || `Resume run failed: ${response.status}`, { code: error?.code || "RESUME_FAILED" });
340
- }
341
- return response.json();
342
- }
343
- finally {
344
- clearTimeout(timeoutId);
345
- }
346
- }
347
- /**
348
- * Pause a running workflow at the next step boundary (scoped injection).
349
- */
350
- async pauseRun(runId) {
351
- this.ensureConfigured();
352
- const url = `${this.config.serverUrl}/ironflow.v1.IronflowService/PauseRun`;
353
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
354
- const controller = new AbortController();
355
- const timeoutId = setTimeout(() => controller.abort(), timeout);
356
- try {
357
- const headers = {
358
- "Content-Type": "application/json",
359
- [HEADERS.ENVIRONMENT]: this.config.environment,
360
- };
361
- if (this.config.auth?.apiKey) {
362
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
363
- }
364
- const response = await fetch(url, {
365
- method: "POST",
366
- headers,
367
- body: JSON.stringify({ run_id: runId }),
368
- signal: controller.signal,
369
- });
370
- if (!response.ok) {
371
- const error = safeJsonParse(await response.text());
372
- throw new IronflowError(error?.message || `Pause run failed: ${response.status}`, { code: error?.code || "PAUSE_FAILED" });
373
- }
374
- return response.json();
375
- }
376
- finally {
377
- clearTimeout(timeoutId);
378
- }
379
- }
380
- /**
381
- * Get the paused state of a run, including completed steps and next step hint.
382
- */
383
- async getPausedState(runId) {
384
- this.ensureConfigured();
385
- const url = `${this.config.serverUrl}/ironflow.v1.IronflowService/GetPausedState`;
386
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
387
- const controller = new AbortController();
388
- const timeoutId = setTimeout(() => controller.abort(), timeout);
389
- try {
390
- const headers = {
391
- "Content-Type": "application/json",
392
- [HEADERS.ENVIRONMENT]: this.config.environment,
393
- };
394
- if (this.config.auth?.apiKey) {
395
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
396
- }
397
- const response = await fetch(url, {
398
- method: "POST",
399
- headers,
400
- body: JSON.stringify({ run_id: runId }),
401
- signal: controller.signal,
402
- });
403
- if (!response.ok) {
404
- const error = safeJsonParse(await response.text());
405
- throw new IronflowError(error?.message || `Get paused state failed: ${response.status}`, { code: error?.code || "GET_PAUSED_STATE_FAILED" });
406
- }
407
- const data = (await response.json());
408
- return {
409
- steps: (data.steps || []).map((s) => ({
410
- id: s.id,
411
- name: s.name,
412
- output: s.output ? JSON.parse(s.output) : null,
413
- injected: s.injected,
414
- completedAt: s.completed_at,
415
- })),
416
- nextStepHint: data.next_step_hint,
417
- pauseReason: data.pause_reason,
418
- };
419
- }
420
- finally {
421
- clearTimeout(timeoutId);
422
- }
423
- }
424
- /**
425
- * Inject new output for a step in a paused run (scoped injection).
426
- */
427
- async injectStepOutput(runId, stepId, newOutput, reason) {
428
- this.ensureConfigured();
429
- const url = `${this.config.serverUrl}/ironflow.v1.IronflowService/InjectStepOutput`;
430
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
431
- const controller = new AbortController();
432
- const timeoutId = setTimeout(() => controller.abort(), timeout);
433
- try {
434
- const headers = {
435
- "Content-Type": "application/json",
436
- [HEADERS.ENVIRONMENT]: this.config.environment,
437
- };
438
- if (this.config.auth?.apiKey) {
439
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
440
- }
441
- const response = await fetch(url, {
442
- method: "POST",
443
- headers,
444
- body: JSON.stringify({
445
- run_id: runId,
446
- step_id: stepId,
447
- new_output: JSON.stringify(newOutput),
448
- reason: reason ?? "",
449
- }),
450
- signal: controller.signal,
451
- });
452
- if (!response.ok) {
453
- const error = safeJsonParse(await response.text());
454
- throw new IronflowError(error?.message || `Inject step output failed: ${response.status}`, { code: error?.code || "INJECT_FAILED" });
455
- }
456
- const data = (await response.json());
457
- return {
458
- stepId: data.step_id,
459
- previousOutput: data.previous_output
460
- ? JSON.parse(data.previous_output)
461
- : null,
462
- };
463
- }
464
- finally {
465
- clearTimeout(timeoutId);
466
- }
467
- }
468
- // ==========================================================================
469
- // Time-Travel Debugging
470
- // ==========================================================================
471
- /**
472
- * Get the reconstructed state of a run at a specific timestamp.
473
- */
474
- async getRunStateAt(runId, timestamp) {
475
- this.ensureConfigured();
476
- const url = `${this.config.serverUrl}/ironflow.v1.TimeTravelService/GetRunStateAt`;
477
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
478
- const controller = new AbortController();
479
- const timeoutId = setTimeout(() => controller.abort(), timeout);
480
- try {
481
- const headers = {
482
- "Content-Type": "application/json",
483
- [HEADERS.ENVIRONMENT]: this.config.environment,
484
- };
485
- if (this.config.auth?.apiKey) {
486
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
487
- }
488
- const response = await fetch(url, {
489
- method: "POST",
490
- headers,
491
- body: JSON.stringify({
492
- runId,
493
- timestamp: timestamp.toISOString(),
494
- }),
495
- signal: controller.signal,
496
- });
497
- if (!response.ok) {
498
- const error = safeJsonParse(await response.text());
499
- throw new IronflowError(error?.message || `Get run state failed: ${response.status}`, { code: error?.code || "GET_RUN_STATE_FAILED" });
500
- }
501
- const data = (await response.json());
502
- const s = data.snapshot;
503
- return {
504
- runId: s.runId,
505
- functionId: s.functionId,
506
- status: s.status,
507
- input: s.input ? JSON.parse(atob(s.input)) : null,
508
- steps: (s.steps || []).map((step) => ({
509
- stepId: step.stepId,
510
- name: step.name,
511
- type: step.type,
512
- sequence: step.sequence,
513
- status: step.status,
514
- output: step.output ? JSON.parse(atob(step.output)) : null,
515
- error: step.error ? JSON.parse(atob(step.error)) : null,
516
- originalOutput: step.originalOutput ? JSON.parse(atob(step.originalOutput)) : null,
517
- startedAt: step.startedAt ? new Date(step.startedAt) : null,
518
- completedAt: step.completedAt ? new Date(step.completedAt) : null,
519
- durationMs: step.durationMs ?? null,
520
- injected: step.injected,
521
- patched: step.patched,
522
- })),
523
- timestamp: new Date(s.timestamp),
524
- createdAt: s.createdAt ? new Date(s.createdAt) : null,
525
- };
526
- }
527
- finally {
528
- clearTimeout(timeoutId);
529
- }
530
- }
531
- /**
532
- * Get the timeline of audit events for a run.
533
- */
534
- async getRunTimeline(runId) {
535
- this.ensureConfigured();
536
- const url = `${this.config.serverUrl}/ironflow.v1.TimeTravelService/GetRunTimeline`;
537
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
538
- const controller = new AbortController();
539
- const timeoutId = setTimeout(() => controller.abort(), timeout);
540
- try {
541
- const headers = {
542
- "Content-Type": "application/json",
543
- [HEADERS.ENVIRONMENT]: this.config.environment,
544
- };
545
- if (this.config.auth?.apiKey) {
546
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
547
- }
548
- const response = await fetch(url, {
549
- method: "POST",
550
- headers,
551
- body: JSON.stringify({ runId }),
552
- signal: controller.signal,
553
- });
554
- if (!response.ok) {
555
- const error = safeJsonParse(await response.text());
556
- throw new IronflowError(error?.message || `Get run timeline failed: ${response.status}`, { code: error?.code || "GET_TIMELINE_FAILED" });
557
- }
558
- const data = (await response.json());
559
- return (data.events || []).map((e) => ({
560
- id: e.id,
561
- eventType: e.eventType,
562
- stepId: e.stepId || "",
563
- stepName: e.stepName || "",
564
- summary: e.summary,
565
- significant: e.significant,
566
- timestamp: new Date(e.timestamp),
567
- }));
568
- }
569
- finally {
570
- clearTimeout(timeoutId);
571
- }
572
- }
573
- /**
574
- * Get the output of a specific step at a specific timestamp.
575
- */
576
- async getStepOutputAt(runId, stepId, timestamp) {
577
- this.ensureConfigured();
578
- const url = `${this.config.serverUrl}/ironflow.v1.TimeTravelService/GetStepOutputAt`;
579
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
580
- const controller = new AbortController();
581
- const timeoutId = setTimeout(() => controller.abort(), timeout);
582
- try {
583
- const headers = {
584
- "Content-Type": "application/json",
585
- [HEADERS.ENVIRONMENT]: this.config.environment,
586
- };
587
- if (this.config.auth?.apiKey) {
588
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
589
- }
590
- const response = await fetch(url, {
591
- method: "POST",
592
- headers,
593
- body: JSON.stringify({
594
- runId,
595
- stepId,
596
- timestamp: timestamp.toISOString(),
597
- }),
598
- signal: controller.signal,
599
- });
600
- if (!response.ok) {
601
- const error = safeJsonParse(await response.text());
602
- throw new IronflowError(error?.message || `Get step output failed: ${response.status}`, { code: error?.code || "GET_STEP_OUTPUT_FAILED" });
603
- }
604
- const data = (await response.json());
605
- return {
606
- stepId: data.stepId,
607
- status: data.status,
608
- output: data.output ? JSON.parse(atob(data.output)) : null,
609
- originalOutput: data.originalOutput ? JSON.parse(atob(data.originalOutput)) : null,
610
- patched: data.patched,
611
- injected: data.injected,
612
- };
613
- }
614
- finally {
615
- clearTimeout(timeoutId);
616
- }
617
- }
618
- /**
619
- * List registered functions
620
- */
621
- async listFunctions() {
622
- this.ensureConfigured();
623
- const url = `${this.config.serverUrl}/api/v1/functions`;
624
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
625
- const controller = new AbortController();
626
- const timeoutId = setTimeout(() => controller.abort(), timeout);
627
- try {
628
- const headers = {
629
- [HEADERS.ENVIRONMENT]: this.config.environment,
630
- };
631
- if (this.config.auth?.apiKey) {
632
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
633
- }
634
- const response = await fetch(url, {
635
- method: "GET",
636
- headers,
637
- signal: controller.signal,
638
- });
639
- if (!response.ok) {
640
- throw new IronflowError(`List functions failed: ${response.status}`, { code: "LIST_FUNCTIONS_FAILED" });
641
- }
642
- const data = await response.json();
643
- return data.functions || [];
644
- }
645
- finally {
646
- clearTimeout(timeoutId);
647
- }
648
- }
649
- /**
650
- * List connected workers
651
- */
652
- async listWorkers() {
653
- this.ensureConfigured();
654
- const url = `${this.config.serverUrl}/api/v1/workers`;
655
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
656
- const controller = new AbortController();
657
- const timeoutId = setTimeout(() => controller.abort(), timeout);
658
- try {
659
- const headers = {
660
- [HEADERS.ENVIRONMENT]: this.config.environment,
661
- };
662
- if (this.config.auth?.apiKey) {
663
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
664
- }
665
- const response = await fetch(url, {
666
- method: "GET",
667
- headers,
668
- signal: controller.signal,
669
- });
670
- if (!response.ok) {
671
- throw new IronflowError(`List workers failed: ${response.status}`, { code: "LIST_WORKERS_FAILED" });
672
- }
673
- const data = await response.json();
674
- return data.workers || [];
675
- }
676
- finally {
677
- clearTimeout(timeoutId);
678
- }
679
- }
680
- /**
681
- * Health check
682
- */
683
- async health() {
684
- this.ensureConfigured();
685
- const url = `${this.config.serverUrl}/health`;
686
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
687
- const controller = new AbortController();
688
- const timeoutId = setTimeout(() => controller.abort(), timeout);
689
- try {
690
- const response = await fetch(url, {
691
- method: "GET",
692
- signal: controller.signal,
693
- });
694
- if (!response.ok) {
695
- throw new IronflowError(`Health check failed: ${response.status}`, { code: "HEALTH_FAILED" });
696
- }
697
- return response.json();
698
- }
699
- finally {
700
- clearTimeout(timeoutId);
701
- }
702
- }
703
- /**
704
- * Get server capabilities
705
- */
706
- async getCapabilities() {
707
- this.ensureConfigured();
708
- const url = `${this.config.serverUrl}/api/v1/capabilities`;
709
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
710
- const controller = new AbortController();
711
- const timeoutId = setTimeout(() => controller.abort(), timeout);
712
- try {
713
- const response = await fetch(url, {
714
- method: "GET",
715
- signal: controller.signal,
716
- });
717
- if (!response.ok) {
718
- throw new IronflowError(`Get capabilities failed: ${response.status}`, { code: "CAPABILITIES_FAILED" });
719
- }
720
- return response.json();
721
- }
722
- finally {
723
- clearTimeout(timeoutId);
724
- }
725
- }
726
- // ============================================================================
727
- // Event Emission
728
- // ============================================================================
729
- /**
730
- * Emit an event
731
- *
732
- * @example
733
- * ```typescript
734
- * await ironflow.emit('order.approved', {
735
- * orderId: '123',
736
- * approvedBy: 'user@example.com'
737
- * });
738
- * ```
739
- */
740
- async emit(eventName, data, options) {
741
- this.ensureConfigured();
742
- const response = await this.request(TriggerResponseSchema, "POST", "/ironflow.v1.PubSubService/Emit", {
743
- event: eventName,
744
- data,
745
- ...(options?.version ? { version: options.version } : {}),
746
- idempotency_key: options?.idempotencyKey,
747
- metadata: options?.metadata,
748
- namespace: options?.namespace,
749
- });
750
- return {
751
- runIds: response.runIds ?? [],
752
- eventId: response.eventId,
753
- };
754
- }
755
- /**
756
- * Emit an event synchronously — waits for the triggered run to complete and returns the result.
757
- *
758
- * Calls the TriggerSync endpoint, which blocks until the run finishes or the timeout elapses.
759
- * Throws RunFailedError if the run fails, RunCancelledError if it is cancelled.
760
- *
761
- * @example
762
- * ```typescript
763
- * const result = await ironflow.emitSync("order.placed", { orderId: "123" });
764
- * console.log("Output:", result.output);
765
- * ```
766
- */
767
- async emitSync(eventName, data, options) {
768
- this.ensureConfigured();
769
- const timeout = options?.timeout ?? 30000;
770
- const response = await this.request(TriggerSyncResponseSchema, "POST", "/ironflow.v1.IronflowService/TriggerSync", { event: eventName, data, timeout_ms: timeout });
771
- const results = response.results;
772
- if (!results?.length) {
773
- throw new IronflowError("No results returned from TriggerSync", { code: "NO_RESULTS", retryable: false });
774
- }
775
- const result = results[0];
776
- if (!result) {
777
- throw new IronflowError("No results returned from TriggerSync", { code: "NO_RESULTS", retryable: false });
778
- }
779
- if (result.status === "failed") {
780
- throw new RunFailedError(result.runId, result.error);
781
- }
782
- if (result.status === "cancelled") {
783
- throw new RunCancelledError(result.runId);
784
- }
785
- return {
786
- runId: result.runId,
787
- functionId: result.functionId,
788
- status: result.status,
789
- output: result.output,
790
- durationMs: result.durationMs,
791
- };
792
- }
793
- // ============================================================================
794
- // Consumer Groups
795
- // ============================================================================
796
- /**
797
- * Join a consumer group for load-balanced event processing
798
- */
799
- async joinConsumerGroup(groupName, pattern, callbacksAndOptions) {
800
- this.ensureConfigured();
801
- const sub = await this.subscribe(pattern, {
802
- ...callbacksAndOptions,
803
- consumerGroup: groupName,
804
- ackMode: "manual",
805
- });
806
- return sub;
807
- }
808
- // ============================================================================
809
- // Entity Streams
810
- // ============================================================================
811
- /**
812
- * Entity stream operations
813
- *
814
- * @example
815
- * ```typescript
816
- * // Append an event to a stream
817
- * const result = await ironflow.streams.append("order-123", {
818
- * name: "order.created",
819
- * data: { total: 100 },
820
- * entityType: "order",
821
- * });
822
- *
823
- * // Read events from a stream
824
- * const { events } = await ironflow.streams.read("order-123", { limit: 10 });
825
- *
826
- * // Get stream info
827
- * const info = await ironflow.streams.getInfo("order-123");
828
- * ```
829
- */
830
- streams = {
831
- /**
832
- * Append an event to an entity stream
833
- */
834
- append: async (entityId, input, options) => {
835
- this.ensureConfigured();
836
- const body = {
837
- entity_id: entityId,
838
- entity_type: input.entityType,
839
- event_name: input.name,
840
- data: input.data,
841
- expected_version: options?.expectedVersion ?? -1,
842
- idempotency_key: options?.idempotencyKey ?? "",
843
- version: options?.version ?? 1,
844
- };
845
- if (options?.metadata !== undefined) {
846
- body.metadata = options.metadata;
847
- }
848
- const response = await this.streamRequest("/ironflow.v1.EntityStreamService/AppendEvent", body);
849
- return {
850
- entityVersion: Number(response.entityVersion ?? 0),
851
- eventId: response.eventId ?? "",
852
- };
853
- },
854
- /**
855
- * Read events from an entity stream
856
- */
857
- read: async (entityId, options) => {
858
- this.ensureConfigured();
859
- const response = await this.streamRequest("/ironflow.v1.EntityStreamService/ReadStream", {
860
- entity_id: entityId,
861
- from_version: options?.fromVersion ?? 0,
862
- limit: options?.limit ?? 0,
863
- direction: options?.direction ?? "forward",
864
- });
865
- return {
866
- events: (response.events ?? []).map((e) => ({
867
- id: e.id,
868
- name: e.name,
869
- data: e.data ?? {},
870
- entityVersion: Number(e.entityVersion ?? 0),
871
- version: e.version,
872
- timestamp: e.timestamp,
873
- source: e.source,
874
- metadata: e.metadata,
875
- })),
876
- totalCount: Number(response.totalCount ?? 0),
877
- };
878
- },
879
- /**
880
- * Get information about an entity stream.
881
- *
882
- * Returns `null` if no events have been written to this stream yet — safe to
883
- * pass `expectedVersion: 0` to `append()` in that case to create the first event.
884
- *
885
- * @example
886
- * ```typescript
887
- * const info = await ironflow.streams.getInfo("order-123");
888
- * await ironflow.streams.append("order-123", event, {
889
- * expectedVersion: info ? info.version : 0,
890
- * });
891
- * ```
892
- */
893
- getInfo: async (entityId) => {
894
- this.ensureConfigured();
895
- try {
896
- const response = await this.streamRequest("/ironflow.v1.EntityStreamService/GetStreamInfo", {
897
- entity_id: entityId,
898
- });
899
- return {
900
- entityId: response.entityId ?? "",
901
- entityType: response.entityType ?? "",
902
- version: Number(response.version ?? 0),
903
- eventCount: Number(response.eventCount ?? 0),
904
- createdAt: response.createdAt ?? "",
905
- updatedAt: response.updatedAt ?? "",
906
- };
907
- }
908
- catch (err) {
909
- if (err instanceof IronflowError && err.message === "stream not found") {
910
- return null;
911
- }
912
- throw err;
913
- }
914
- },
915
- /**
916
- * Create a snapshot of the materialized state at a specific stream version.
917
- */
918
- createSnapshot: async (entityId, input) => {
919
- this.ensureConfigured();
920
- const response = await this.streamRequest("/ironflow.v1.EntityStreamService/CreateSnapshot", {
921
- entity_id: entityId,
922
- entity_type: input.entityType,
923
- entity_version: input.entityVersion,
924
- state: input.state,
925
- });
926
- return { snapshotId: response.snapshotId ?? "" };
927
- },
928
- /**
929
- * Get the latest snapshot at or before a given version.
930
- */
931
- getSnapshot: async (entityId, options) => {
932
- this.ensureConfigured();
933
- const response = await this.streamRequest("/ironflow.v1.EntityStreamService/GetSnapshot", {
934
- entity_id: entityId,
935
- before_version: options?.beforeVersion ?? 0,
936
- });
937
- return {
938
- snapshotId: response.snapshotId ?? "",
939
- entityId: response.entityId ?? "",
940
- entityType: response.entityType ?? "",
941
- entityVersion: Number(response.entityVersion ?? 0),
942
- state: response.state ?? {},
943
- createdAt: response.createdAt ?? "",
944
- };
945
- },
946
- /**
947
- * Subscribe to real-time events for an entity stream
948
- *
949
- * @example
950
- * ```typescript
951
- * const sub = await ironflow.streams.subscribe("order-123", {
952
- * entityType: "order",
953
- * onEvent: (event) => console.log(event),
954
- * replay: 100,
955
- * });
956
- *
957
- * // Cleanup
958
- * sub.unsubscribe();
959
- * ```
960
- */
961
- subscribe: async (entityId, options) => {
962
- this.ensureConfigured();
963
- const pattern = `entity:${options.entityType}.${entityId}.>`;
964
- const sub = await this.subscribe(pattern, {
965
- onEvent: (event) => {
966
- const data = event.data;
967
- const streamEvent = {
968
- id: data.id ?? "",
969
- name: data.name ?? "",
970
- data: data.data ?? {},
971
- entityVersion: data.entityVersion ?? 0,
972
- version: data.version ?? 0,
973
- timestamp: data.timestamp ?? "",
974
- source: data.source,
975
- metadata: data.metadata,
976
- };
977
- options.onEvent(streamEvent);
978
- },
979
- onError: options.onError
980
- ? (info) => options.onError(new Error(info.message))
981
- : undefined,
982
- replay: options.replay,
983
- });
984
- return sub;
985
- },
986
- };
987
- // ============================================================================
988
- // Projections
989
- // ============================================================================
990
- /**
991
- * Get the current state of a projection
992
- *
993
- * @example
994
- * ```typescript
995
- * const result = await ironflow.getProjection<OrderStats>('order-stats');
996
- * console.log(result.state); // { totalOrders: 42, ... }
997
- *
998
- * // With partition
999
- * const result = await ironflow.getProjection('order-stats', { partition: 'customer-123' });
1000
- * ```
1001
- */
1002
- async getProjection(name, options) {
1003
- this.ensureConfigured();
1004
- let url = `${this.config.serverUrl}/api/v1/projections/${encodeURIComponent(name)}`;
1005
- if (options?.partition) {
1006
- url += `?partition=${encodeURIComponent(options.partition)}`;
1007
- }
1008
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
1009
- const controller = new AbortController();
1010
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1011
- try {
1012
- const headers = {
1013
- [HEADERS.ENVIRONMENT]: this.config.environment,
1014
- };
1015
- if (this.config.auth?.apiKey) {
1016
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1017
- }
1018
- const response = await fetch(url, {
1019
- method: "GET",
1020
- headers,
1021
- signal: controller.signal,
1022
- });
1023
- if (!response.ok) {
1024
- const error = safeJsonParse(await response.text());
1025
- throw new IronflowError(error?.message || `Get projection failed: ${response.status}`, { code: error?.code || "GET_PROJECTION_FAILED" });
1026
- }
1027
- const data = await response.json();
1028
- return {
1029
- name: data.name,
1030
- partition: data.state?.partition_key ?? "__global__",
1031
- state: data.state?.state ?? {},
1032
- lastEventId: data.state?.last_event_id ?? "",
1033
- lastEventTime: data.state?.last_event_time
1034
- ? new Date(data.state.last_event_time)
1035
- : new Date(0),
1036
- version: data.state?.version ?? 0,
1037
- mode: data.mode,
1038
- };
1039
- }
1040
- finally {
1041
- clearTimeout(timeoutId);
1042
- }
1043
- }
1044
- /**
1045
- * Query a SQL-backed projection table with optional filtering, ordering, and pagination.
1046
- *
1047
- * @example
1048
- * ```typescript
1049
- * const result = await ironflow.querySQLProjection('board', {
1050
- * where: "status = 'OPEN'",
1051
- * orderBy: "title ASC",
1052
- * limit: 50,
1053
- * });
1054
- * ```
1055
- */
1056
- async querySQLProjection(name, options) {
1057
- this.ensureConfigured();
1058
- const response = await this.streamRequest("/ironflow.v1.ProjectionService/QuerySQLProjection", {
1059
- name,
1060
- where: options?.where ?? "",
1061
- order_by: options?.orderBy ?? "",
1062
- limit: options?.limit ?? 100,
1063
- offset: options?.offset ?? 0,
1064
- });
1065
- return {
1066
- columns: response.columns ?? [],
1067
- rows: (response.rows ?? []).map((r) => r.values),
1068
- totalCount: Number(response.totalCount ?? 0),
1069
- };
1070
- }
1071
- /**
1072
- * Get the status of a projection
1073
- *
1074
- * @example
1075
- * ```typescript
1076
- * const status = await ironflow.getProjectionStatus('order-stats');
1077
- * console.log(status.status); // 'active' | 'rebuilding' | 'paused' | 'error'
1078
- * ```
1079
- */
1080
- async getProjectionStatus(name) {
1081
- this.ensureConfigured();
1082
- const url = `${this.config.serverUrl}/api/v1/projections/${encodeURIComponent(name)}/status`;
1083
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
1084
- const controller = new AbortController();
1085
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1086
- try {
1087
- const headers = {
1088
- [HEADERS.ENVIRONMENT]: this.config.environment,
1089
- };
1090
- if (this.config.auth?.apiKey) {
1091
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1092
- }
1093
- const response = await fetch(url, {
1094
- method: "GET",
1095
- headers,
1096
- signal: controller.signal,
1097
- });
1098
- if (!response.ok) {
1099
- const error = safeJsonParse(await response.text());
1100
- throw new IronflowError(error?.message || `Get projection status failed: ${response.status}`, { code: error?.code || "GET_PROJECTION_STATUS_FAILED" });
1101
- }
1102
- const data = await response.json();
1103
- return {
1104
- name: data.name,
1105
- status: data.status,
1106
- mode: data.mode,
1107
- lastEventSeq: data.last_event_seq ?? 0,
1108
- lag: data.lag ?? 0,
1109
- errorMessage: data.error_message || undefined,
1110
- updatedAt: data.updated_at ? new Date(data.updated_at) : new Date(),
1111
- };
1112
- }
1113
- finally {
1114
- clearTimeout(timeoutId);
1115
- }
1116
- }
1117
- /**
1118
- * Trigger a rebuild of a projection
1119
- *
1120
- * @example
1121
- * ```typescript
1122
- * const result = await ironflow.rebuildProjection('order-stats');
1123
- * ```
1124
- */
1125
- async rebuildProjection(name, options) {
1126
- this.ensureConfigured();
1127
- const url = `${this.config.serverUrl}/api/v1/projections/${encodeURIComponent(name)}/rebuild`;
1128
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
1129
- const controller = new AbortController();
1130
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1131
- try {
1132
- const headers = {
1133
- "Content-Type": "application/json",
1134
- [HEADERS.ENVIRONMENT]: this.config.environment,
1135
- };
1136
- if (this.config.auth?.apiKey) {
1137
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1138
- }
1139
- const response = await fetch(url, {
1140
- method: "POST",
1141
- headers,
1142
- body: JSON.stringify({
1143
- partition: options?.partition,
1144
- from_event_id: options?.fromEventId,
1145
- dry_run: options?.dryRun,
1146
- }),
1147
- signal: controller.signal,
1148
- });
1149
- if (!response.ok) {
1150
- const error = safeJsonParse(await response.text());
1151
- throw new IronflowError(error?.message || `Rebuild projection failed: ${response.status}`, { code: error?.code || "REBUILD_PROJECTION_FAILED" });
1152
- }
1153
- return response.json();
1154
- }
1155
- finally {
1156
- clearTimeout(timeoutId);
1157
- }
1158
- }
1159
- /**
1160
- * Wait until the named projection has processed events up to `minSeq`,
1161
- * or the timeout elapses. Read-your-writes primitive for CQRS UIs:
1162
- * pair with `sequence` from an `appendEntityEvent` response.
1163
- *
1164
- * Issue #473.
1165
- */
1166
- async waitForProjectionCatchup(name, opts) {
1167
- this.ensureConfigured();
1168
- const params = new URLSearchParams();
1169
- params.set("minSeq", String(opts.minSeq));
1170
- if (opts.timeoutMs !== undefined)
1171
- params.set("timeout", String(opts.timeoutMs));
1172
- if (opts.partition)
1173
- params.set("partition", opts.partition);
1174
- const url = `${this.config.serverUrl}/api/v1/projections/${encodeURIComponent(name)}/catchup?${params.toString()}`;
1175
- // The wait timeout may exceed the normal client timeout — use the
1176
- // opts.timeoutMs (plus a small grace period) so the client doesn't
1177
- // abort the request before the server gets a chance to time out.
1178
- const clientTimeoutMs = (opts.timeoutMs ?? 30_000) + 2_000;
1179
- const controller = new AbortController();
1180
- const timeoutId = setTimeout(() => controller.abort(), clientTimeoutMs);
1181
- try {
1182
- const headers = {
1183
- [HEADERS.ENVIRONMENT]: this.config.environment,
1184
- };
1185
- if (this.config.auth?.apiKey) {
1186
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1187
- }
1188
- const response = await fetch(url, {
1189
- method: "GET",
1190
- headers,
1191
- signal: controller.signal,
1192
- });
1193
- if (!response.ok) {
1194
- const error = safeJsonParse(await response.text());
1195
- throw new IronflowError(error?.message || `Wait for projection catchup failed: ${response.status}`, { code: error?.code || "WAIT_FOR_PROJECTION_FAILED" });
1196
- }
1197
- return (await response.json());
1198
- }
1199
- finally {
1200
- clearTimeout(timeoutId);
1201
- }
1202
- }
1203
- /**
1204
- * Streaming wait for a projection to catch up to `minSeq` (issue #476).
1205
- *
1206
- * Returns an `AsyncIterable<WaitProgress>` that yields a frame each time
1207
- * the projection cursor advances, plus one terminal frame (`terminal:
1208
- * true`) on catch-up, timeout, or error, after which iteration ends.
1209
- * Heartbeats are filtered inside the SDK — callers never see them.
1210
- *
1211
- * Unlike the unary `waitForProjectionCatchup`, this path uses a
1212
- * server-streaming ConnectRPC connection. The server caps the wait at
1213
- * 300s regardless of `timeoutMs`. Pass `opts.signal` to cancel early
1214
- * (e.g., on React unmount); breaking out of `for await` also aborts
1215
- * the stream.
1216
- *
1217
- * Example:
1218
- * ```ts
1219
- * for await (const p of ironflow.waitForProjectionCatchupStream("order-view", { minSeq: 42 })) {
1220
- * if (p.terminal) {
1221
- * if (p.caughtUp) return;
1222
- * throw new Error(`wait failed: timedOut=${p.timedOut} error=${p.error}`);
1223
- * }
1224
- * console.log(`progress: ${p.currentSeq}/${p.targetSeq} behind=${p.behindByEvents}`);
1225
- * }
1226
- * ```
1227
- */
1228
- async *waitForProjectionCatchupStream(name, opts) {
1229
- this.ensureConfigured();
1230
- // Lazy-load the generated projection client deps only when someone
1231
- // actually calls the streaming API — keeps the tree-shaken bundle small
1232
- // for apps that only use the unary wait.
1233
- const { createClient } = await import("@connectrpc/connect");
1234
- const { createConnectTransport } = await import("@connectrpc/connect-web");
1235
- const { create } = await import("@bufbuild/protobuf");
1236
- const { ProjectionService, WaitProjectionCatchupRequestSchema, WaitStreamFrameKind } = await import("@ironflow/core/gen");
1237
- const transport = createConnectTransport({
1238
- baseUrl: this.config.serverUrl,
1239
- interceptors: [
1240
- (next) => async (req) => {
1241
- req.header.set(HEADERS.ENVIRONMENT, this.config.environment);
1242
- const token = this.config.auth?.apiKey || this.config.auth?.token;
1243
- if (token)
1244
- req.header.set("Authorization", `Bearer ${token}`);
1245
- return next(req);
1246
- },
1247
- ],
1248
- });
1249
- const client = createClient(ProjectionService, transport);
1250
- const request = create(WaitProjectionCatchupRequestSchema, {
1251
- name,
1252
- minSeq: typeof opts.minSeq === "bigint" ? opts.minSeq : BigInt(opts.minSeq),
1253
- partition: opts.partition ?? "",
1254
- timeout: opts.timeoutMs !== undefined
1255
- ? {
1256
- $typeName: "google.protobuf.Duration",
1257
- seconds: BigInt(Math.floor(opts.timeoutMs / 1000)),
1258
- nanos: (opts.timeoutMs % 1000) * 1_000_000,
1259
- }
1260
- : undefined,
1261
- });
1262
- const stream = client.waitProjectionCatchupStream(request, { signal: opts.signal });
1263
- yield* filterWaitStreamFrames(stream, WaitStreamFrameKind);
1264
- }
1265
- /**
1266
- * Wait for a specific event to be processed by the given projection.
1267
- * The server resolves `eventId` to its NATS sequence internally.
1268
- *
1269
- * Issue #473.
1270
- */
1271
- async waitForEvent(eventId, projection, opts = {}) {
1272
- this.ensureConfigured();
1273
- const url = `${this.config.serverUrl}/api/v1/projections/wait-for-event`;
1274
- const clientTimeoutMs = (opts.timeoutMs ?? 30_000) + 2_000;
1275
- const controller = new AbortController();
1276
- const timeoutId = setTimeout(() => controller.abort(), clientTimeoutMs);
1277
- try {
1278
- const headers = {
1279
- "Content-Type": "application/json",
1280
- [HEADERS.ENVIRONMENT]: this.config.environment,
1281
- };
1282
- if (this.config.auth?.apiKey) {
1283
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1284
- }
1285
- const body = { eventId, projection };
1286
- if (opts.timeoutMs !== undefined)
1287
- body.timeoutMs = opts.timeoutMs;
1288
- if (opts.partition)
1289
- body.partition = opts.partition;
1290
- const response = await fetch(url, {
1291
- method: "POST",
1292
- headers,
1293
- body: JSON.stringify(body),
1294
- signal: controller.signal,
1295
- });
1296
- if (!response.ok) {
1297
- const error = safeJsonParse(await response.text());
1298
- throw new IronflowError(error?.message || `Wait for event failed: ${response.status}`, { code: error?.code || "WAIT_FOR_EVENT_FAILED" });
1299
- }
1300
- return (await response.json());
1301
- }
1302
- finally {
1303
- clearTimeout(timeoutId);
1304
- }
1305
- }
1306
- /**
1307
- * List all registered projections
1308
- *
1309
- * @example
1310
- * ```typescript
1311
- * const projections = await ironflow.listProjections();
1312
- * projections.forEach(p => console.log(p.name, p.status));
1313
- * ```
1314
- */
1315
- async listProjections() {
1316
- this.ensureConfigured();
1317
- const url = `${this.config.serverUrl}/api/v1/projections`;
1318
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
1319
- const controller = new AbortController();
1320
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1321
- try {
1322
- const headers = {
1323
- [HEADERS.ENVIRONMENT]: this.config.environment,
1324
- };
1325
- if (this.config.auth?.apiKey) {
1326
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1327
- }
1328
- const response = await fetch(url, {
1329
- method: "GET",
1330
- headers,
1331
- signal: controller.signal,
1332
- });
1333
- if (!response.ok) {
1334
- throw new IronflowError(`List projections failed: ${response.status}`, { code: "LIST_PROJECTIONS_FAILED" });
1335
- }
1336
- const data = await response.json();
1337
- const projections = data.projections || [];
1338
- return projections.map((p) => ({
1339
- name: p.name,
1340
- status: p.status,
1341
- mode: p.mode,
1342
- lastEventSeq: p.last_event_seq ?? 0,
1343
- lag: 0,
1344
- errorMessage: p.error_message || undefined,
1345
- updatedAt: p.updated_at ? new Date(p.updated_at) : new Date(),
1346
- }));
1347
- }
1348
- finally {
1349
- clearTimeout(timeoutId);
1350
- }
1351
- }
1352
- /**
1353
- * Subscribe to real-time updates for a projection
1354
- *
1355
- * @example
1356
- * ```typescript
1357
- * const sub = await ironflow.subscribeToProjection<OrderStats>('order-stats', {
1358
- * onUpdate: (state, event) => console.log('Updated:', state),
1359
- * onError: (error) => console.error(error),
1360
- * });
1361
- *
1362
- * // With partition
1363
- * const sub = await ironflow.subscribeToProjection('order-stats', {
1364
- * onUpdate: (state, event) => console.log('Updated:', state),
1365
- * }, { partition: 'customer-123' });
1366
- *
1367
- * // Cleanup
1368
- * sub.unsubscribe();
1369
- * ```
1370
- */
1371
- async subscribeToProjection(name, callbacks, options) {
1372
- this.ensureConfigured();
1373
- // Build the subscription pattern
1374
- let pattern;
1375
- if (options?.partition) {
1376
- pattern = `system.projection.${name}.${options.partition}.updated`;
1377
- }
1378
- else {
1379
- pattern = `system.projection.${name}.>`;
1380
- }
1381
- const sub = await this.subscribe(pattern, {
1382
- onEvent: (event) => {
1383
- const payload = event.data;
1384
- const state = payload.state ?? {};
1385
- callbacks.onUpdate(state, {
1386
- id: payload.last_event_id ?? "",
1387
- name: payload.last_event_name ?? "",
1388
- });
1389
- },
1390
- onError: callbacks.onError
1391
- ? (info) => callbacks.onError(new Error(info.message))
1392
- : undefined,
1393
- replay: options?.replay,
1394
- });
1395
- return sub;
1396
- }
1397
- // ============================================================================
1398
- // KV Store
1399
- // ============================================================================
1400
- /**
1401
- * KV store operations
1402
- *
1403
- * @example
1404
- * ```typescript
1405
- * const kv = ironflow.kv();
1406
- * const bucket = await kv.createBucket({ name: "sessions", ttlSeconds: 3600 });
1407
- * const handle = kv.bucket("sessions");
1408
- * const { revision } = await handle.put("user.123", { token: "abc" });
1409
- * const entry = await handle.get("user.123");
1410
- *
1411
- * // Watch for changes
1412
- * const watcher = handle.watch({
1413
- * onUpdate: (event) => console.log(event),
1414
- * }, { key: "user.*" });
1415
- * ```
1416
- */
1417
- kv() {
1418
- this.ensureConfigured();
1419
- return new BrowserKVClient(this.config);
1420
- }
1421
- // ============================================================================
1422
- // Config Management
1423
- // ============================================================================
1424
- /**
1425
- * Config management operations
1426
- *
1427
- * @example
1428
- * ```typescript
1429
- * const cfg = ironflow.configManager();
1430
- * await cfg.set("app", { featureX: true, maxRetries: 3 });
1431
- * const { data, revision } = await cfg.get("app");
1432
- * await cfg.patch("app", { maxRetries: 5 });
1433
- * const configs = await cfg.list();
1434
- * await cfg.delete("app");
1435
- *
1436
- * // Watch for changes
1437
- * const sub = await cfg.watch("app", {
1438
- * onEvent: (config) => console.log(config),
1439
- * });
1440
- * sub.unsubscribe();
1441
- * ```
1442
- */
1443
- configManager() {
1444
- this.ensureConfigured();
1445
- return new BrowserConfigClient(this.config, (pattern, callbacks) => this.subscribe(pattern, callbacks));
1446
- }
1447
- // ============================================================================
1448
- // Auth Management
1449
- // ============================================================================
1450
- /**
1451
- * API key management operations
1452
- *
1453
- * @example
1454
- * ```typescript
1455
- * // Create an API key
1456
- * const key = await ironflow.apiKeys.create({ name: "my-key" });
1457
- * console.log(key.key); // Only shown once
1458
- *
1459
- * // List API keys
1460
- * const keys = await ironflow.apiKeys.list();
1461
- *
1462
- * // Delete an API key
1463
- * await ironflow.apiKeys.delete(key.id);
1464
- * ```
1465
- */
1466
- apiKeys = {
1467
- /**
1468
- * Create a new API key
1469
- */
1470
- create: async (input) => {
1471
- this.ensureConfigured();
1472
- return this.restRequest("POST", "/api/v1/apikeys", input);
1473
- },
1474
- /**
1475
- * List all API keys
1476
- */
1477
- list: async () => {
1478
- this.ensureConfigured();
1479
- return this.restRequest("GET", "/api/v1/apikeys");
1480
- },
1481
- /**
1482
- * Get an API key by ID
1483
- */
1484
- get: async (id) => {
1485
- this.ensureConfigured();
1486
- return this.restRequest("GET", `/api/v1/apikeys/${encodeURIComponent(id)}`);
1487
- },
1488
- /**
1489
- * Delete an API key
1490
- */
1491
- delete: async (id) => {
1492
- this.ensureConfigured();
1493
- await this.restRequest("DELETE", `/api/v1/apikeys/${encodeURIComponent(id)}`);
1494
- },
1495
- /**
1496
- * Rotate an API key, returning a new secret
1497
- */
1498
- rotate: async (id) => {
1499
- this.ensureConfigured();
1500
- return this.restRequest("POST", `/api/v1/apikeys/${encodeURIComponent(id)}/rotate`);
1501
- },
1502
- };
1503
- /**
1504
- * Organization management operations (enterprise-only)
1505
- *
1506
- * @example
1507
- * ```typescript
1508
- * // Create an organization
1509
- * const org = await ironflow.orgs.create({ name: "Acme Corp" });
1510
- *
1511
- * // List organizations
1512
- * const orgs = await ironflow.orgs.list();
1513
- *
1514
- * // Update an organization
1515
- * await ironflow.orgs.update(org.id, { name: "Acme Inc" });
1516
- * ```
1517
- */
1518
- orgs = {
1519
- /**
1520
- * Create a new organization
1521
- */
1522
- create: async (input) => {
1523
- this.ensureConfigured();
1524
- return this.restRequest("POST", "/api/v1/orgs", input);
1525
- },
1526
- /**
1527
- * List all organizations
1528
- */
1529
- list: async () => {
1530
- this.ensureConfigured();
1531
- return this.restRequest("GET", "/api/v1/orgs");
1532
- },
1533
- /**
1534
- * Get an organization by ID
1535
- */
1536
- get: async (id) => {
1537
- this.ensureConfigured();
1538
- return this.restRequest("GET", `/api/v1/orgs/${encodeURIComponent(id)}`);
1539
- },
1540
- /**
1541
- * Update an organization
1542
- */
1543
- update: async (id, input) => {
1544
- this.ensureConfigured();
1545
- return this.restRequest("PATCH", `/api/v1/orgs/${encodeURIComponent(id)}`, input);
1546
- },
1547
- /**
1548
- * Delete an organization
1549
- */
1550
- delete: async (id) => {
1551
- this.ensureConfigured();
1552
- await this.restRequest("DELETE", `/api/v1/orgs/${encodeURIComponent(id)}`);
1553
- },
1554
- };
1555
- /**
1556
- * Role management operations (enterprise-only)
1557
- *
1558
- * @example
1559
- * ```typescript
1560
- * // Create a role
1561
- * const role = await ironflow.roles.create({ name: "editor", org_id: "org-1" });
1562
- *
1563
- * // Assign a policy to the role
1564
- * await ironflow.roles.assignPolicy(role.id, policyId);
1565
- *
1566
- * // List roles
1567
- * const roles = await ironflow.roles.list();
1568
- * ```
1569
- */
1570
- roles = {
1571
- /**
1572
- * Create a new role
1573
- */
1574
- create: async (input) => {
1575
- this.ensureConfigured();
1576
- return this.restRequest("POST", "/api/v1/roles", input);
1577
- },
1578
- /**
1579
- * List all roles
1580
- */
1581
- list: async (orgId) => {
1582
- this.ensureConfigured();
1583
- const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
1584
- return this.restRequest("GET", `/api/v1/roles${query}`);
1585
- },
1586
- /**
1587
- * Get a role by ID
1588
- */
1589
- get: async (id) => {
1590
- this.ensureConfigured();
1591
- return this.restRequest("GET", `/api/v1/roles/${encodeURIComponent(id)}`);
1592
- },
1593
- /**
1594
- * Update a role
1595
- */
1596
- update: async (id, input) => {
1597
- this.ensureConfigured();
1598
- return this.restRequest("PATCH", `/api/v1/roles/${encodeURIComponent(id)}`, input);
1599
- },
1600
- /**
1601
- * Delete a role
1602
- */
1603
- delete: async (id) => {
1604
- this.ensureConfigured();
1605
- await this.restRequest("DELETE", `/api/v1/roles/${encodeURIComponent(id)}`);
1606
- },
1607
- /**
1608
- * Assign a policy to a role
1609
- */
1610
- assignPolicy: async (roleId, policyId) => {
1611
- this.ensureConfigured();
1612
- await this.restRequest("POST", `/api/v1/roles/${encodeURIComponent(roleId)}/policies`, { policy_id: policyId });
1613
- },
1614
- /**
1615
- * Remove a policy from a role
1616
- */
1617
- removePolicy: async (roleId, policyId) => {
1618
- this.ensureConfigured();
1619
- await this.restRequest("DELETE", `/api/v1/roles/${encodeURIComponent(roleId)}/policies/${encodeURIComponent(policyId)}`);
1620
- },
1621
- };
1622
- /**
1623
- * Policy management operations (enterprise-only)
1624
- *
1625
- * @example
1626
- * ```typescript
1627
- * // Create a policy
1628
- * const policy = await ironflow.policies.create({
1629
- * name: "allow-read",
1630
- * effect: "allow",
1631
- * actions: "read",
1632
- * resources: "*",
1633
- * org_id: "org-1",
1634
- * });
1635
- *
1636
- * // List policies
1637
- * const policies = await ironflow.policies.list();
1638
- * ```
1639
- */
1640
- policies = {
1641
- /**
1642
- * Create a new policy
1643
- */
1644
- create: async (input) => {
1645
- this.ensureConfigured();
1646
- return this.restRequest("POST", "/api/v1/policies", input);
1647
- },
1648
- /**
1649
- * List all policies
1650
- */
1651
- list: async (orgId) => {
1652
- this.ensureConfigured();
1653
- const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
1654
- return this.restRequest("GET", `/api/v1/policies${query}`);
1655
- },
1656
- /**
1657
- * Get a policy by ID
1658
- */
1659
- get: async (id) => {
1660
- this.ensureConfigured();
1661
- return this.restRequest("GET", `/api/v1/policies/${encodeURIComponent(id)}`);
1662
- },
1663
- /**
1664
- * Update a policy
1665
- */
1666
- update: async (id, input) => {
1667
- this.ensureConfigured();
1668
- return this.restRequest("PATCH", `/api/v1/policies/${encodeURIComponent(id)}`, input);
1669
- },
1670
- /**
1671
- * Delete a policy
1672
- */
1673
- delete: async (id) => {
1674
- this.ensureConfigured();
1675
- await this.restRequest("DELETE", `/api/v1/policies/${encodeURIComponent(id)}`);
1676
- },
1677
- };
1678
- /**
1679
- * Event schema registry operations
1680
- *
1681
- * @example
1682
- * ```typescript
1683
- * // Register a schema
1684
- * const schema = await ironflow.schemas.register({
1685
- * name: "order.placed",
1686
- * version: 1,
1687
- * schema: { type: "object", properties: { orderId: { type: "string" } } },
1688
- * });
1689
- *
1690
- * // List all schemas
1691
- * const schemas = await ironflow.schemas.list();
1692
- *
1693
- * // Get latest version of a schema
1694
- * const latest = await ironflow.schemas.get("order.placed");
1695
- *
1696
- * // Test an upcast transformation
1697
- * const result = await ironflow.schemas.testUpcast({
1698
- * eventName: "order.placed",
1699
- * fromVersion: 1,
1700
- * toVersion: 2,
1701
- * data: { orderId: "123" },
1702
- * });
1703
- * ```
1704
- */
1705
- schemas = {
1706
- /** Register a new event schema (or a new version of an existing schema) */
1707
- register: async (input) => {
1708
- this.ensureConfigured();
1709
- return this.restRequest("POST", "/api/v1/events/schemas", {
1710
- event_name: input.name,
1711
- version: input.version,
1712
- schema_json: JSON.stringify(input.schema),
1713
- });
1714
- },
1715
- /** List all registered event schemas */
1716
- list: async () => {
1717
- this.ensureConfigured();
1718
- const resp = await this.restRequest("GET", "/api/v1/events/schemas");
1719
- return resp.schemas ?? [];
1720
- },
1721
- /** Get the latest version of an event schema by name */
1722
- get: async (name) => {
1723
- this.ensureConfigured();
1724
- return this.restRequest("GET", `/api/v1/events/schemas/${encodeURIComponent(name)}`);
1725
- },
1726
- /** Get a specific version of an event schema */
1727
- getVersion: async (name, version) => {
1728
- this.ensureConfigured();
1729
- return this.restRequest("GET", `/api/v1/events/schemas/${encodeURIComponent(name)}/${version}`);
1730
- },
1731
- /** Delete a specific version of an event schema */
1732
- delete: async (name, version) => {
1733
- this.ensureConfigured();
1734
- await this.restRequest("DELETE", `/api/v1/events/schemas/${encodeURIComponent(name)}/${version}`);
1735
- },
1736
- /** Test an upcast transformation between two schema versions */
1737
- testUpcast: async (input) => {
1738
- this.ensureConfigured();
1739
- return this.restRequest("POST", "/api/v1/events/upcast", input);
1740
- },
1741
- };
1742
- // ============================================================================
1743
- // Audit Trail
1744
- // ============================================================================
1745
- /**
1746
- * Get the audit trail for a run.
1747
- *
1748
- * @param runId The run ID to retrieve the audit trail for
1749
- * @param options Optional filtering options
1750
- *
1751
- * @example
1752
- * ```typescript
1753
- * const result = await ironflow.getAuditTrail("run-abc123");
1754
- * for (const event of result.events) {
1755
- * console.log(event.eventType, event.createdAt);
1756
- * }
1757
- * ```
1758
- */
1759
- async getAuditTrail(runId, options) {
1760
- this.ensureConfigured();
1761
- const response = await this.streamRequest("/ironflow.v1.AuditService/GetAuditTrail", {
1762
- run_id: runId,
1763
- event_type: options?.eventType ?? "",
1764
- from_timestamp: options?.fromTimestamp ?? "",
1765
- to_timestamp: options?.toTimestamp ?? "",
1766
- limit: options?.limit ?? 0,
1767
- cursor: options?.cursor ?? "",
1768
- });
1769
- return {
1770
- events: (response.events ?? []).map((e) => ({
1771
- id: e.id,
1772
- runId: e.run_id,
1773
- functionId: e.function_id,
1774
- stepId: e.step_id,
1775
- eventType: e.event_type,
1776
- payload: e.payload ?? {},
1777
- metadata: e.metadata,
1778
- createdAt: e.created_at,
1779
- })),
1780
- totalCount: response.total_count ?? 0,
1781
- nextCursor: response.next_cursor,
1782
- };
1783
- }
1784
- // ============================================================================
1785
- // Webhook Management
1786
- // ============================================================================
1787
- /**
1788
- * Webhook management operations
1789
- *
1790
- * @example
1791
- * ```typescript
1792
- * // List all webhook sources
1793
- * const sources = await ironflow.webhooks.listSources();
1794
- *
1795
- * // Delete a webhook source
1796
- * await ironflow.webhooks.deleteSource("my-webhook");
1797
- *
1798
- * // List deliveries
1799
- * const { deliveries } = await ironflow.webhooks.listDeliveries({ sourceId: "my-webhook" });
1800
- * ```
1801
- */
1802
- webhooks = {
1803
- /** Create a new webhook source */
1804
- create: async (input) => {
1805
- this.ensureConfigured();
1806
- const response = await this.streamRequest("/ironflow.v1.WebhookService/CreateWebhookSource", {
1807
- id: input.id,
1808
- event_prefix: input.eventPrefix,
1809
- verify_header: input.verifyHeader ?? "",
1810
- verify_algorithm: input.verifyAlgorithm ?? "",
1811
- verify_secret: input.verifySecret ?? "",
1812
- metadata: input.metadata,
1813
- });
1814
- return {
1815
- id: response.id,
1816
- eventPrefix: response.event_prefix,
1817
- verifyHeader: response.verify_header,
1818
- verifyAlgorithm: response.verify_algorithm,
1819
- sourceType: response.source_type,
1820
- metadata: response.metadata,
1821
- createdAt: response.created_at,
1822
- updatedAt: response.updated_at,
1823
- };
1824
- },
1825
- /** List all registered webhook sources */
1826
- listSources: async () => {
1827
- this.ensureConfigured();
1828
- const response = await this.streamRequest("/ironflow.v1.WebhookService/ListWebhookSources", { limit: 0, offset: 0 });
1829
- return (response.sources ?? []).map((s) => ({
1830
- id: s.id,
1831
- eventPrefix: s.event_prefix,
1832
- verifyHeader: s.verify_header,
1833
- verifyAlgorithm: s.verify_algorithm,
1834
- sourceType: s.source_type,
1835
- metadata: s.metadata,
1836
- createdAt: s.created_at,
1837
- updatedAt: s.updated_at,
1838
- }));
1839
- },
1840
- /** Delete a webhook source by ID */
1841
- deleteSource: async (id) => {
1842
- this.ensureConfigured();
1843
- await this.streamRequest("/ironflow.v1.WebhookService/DeleteWebhookSource", { id });
1844
- },
1845
- /** List webhook deliveries with optional filtering */
1846
- listDeliveries: async (opts) => {
1847
- this.ensureConfigured();
1848
- const response = await this.streamRequest("/ironflow.v1.WebhookService/ListWebhookDeliveries", {
1849
- source_id: opts?.sourceId ?? "",
1850
- status: opts?.status ?? "",
1851
- limit: opts?.limit ?? 0,
1852
- offset: opts?.offset ?? 0,
1853
- });
1854
- return {
1855
- deliveries: (response.deliveries ?? []).map((d) => ({
1856
- id: d.id,
1857
- sourceId: d.source_id,
1858
- externalId: d.external_id,
1859
- status: d.status,
1860
- eventId: d.event_id,
1861
- error: d.error,
1862
- createdAt: d.created_at,
1863
- })),
1864
- totalCount: response.total_count ?? 0,
1865
- };
1866
- },
1867
- };
1868
- // ============================================================================
1869
- // User Management
1870
- // ============================================================================
1871
- /**
1872
- * User management operations
1873
- *
1874
- * @example
1875
- * ```typescript
1876
- * // List all users
1877
- * const users = await ironflow.users.list();
1878
- *
1879
- * // Create a user
1880
- * const user = await ironflow.users.create({
1881
- * email: "alice@example.com",
1882
- * password: "secret",
1883
- * roles: ["admin"],
1884
- * });
1885
- *
1886
- * // Update a user
1887
- * await ironflow.users.update(user.id, { name: "Alice" });
1888
- * ```
1889
- */
1890
- users = {
1891
- /** Create a new user (admin only) */
1892
- create: async (input) => {
1893
- this.ensureConfigured();
1894
- return this.restRequest("POST", "/api/v1/users", input);
1895
- },
1896
- /** List all users in the current organization (admin only) */
1897
- list: async () => {
1898
- this.ensureConfigured();
1899
- return this.restRequest("GET", "/api/v1/users");
1900
- },
1901
- /** Get a user by ID */
1902
- get: async (id) => {
1903
- this.ensureConfigured();
1904
- return this.restRequest("GET", `/api/v1/users/${encodeURIComponent(id)}`);
1905
- },
1906
- /** Update a user's profile (admin only) */
1907
- update: async (id, input) => {
1908
- this.ensureConfigured();
1909
- return this.restRequest("PATCH", `/api/v1/users/${encodeURIComponent(id)}`, input);
1910
- },
1911
- /** Delete a user (admin only) */
1912
- delete: async (id) => {
1913
- this.ensureConfigured();
1914
- await this.restRequest("DELETE", `/api/v1/users/${encodeURIComponent(id)}`);
1915
- },
1916
- };
1917
- /**
1918
- * Tenant management operations (enterprise-only)
1919
- *
1920
- * @example
1921
- * ```typescript
1922
- * // List all tenants
1923
- * const tenants = await ironflow.tenants.list();
1924
- * ```
1925
- */
1926
- tenants = {
1927
- /** List all tenants (enterprise-only) */
1928
- list: async () => {
1929
- this.ensureConfigured();
1930
- return this.restRequest("GET", "/api/v1/tenants");
1931
- },
1932
- };
1933
- // ============================================================================
1934
- // Pattern Helpers (static)
1935
- // ============================================================================
1936
- /**
1937
- * Pattern helpers for building subscription patterns
1938
- */
1939
- static patterns = patterns;
1940
- // ============================================================================
1941
- // Internal Methods
1942
- // ============================================================================
1943
- ensureConfigured() {
1944
- if (!this.config) {
1945
- throw new NotConfiguredError();
1946
- }
1947
- }
1948
- cleanup() {
1949
- if (this.visibilityHandler) {
1950
- document.removeEventListener("visibilitychange", this.visibilityHandler);
1951
- this.visibilityHandler = null;
1952
- }
1953
- if (this.subscriptionManager) {
1954
- this.subscriptionManager.disconnect();
1955
- this.subscriptionManager = null;
1956
- }
1957
- this.transport = null;
1958
- }
1959
- /**
1960
- * Reset client state for testing. Not intended for production use.
1961
- * @internal
1962
- */
1963
- _resetForTesting() {
1964
- this.cleanup();
1965
- this.config = null;
1966
- this.logger = createNoopLogger();
1967
- }
1968
- setupVisibilityHandling() {
1969
- this.visibilityHandler = () => {
1970
- if (document.hidden) {
1971
- this.logger.debug("Tab hidden, pausing subscriptions");
1972
- this.subscriptionManager?.pause();
1973
- }
1974
- else {
1975
- if (this.config?.visibility.reconnectOnVisible) {
1976
- this.logger.debug("Tab visible, resuming subscriptions");
1977
- this.subscriptionManager?.resume();
1978
- }
1979
- }
1980
- };
1981
- document.addEventListener("visibilitychange", this.visibilityHandler);
1982
- }
1983
- async request(schema, method, path, body) {
1984
- const url = `${this.config.serverUrl}${path}`;
1985
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
1986
- const controller = new AbortController();
1987
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1988
- try {
1989
- const headers = {
1990
- "Content-Type": "application/json",
1991
- [HEADERS.ENVIRONMENT]: this.config.environment,
1992
- };
1993
- if (this.config.auth?.apiKey) {
1994
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
1995
- }
1996
- else if (this.config.auth?.token) {
1997
- headers["Authorization"] = `Bearer ${this.config.auth.token}`;
1998
- }
1999
- const response = await fetch(url, {
2000
- method,
2001
- headers,
2002
- body: JSON.stringify(body),
2003
- signal: controller.signal,
2004
- });
2005
- const responseBody = await response.text();
2006
- if (!response.ok) {
2007
- const errorResult = ErrorResponseSchema.safeParse(safeJsonParse(responseBody));
2008
- const errorData = errorResult.success
2009
- ? errorResult.data
2010
- : { message: responseBody };
2011
- throw new IronflowError(errorData.message ?? `Request failed: ${response.status}`, {
2012
- code: errorData.code ?? `HTTP_${response.status}`,
2013
- retryable: response.status >= 500,
2014
- });
2015
- }
2016
- const parsed = safeJsonParse(responseBody);
2017
- if (parsed === undefined) {
2018
- throw new ValidationError("Invalid JSON response from server");
2019
- }
2020
- const result = schema.safeParse(parsed);
2021
- if (!result.success) {
2022
- const issues = result.error.issues
2023
- .map((i) => `${i.path.join(".")}: ${i.message}`)
2024
- .join(", ");
2025
- throw new ValidationError(`Invalid response from server: ${issues}`);
2026
- }
2027
- return result.data;
2028
- }
2029
- catch (error) {
2030
- if (error instanceof IronflowError || error instanceof ValidationError) {
2031
- throw error;
2032
- }
2033
- if (error instanceof Error && error.name === "AbortError") {
2034
- throw new IronflowError(`Request timeout after ${timeout}ms for ${method} ${path}`, {
2035
- code: "TIMEOUT",
2036
- retryable: true,
2037
- });
2038
- }
2039
- throw new IronflowError(error instanceof Error
2040
- ? `${method} ${path} failed: ${error.message}`
2041
- : `${method} ${path} failed`, {
2042
- code: "REQUEST_FAILED",
2043
- retryable: true,
2044
- cause: error instanceof Error ? error : undefined,
2045
- });
2046
- }
2047
- finally {
2048
- clearTimeout(timeoutId);
2049
- }
2050
- }
2051
- async streamRequest(path, body) {
2052
- const url = `${this.config.serverUrl}${path}`;
2053
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
2054
- const controller = new AbortController();
2055
- const timeoutId = setTimeout(() => controller.abort(), timeout);
2056
- try {
2057
- const headers = {
2058
- "Content-Type": "application/json",
2059
- [HEADERS.ENVIRONMENT]: this.config.environment,
2060
- };
2061
- if (this.config.auth?.apiKey) {
2062
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
2063
- }
2064
- else if (this.config.auth?.token) {
2065
- headers["Authorization"] = `Bearer ${this.config.auth.token}`;
2066
- }
2067
- const response = await fetch(url, {
2068
- method: "POST",
2069
- headers,
2070
- body: JSON.stringify(body),
2071
- signal: controller.signal,
2072
- });
2073
- if (!response.ok) {
2074
- const errorBody = safeJsonParse(await response.text());
2075
- throw new IronflowError(errorBody?.message ?? `Request failed: ${response.status}`, {
2076
- code: errorBody?.code ?? `HTTP_${response.status}`,
2077
- retryable: response.status >= 500,
2078
- });
2079
- }
2080
- return response.json();
2081
- }
2082
- catch (error) {
2083
- if (error instanceof IronflowError) {
2084
- throw error;
2085
- }
2086
- if (error instanceof Error && error.name === "AbortError") {
2087
- throw new IronflowError(`Request timeout after ${timeout}ms for POST ${path}`, { code: "TIMEOUT", retryable: true });
2088
- }
2089
- throw new IronflowError(error instanceof Error
2090
- ? `POST ${path} failed: ${error.message}`
2091
- : `POST ${path} failed`, {
2092
- code: "REQUEST_FAILED",
2093
- retryable: true,
2094
- cause: error instanceof Error ? error : undefined,
2095
- });
2096
- }
2097
- finally {
2098
- clearTimeout(timeoutId);
2099
- }
2100
- }
2101
- /**
2102
- * REST request helper for auth management endpoints.
2103
- *
2104
- * Supports GET, POST, PATCH, DELETE methods with typed error mapping:
2105
- * - 401 → UnauthenticatedError
2106
- * - 402 → EnterpriseRequiredError
2107
- * - 403 → UnauthorizedError
2108
- */
2109
- async restRequest(method, path, body) {
2110
- const url = `${this.config.serverUrl}${path}`;
2111
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
2112
- const controller = new AbortController();
2113
- const timeoutId = setTimeout(() => controller.abort(), timeout);
2114
- try {
2115
- const headers = {
2116
- [HEADERS.ENVIRONMENT]: this.config.environment,
2117
- };
2118
- if (body !== undefined) {
2119
- headers["Content-Type"] = "application/json";
2120
- }
2121
- if (this.config.auth?.apiKey) {
2122
- headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
2123
- }
2124
- else if (this.config.auth?.token) {
2125
- headers["Authorization"] = `Bearer ${this.config.auth.token}`;
2126
- }
2127
- const fetchOptions = {
2128
- method,
2129
- headers,
2130
- signal: controller.signal,
2131
- };
2132
- if (body !== undefined) {
2133
- fetchOptions.body = JSON.stringify(body);
2134
- }
2135
- const response = await fetch(url, fetchOptions);
2136
- // Handle 204 No Content
2137
- if (response.status === 204) {
2138
- return undefined;
2139
- }
2140
- if (!response.ok) {
2141
- const errorText = await response.text();
2142
- let errorMessage = `Request failed with status ${response.status}`;
2143
- if (errorText) {
2144
- const errorJson = safeJsonParse(errorText);
2145
- if (errorJson?.message) {
2146
- errorMessage = errorJson.message;
2147
- }
2148
- else if (errorText) {
2149
- errorMessage = errorText;
2150
- }
2151
- }
2152
- switch (response.status) {
2153
- case 401:
2154
- throw new UnauthenticatedError(errorMessage);
2155
- case 402:
2156
- throw new EnterpriseRequiredError(errorMessage);
2157
- case 403:
2158
- throw new UnauthorizedError(errorMessage);
2159
- default:
2160
- throw new IronflowError(errorMessage, {
2161
- code: `HTTP_${response.status}`,
2162
- retryable: response.status >= 500,
2163
- });
2164
- }
2165
- }
2166
- return response.json();
2167
- }
2168
- catch (error) {
2169
- if (error instanceof IronflowError ||
2170
- error instanceof UnauthenticatedError ||
2171
- error instanceof EnterpriseRequiredError ||
2172
- error instanceof UnauthorizedError) {
2173
- throw error;
2174
- }
2175
- if (error instanceof Error && error.name === "AbortError") {
2176
- throw new IronflowError(`Request timeout after ${timeout}ms for ${method} ${path}`, { code: "TIMEOUT", retryable: true });
2177
- }
2178
- throw new IronflowError(error instanceof Error
2179
- ? `${method} ${path} failed: ${error.message}`
2180
- : `${method} ${path} failed`, {
2181
- code: "REQUEST_FAILED",
2182
- retryable: true,
2183
- cause: error instanceof Error ? error : undefined,
2184
- });
2185
- }
2186
- finally {
2187
- clearTimeout(timeoutId);
2188
- }
2189
- }
2190
- mapRunResponse(response) {
2191
- // ConnectRPC returns proto enum strings like "RUN_STATUS_COMPLETED" — normalize to "completed"
2192
- const rawStatus = response.status.toLowerCase().replace(/^run_status_/, "");
2193
- const statusResult = RunStatusSchema.safeParse(rawStatus);
2194
- const status = statusResult.success ? statusResult.data : "failed";
2195
- return {
2196
- id: response.id,
2197
- functionId: response.functionId,
2198
- eventId: response.eventId,
2199
- status,
2200
- attempt: response.attempt,
2201
- maxAttempts: response.maxAttempts,
2202
- input: response.input,
2203
- output: response.output,
2204
- error: response.error,
2205
- startedAt: response.startedAt ? new Date(response.startedAt) : undefined,
2206
- endedAt: response.endedAt ? new Date(response.endedAt) : undefined,
2207
- createdAt: new Date(response.createdAt),
2208
- updatedAt: new Date(response.updatedAt),
2209
- };
2210
- }
2211
- }
2212
- /**
2213
- * Singleton instance
2214
- */
2215
- export const ironflow = new IronflowClient();
2216
- /**
2217
- * Export the class for advanced usage
2218
- */
2219
- export { IronflowClient };
2220
- //# sourceMappingURL=client.js.map