@ironflow/browser 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1045 -98
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# @ironflow/browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Private browser client for [Ironflow](https://github.com/sahina/ironflow), an event-driven backend platform. Provides real-time subscriptions, workflow triggers, event emission, entity streams, projections, KV store, config management, and auth management for web applications.
|
|
4
|
+
|
|
5
|
+
This is a **private npm package**. This README is the sole reference for coding agents integrating with the browser SDK.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [Connection Management](#connection-management)
|
|
12
|
+
- [Events and Subscriptions](#events-and-subscriptions)
|
|
13
|
+
- [Emitting Events](#emitting-events)
|
|
14
|
+
- [Workflow Operations](#workflow-operations)
|
|
15
|
+
- [Entity Streams (Event Sourcing)](#entity-streams-event-sourcing)
|
|
16
|
+
- [Projections](#projections)
|
|
17
|
+
- [KV Store](#kv-store)
|
|
18
|
+
- [Config Management](#config-management)
|
|
19
|
+
- [Auth Management](#auth-management)
|
|
20
|
+
- [Server Inspection](#server-inspection)
|
|
21
|
+
- [React Integration Patterns](#react-integration-patterns)
|
|
22
|
+
- [Transport Configuration](#transport-configuration)
|
|
23
|
+
- [Error Handling](#error-handling)
|
|
24
|
+
- [Browser Compatibility](#browser-compatibility)
|
|
4
25
|
|
|
5
26
|
## Installation
|
|
6
27
|
|
|
@@ -8,177 +29,1072 @@ Browser client for [Ironflow](https://github.com/sahina/ironflow), an event-driv
|
|
|
8
29
|
npm install @ironflow/browser
|
|
9
30
|
```
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
The package re-exports commonly used types from `@ironflow/core`, so most applications only need this single dependency.
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Call `ironflow.configure()` once at application startup before any other operations. The client is a singleton.
|
|
12
37
|
|
|
13
38
|
```typescript
|
|
14
39
|
import { ironflow } from '@ironflow/browser';
|
|
15
40
|
|
|
16
|
-
|
|
41
|
+
ironflow.configure({
|
|
42
|
+
serverUrl: 'http://localhost:9123', // Default: 'http://localhost:9123'
|
|
43
|
+
transport: 'connectrpc', // Default: 'connectrpc'. Options: 'connectrpc' | 'websocket'
|
|
44
|
+
environment: 'default', // Default: 'default'. Target environment for isolation.
|
|
45
|
+
timeout: 30000, // Request timeout in ms (default: 30000)
|
|
46
|
+
auth: {
|
|
47
|
+
apiKey: 'your-api-key', // API key for authentication
|
|
48
|
+
token: 'bearer-token', // Alternative: bearer token
|
|
49
|
+
},
|
|
50
|
+
reconnect: {
|
|
51
|
+
enabled: true, // Default: true
|
|
52
|
+
maxAttempts: 10, // Default: 10. Use -1 for infinite.
|
|
53
|
+
backoff: {
|
|
54
|
+
initial: 1000, // Default: 1000ms
|
|
55
|
+
max: 30000, // Default: 30000ms
|
|
56
|
+
multiplier: 2, // Default: 2
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
visibility: {
|
|
60
|
+
pauseOnHidden: true, // Default: true. Pause subscriptions when tab is hidden.
|
|
61
|
+
reconnectOnVisible: true, // Default: true. Resume when tab becomes visible.
|
|
62
|
+
},
|
|
63
|
+
logger: false, // Default: console logger with [ironflow] prefix. Pass false to disable.
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also pass `reconnect: false` as shorthand to disable reconnection entirely.
|
|
68
|
+
|
|
69
|
+
### Transport Auto-Detection
|
|
70
|
+
|
|
71
|
+
Use `detectTransport()` to probe the server and choose the best available transport. ConnectRPC is preferred over WebSocket.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const transport = await ironflow.detectTransport();
|
|
75
|
+
// Returns 'connectrpc' | 'websocket'
|
|
76
|
+
|
|
17
77
|
ironflow.configure({
|
|
18
78
|
serverUrl: 'http://localhost:9123',
|
|
79
|
+
transport,
|
|
19
80
|
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Reading Configuration
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const config = ironflow.getConfig(); // Returns IronflowConfig. Throws NotConfiguredError if not configured.
|
|
87
|
+
const configured = ironflow.isConfigured; // boolean
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Connection Management
|
|
20
91
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
92
|
+
```typescript
|
|
93
|
+
// Explicitly connect (subscriptions auto-connect, but you can call this eagerly)
|
|
94
|
+
await ironflow.connect();
|
|
95
|
+
|
|
96
|
+
// Disconnect and clean up all subscriptions
|
|
97
|
+
ironflow.disconnect();
|
|
98
|
+
|
|
99
|
+
// Monitor connection state changes
|
|
100
|
+
const unsubscribe = ironflow.onConnectionChange((state) => {
|
|
101
|
+
// state: 'connected' | 'disconnected' | 'connecting' | 'reconnecting'
|
|
102
|
+
console.log('Connection state:', state);
|
|
24
103
|
});
|
|
25
104
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
105
|
+
// Stop listening
|
|
106
|
+
unsubscribe();
|
|
107
|
+
|
|
108
|
+
// Read current connection state
|
|
109
|
+
const state = ironflow.connectionState;
|
|
110
|
+
// Returns 'connected' | 'disconnected' | 'connecting' | 'reconnecting'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Global Error Handler
|
|
114
|
+
|
|
115
|
+
Register a global handler that fires for all subscription errors:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const unsubscribe = ironflow.onError((error) => {
|
|
119
|
+
// error: { message: string; code: string; retryable?: boolean }
|
|
120
|
+
console.error('Ironflow error:', error.message, error.code);
|
|
29
121
|
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Events and Subscriptions
|
|
30
125
|
|
|
31
|
-
|
|
32
|
-
|
|
126
|
+
### Basic Subscription
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { ironflow } from '@ironflow/browser';
|
|
130
|
+
|
|
131
|
+
const sub = await ironflow.subscribe('events:order.*', {
|
|
132
|
+
onEvent: (event) => {
|
|
133
|
+
console.log('Event:', event.topic, event.data);
|
|
134
|
+
},
|
|
135
|
+
onError: (error) => {
|
|
136
|
+
console.error('Subscription error:', error.message);
|
|
137
|
+
},
|
|
138
|
+
onStateChange: (state) => {
|
|
139
|
+
console.log('Subscription connection state:', state);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
33
142
|
|
|
34
143
|
// Cleanup
|
|
35
144
|
sub.unsubscribe();
|
|
36
145
|
```
|
|
37
146
|
|
|
38
|
-
|
|
147
|
+
### Subscription Options
|
|
39
148
|
|
|
40
|
-
|
|
41
|
-
- **Workflow triggers** and full run management (cancel, retry, resume)
|
|
42
|
-
- **Event emission** for workflow coordination
|
|
43
|
-
- **Entity streams** for event sourcing (append, read, subscribe)
|
|
44
|
-
- **Projections** with real-time state subscriptions and rebuild support
|
|
45
|
-
- **KV store** for distributed key-value storage with bucket management
|
|
46
|
-
- **Config management** for centralized configuration (set, get, patch, delete, watch)
|
|
47
|
-
- **Consumer groups** for load-balanced event processing
|
|
48
|
-
- **Auto-reconnect** with exponential backoff and visibility handling
|
|
49
|
-
- **Type-safe** API with full TypeScript support
|
|
149
|
+
All options from `SubscribeOptions` plus `trackState` (browser-specific):
|
|
50
150
|
|
|
51
|
-
|
|
151
|
+
```typescript
|
|
152
|
+
const sub = await ironflow.subscribe('events:order.*', {
|
|
153
|
+
onEvent: (event) => { /* ... */ },
|
|
154
|
+
|
|
155
|
+
// Replay the last N historical events on connect
|
|
156
|
+
replay: 100,
|
|
52
157
|
|
|
53
|
-
|
|
158
|
+
// Include event metadata (timestamp, sequence)
|
|
159
|
+
includeMetadata: true,
|
|
160
|
+
|
|
161
|
+
// CEL expression for server-side content-based filtering
|
|
162
|
+
filter: 'data.amount > 100',
|
|
163
|
+
|
|
164
|
+
// Namespace for the subscription (default: "default")
|
|
165
|
+
namespace: 'production',
|
|
166
|
+
|
|
167
|
+
// Consumer group for load-balanced delivery (see Consumer Groups below)
|
|
168
|
+
consumerGroup: 'order-processors',
|
|
169
|
+
|
|
170
|
+
// Acknowledgment mode: 'auto' (default) | 'manual'
|
|
171
|
+
ackMode: 'manual',
|
|
172
|
+
|
|
173
|
+
// Backpressure handling: 'buffer' (default) | 'drop'
|
|
174
|
+
backpressure: 'buffer',
|
|
175
|
+
|
|
176
|
+
// Browser-specific: track last event for state access
|
|
177
|
+
trackState: true,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// When trackState is true, access the last received event:
|
|
181
|
+
console.log(sub.lastEvent);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Multiple Patterns
|
|
185
|
+
|
|
186
|
+
Subscribe to an array of patterns. Returns a combined subscription that unsubscribes from all at once:
|
|
54
187
|
|
|
55
188
|
```typescript
|
|
56
|
-
ironflow.
|
|
57
|
-
|
|
58
|
-
|
|
189
|
+
const sub = await ironflow.subscribe(
|
|
190
|
+
['system.run.*', 'events:order.*', 'events:payment.*'],
|
|
191
|
+
{
|
|
192
|
+
onEvent: (event) => {
|
|
193
|
+
console.log('Received:', event.topic);
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Unsubscribes from all three patterns
|
|
199
|
+
sub.unsubscribe();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Pattern Helpers
|
|
203
|
+
|
|
204
|
+
Use the `patterns` utility to build subscription patterns. Available as a static property on the client class and as a direct import:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { ironflow, patterns } from '@ironflow/browser';
|
|
208
|
+
|
|
209
|
+
// System run patterns
|
|
210
|
+
patterns.allRuns() // 'system.run.>'
|
|
211
|
+
patterns.run('run_abc123') // 'system.run.run_abc123.>'
|
|
212
|
+
patterns.runLifecycle('run_abc123') // 'system.run.run_abc123.*'
|
|
213
|
+
patterns.runSteps('run_abc123') // 'system.run.run_abc123.step.>'
|
|
214
|
+
|
|
215
|
+
// Function patterns
|
|
216
|
+
patterns.allFunctions() // 'system.function.>'
|
|
217
|
+
patterns.function('process-order') // 'system.function.process-order.>'
|
|
218
|
+
|
|
219
|
+
// User event patterns
|
|
220
|
+
patterns.userEvent('order.*') // 'events:order.*'
|
|
221
|
+
patterns.allUserEvents() // 'events:>'
|
|
222
|
+
|
|
223
|
+
// Secret patterns
|
|
224
|
+
patterns.allSecrets() // 'system.secret.*'
|
|
225
|
+
patterns.secret('db-password') // 'system.secret.db-password.*'
|
|
226
|
+
patterns.secretAction('updated') // 'system.secret.*.updated'
|
|
227
|
+
|
|
228
|
+
// Developer pub/sub topic patterns
|
|
229
|
+
patterns.topic('chat.room-1') // 'topic:chat.room-1'
|
|
230
|
+
patterns.allTopics() // 'topic:>'
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Subscription Groups
|
|
234
|
+
|
|
235
|
+
Batch-manage multiple subscriptions for easy cleanup:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const group = ironflow.subscriptionGroup();
|
|
239
|
+
|
|
240
|
+
await group.add('system.run.*', {
|
|
241
|
+
onEvent: (event) => console.log('Run event:', event),
|
|
59
242
|
});
|
|
60
243
|
|
|
61
|
-
|
|
62
|
-
|
|
244
|
+
await group.add('events:payment.*', {
|
|
245
|
+
onEvent: (event) => console.log('Payment event:', event),
|
|
246
|
+
replay: 10,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await group.add('events:order.*', {
|
|
250
|
+
onEvent: (event) => console.log('Order event:', event),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Unsubscribe from all at once
|
|
254
|
+
group.unsubscribeAll();
|
|
63
255
|
```
|
|
64
256
|
|
|
65
|
-
|
|
257
|
+
### Consumer Groups
|
|
66
258
|
|
|
67
|
-
|
|
259
|
+
Join a consumer group for load-balanced event processing across multiple browser tabs or clients. Consumer group subscriptions always use manual acknowledgment:
|
|
68
260
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
261
|
+
```typescript
|
|
262
|
+
const sub = await ironflow.joinConsumerGroup(
|
|
263
|
+
'order-processors', // group name
|
|
264
|
+
'events:order.created', // pattern
|
|
265
|
+
{
|
|
266
|
+
onEvent: (event) => {
|
|
267
|
+
console.log('Processing order:', event.data);
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
);
|
|
75
271
|
|
|
76
|
-
|
|
272
|
+
// Returns AckableSubscription
|
|
273
|
+
sub.ack(eventId); // Acknowledge successful processing
|
|
274
|
+
sub.nak(eventId, 5000); // Negative ack with optional redelivery delay (ms)
|
|
275
|
+
sub.term(eventId); // Terminate - do not redeliver
|
|
77
276
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
| `ironflow.trigger(functionId, options)` | Trigger a workflow |
|
|
81
|
-
| `ironflow.getRun(runId)` | Get run status |
|
|
82
|
-
| `ironflow.listRuns(options?)` | List runs with filtering |
|
|
83
|
-
| `ironflow.cancelRun(runId, reason?)` | Cancel a running workflow |
|
|
84
|
-
| `ironflow.retryRun(runId, fromStep?)` | Retry a failed run |
|
|
85
|
-
| `ironflow.resumeRun(runId, fromStep?)` | Resume a paused/failed run |
|
|
86
|
-
| `ironflow.patchStep(stepId, output, reason?)` | Hot-patch a step's output |
|
|
277
|
+
sub.unsubscribe();
|
|
278
|
+
```
|
|
87
279
|
|
|
88
|
-
|
|
280
|
+
Alternatively, use `subscribe` directly with `consumerGroup` and `ackMode` options:
|
|
89
281
|
|
|
90
282
|
```typescript
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
expectedVersion: 3, // Optimistic concurrency
|
|
283
|
+
const sub = await ironflow.subscribe('events:order.created', {
|
|
284
|
+
onEvent: (event) => { /* ... */ },
|
|
285
|
+
consumerGroup: 'order-processors',
|
|
286
|
+
ackMode: 'manual',
|
|
96
287
|
});
|
|
97
288
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
289
|
+
// sub is AckableSubscription when ackMode is 'manual'
|
|
290
|
+
const ackableSub = sub as AckableSubscription;
|
|
291
|
+
ackableSub.ack(eventId);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Emitting Events
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { ironflow } from '@ironflow/browser';
|
|
298
|
+
|
|
299
|
+
// Basic emit
|
|
300
|
+
const result = await ironflow.emit('order.approved', {
|
|
301
|
+
orderId: '123',
|
|
302
|
+
approvedBy: 'user@example.com',
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
console.log(result.eventId); // Unique event ID assigned by server
|
|
306
|
+
console.log(result.runIds); // IDs of any workflow runs triggered by this event
|
|
307
|
+
|
|
308
|
+
// With options
|
|
309
|
+
const result = await ironflow.emit(
|
|
310
|
+
'order.approved',
|
|
311
|
+
{ orderId: '123', approvedBy: 'user@example.com' },
|
|
312
|
+
{
|
|
313
|
+
version: 2, // Event schema version (default: 1)
|
|
314
|
+
idempotencyKey: 'order-123-approval', // Deduplication key
|
|
315
|
+
metadata: { source: 'dashboard' }, // Arbitrary metadata
|
|
316
|
+
namespace: 'production', // Namespace (default: "default")
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Workflow Operations
|
|
322
|
+
|
|
323
|
+
### Trigger a Workflow
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { ironflow } from '@ironflow/browser';
|
|
327
|
+
|
|
328
|
+
// Trigger with typed input
|
|
329
|
+
const result = await ironflow.trigger<{ orderId: string }>('process-order', {
|
|
330
|
+
data: { orderId: '123' },
|
|
102
331
|
});
|
|
103
332
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
333
|
+
console.log(result.runIds); // ['run_abc123']
|
|
334
|
+
console.log(result.eventId); // Event ID that triggered the run
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Get Run Status
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const run = await ironflow.getRun('run_abc123');
|
|
341
|
+
|
|
342
|
+
console.log(run.id); // 'run_abc123'
|
|
343
|
+
console.log(run.functionId); // 'process-order'
|
|
344
|
+
console.log(run.status); // 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
345
|
+
console.log(run.attempt); // Current attempt number
|
|
346
|
+
console.log(run.maxAttempts); // Maximum retry attempts
|
|
347
|
+
console.log(run.input); // Input data
|
|
348
|
+
console.log(run.output); // Output data (if completed)
|
|
349
|
+
console.log(run.error); // Error message (if failed)
|
|
350
|
+
console.log(run.startedAt); // Date | undefined
|
|
351
|
+
console.log(run.endedAt); // Date | undefined
|
|
352
|
+
console.log(run.createdAt); // Date
|
|
353
|
+
console.log(run.updatedAt); // Date
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### List Runs
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const result = await ironflow.listRuns({
|
|
360
|
+
functionId: 'process-order', // Filter by function
|
|
361
|
+
status: 'failed', // Filter by status
|
|
362
|
+
limit: 25, // Page size
|
|
363
|
+
cursor: 'next-page-token', // Pagination cursor
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
console.log(result.runs); // Run[]
|
|
367
|
+
console.log(result.totalCount); // Total matching runs
|
|
368
|
+
console.log(result.nextCursor); // Cursor for next page (undefined if last page)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Cancel, Retry, Resume, Patch
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// Cancel a running workflow
|
|
375
|
+
const run = await ironflow.cancelRun('run_abc123', 'No longer needed');
|
|
376
|
+
|
|
377
|
+
// Retry a failed run (optionally from a specific step)
|
|
378
|
+
const run = await ironflow.retryRun('run_abc123', 'step-that-failed');
|
|
379
|
+
|
|
380
|
+
// Resume a paused or failed run
|
|
381
|
+
const run = await ironflow.resumeRun('run_abc123', 'step-to-resume-from');
|
|
382
|
+
|
|
383
|
+
// Hot-patch a step's output (replaces the stored output and replays downstream)
|
|
384
|
+
await ironflow.patchStep('step_xyz789', { correctedValue: 42 }, 'Manual fix');
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Entity Streams (Event Sourcing)
|
|
388
|
+
|
|
389
|
+
Entity streams store domain events per entity with optimistic concurrency control.
|
|
390
|
+
|
|
391
|
+
### Append Events
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { ironflow } from '@ironflow/browser';
|
|
395
|
+
|
|
396
|
+
const result = await ironflow.streams.append('order-123', {
|
|
397
|
+
name: 'order.created', // Event name
|
|
398
|
+
data: { total: 99.99 }, // Event payload
|
|
399
|
+
entityType: 'order', // Entity type (required)
|
|
400
|
+
}, {
|
|
401
|
+
expectedVersion: 0, // Optimistic concurrency (-1 = any, 0 = must not exist)
|
|
402
|
+
idempotencyKey: 'create-order-123', // Deduplication
|
|
403
|
+
version: 1, // Event schema version (default: 1)
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
console.log(result.entityVersion); // New entity version after append
|
|
407
|
+
console.log(result.eventId); // Unique event ID
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Read Stream
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
const { events, totalCount } = await ironflow.streams.read('order-123', {
|
|
414
|
+
direction: 'forward', // 'forward' (default) | 'backward'
|
|
415
|
+
limit: 50, // Max events to return (0 = all)
|
|
416
|
+
fromVersion: 0, // Start from this version (0 = beginning)
|
|
107
417
|
});
|
|
108
418
|
|
|
109
|
-
|
|
419
|
+
for (const event of events) {
|
|
420
|
+
console.log(event.id); // Event ID
|
|
421
|
+
console.log(event.name); // 'order.created'
|
|
422
|
+
console.log(event.data); // { total: 99.99 }
|
|
423
|
+
console.log(event.entityVersion); // Version number
|
|
424
|
+
console.log(event.version); // Schema version
|
|
425
|
+
console.log(event.timestamp); // ISO 8601 timestamp
|
|
426
|
+
console.log(event.source); // Optional source identifier
|
|
427
|
+
console.log(event.metadata); // Optional metadata
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Get Stream Info
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
110
434
|
const info = await ironflow.streams.getInfo('order-123');
|
|
435
|
+
|
|
436
|
+
console.log(info.entityId); // 'order-123'
|
|
437
|
+
console.log(info.entityType); // 'order'
|
|
438
|
+
console.log(info.version); // Current version number
|
|
439
|
+
console.log(info.eventCount); // Total events in stream
|
|
440
|
+
console.log(info.createdAt); // ISO 8601 timestamp
|
|
441
|
+
console.log(info.updatedAt); // ISO 8601 timestamp
|
|
111
442
|
```
|
|
112
443
|
|
|
113
|
-
###
|
|
444
|
+
### Subscribe to Stream Updates
|
|
114
445
|
|
|
115
446
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
447
|
+
const sub = await ironflow.streams.subscribe('order-123', {
|
|
448
|
+
entityType: 'order', // Required
|
|
449
|
+
onEvent: (event) => {
|
|
450
|
+
console.log('Stream event:', event.name, event.data);
|
|
451
|
+
},
|
|
452
|
+
onError: (error) => {
|
|
453
|
+
console.error('Stream subscription error:', error);
|
|
454
|
+
},
|
|
455
|
+
replay: 100, // Replay last 100 events
|
|
119
456
|
});
|
|
120
457
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
458
|
+
// Cleanup
|
|
459
|
+
sub.unsubscribe();
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The subscription pattern is automatically constructed as `entity:{entityType}.{entityId}.>`.
|
|
463
|
+
|
|
464
|
+
## Projections
|
|
465
|
+
|
|
466
|
+
Projections build read models from event streams, maintained server-side.
|
|
467
|
+
|
|
468
|
+
### Get Projection State
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { ironflow } from '@ironflow/browser';
|
|
472
|
+
|
|
473
|
+
// Get global projection state
|
|
474
|
+
const result = await ironflow.getProjection<{ totalOrders: number }>('order-stats');
|
|
475
|
+
|
|
476
|
+
console.log(result.name); // 'order-stats'
|
|
477
|
+
console.log(result.state); // { totalOrders: 42 }
|
|
478
|
+
console.log(result.partition); // '__global__' or partition key
|
|
479
|
+
console.log(result.lastEventId); // Last processed event ID
|
|
480
|
+
console.log(result.lastEventTime); // Date
|
|
481
|
+
console.log(result.version); // Projection version
|
|
482
|
+
console.log(result.mode); // 'managed' | 'external'
|
|
483
|
+
|
|
484
|
+
// Get partitioned projection state
|
|
485
|
+
const result = await ironflow.getProjection('order-stats', {
|
|
486
|
+
partition: 'customer-123',
|
|
125
487
|
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Subscribe to Projection Updates
|
|
126
491
|
|
|
127
|
-
|
|
128
|
-
const
|
|
492
|
+
```typescript
|
|
493
|
+
const sub = await ironflow.subscribeToProjection<{ totalOrders: number }>(
|
|
494
|
+
'order-stats',
|
|
495
|
+
{
|
|
496
|
+
onUpdate: (state, event) => {
|
|
497
|
+
console.log('New state:', state); // { totalOrders: 43 }
|
|
498
|
+
console.log('Triggered by:', event.id, event.name);
|
|
499
|
+
},
|
|
500
|
+
onError: (error) => {
|
|
501
|
+
console.error('Projection error:', error);
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
partition: 'customer-123', // Optional: subscribe to specific partition
|
|
506
|
+
replay: 10, // Optional: replay last N updates
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
sub.unsubscribe();
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Without a partition, subscribes to `system.projection.{name}.>` (all partitions). With a partition, subscribes to `system.projection.{name}.{partition}.updated`.
|
|
514
|
+
|
|
515
|
+
### Projection Management
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// List all projections
|
|
129
519
|
const projections = await ironflow.listProjections();
|
|
130
|
-
|
|
520
|
+
for (const p of projections) {
|
|
521
|
+
console.log(p.name, p.status, p.mode, p.lag);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Get detailed status of a projection
|
|
525
|
+
const status = await ironflow.getProjectionStatus('order-stats');
|
|
526
|
+
console.log(status.name); // 'order-stats'
|
|
527
|
+
console.log(status.status); // 'active' | 'rebuilding' | 'paused' | 'error'
|
|
528
|
+
console.log(status.mode); // 'managed' | 'external'
|
|
529
|
+
console.log(status.lastEventSeq); // Last processed sequence number
|
|
530
|
+
console.log(status.lag); // Number of unprocessed events
|
|
531
|
+
console.log(status.errorMessage); // Error message if status is 'error'
|
|
532
|
+
console.log(status.updatedAt); // Date
|
|
533
|
+
|
|
534
|
+
// Trigger a rebuild
|
|
535
|
+
const result = await ironflow.rebuildProjection('order-stats', {
|
|
536
|
+
partition: 'customer-123', // Optional: rebuild specific partition
|
|
537
|
+
fromEventId: 'evt_abc', // Optional: rebuild from specific event
|
|
538
|
+
dryRun: true, // Optional: validate without rebuilding
|
|
539
|
+
});
|
|
540
|
+
console.log(result.status); // 'rebuilding' | 'dry_run_ok'
|
|
131
541
|
```
|
|
132
542
|
|
|
133
|
-
|
|
543
|
+
## KV Store
|
|
544
|
+
|
|
545
|
+
Distributed key-value storage backed by NATS JetStream with bucket management, TTL, compare-and-swap, and real-time watch.
|
|
546
|
+
|
|
547
|
+
### Getting a KV Client
|
|
134
548
|
|
|
135
549
|
```typescript
|
|
550
|
+
import { ironflow } from '@ironflow/browser';
|
|
551
|
+
|
|
136
552
|
const kv = ironflow.kv();
|
|
137
|
-
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Bucket Management
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Create a bucket
|
|
559
|
+
const bucketInfo = await kv.createBucket({
|
|
560
|
+
name: 'sessions',
|
|
561
|
+
description: 'User session store', // Optional
|
|
562
|
+
ttlSeconds: 3600, // Optional: auto-expire keys (0 = no expiry)
|
|
563
|
+
maxValueSize: 1024 * 1024, // Optional: max value size in bytes
|
|
564
|
+
maxBytes: 100 * 1024 * 1024, // Optional: max total bucket size in bytes
|
|
565
|
+
history: 5, // Optional: historical values per key (default: 1)
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// List all buckets
|
|
569
|
+
const buckets = await kv.listBuckets();
|
|
570
|
+
// Returns KVBucketInfo[]
|
|
138
571
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
572
|
+
// Get bucket info
|
|
573
|
+
const info = await kv.getBucketInfo('sessions');
|
|
574
|
+
|
|
575
|
+
// Delete a bucket
|
|
576
|
+
await kv.deleteBucket('sessions');
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Key Operations
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
const bucket = kv.bucket('sessions');
|
|
583
|
+
|
|
584
|
+
// Put a value (unconditional write)
|
|
585
|
+
const { revision } = await bucket.put('user-123', { token: 'abc', expiresAt: '...' });
|
|
586
|
+
|
|
587
|
+
// Get a value
|
|
588
|
+
const entry = await bucket.get('user-123');
|
|
589
|
+
console.log(entry.value); // The stored value
|
|
590
|
+
console.log(entry.revision); // Revision number for CAS
|
|
591
|
+
|
|
592
|
+
// Create only if key does not exist (if-not-exists)
|
|
593
|
+
const { revision } = await bucket.create('user-456', { token: 'def' });
|
|
594
|
+
|
|
595
|
+
// Update only if revision matches (compare-and-swap)
|
|
596
|
+
const { revision: newRev } = await bucket.update('user-123', { token: 'xyz' }, entry.revision);
|
|
597
|
+
|
|
598
|
+
// Soft delete (tombstone)
|
|
599
|
+
await bucket.delete('user-123');
|
|
600
|
+
|
|
601
|
+
// Hard delete (purge key and all history)
|
|
602
|
+
await bucket.purge('user-123');
|
|
603
|
+
|
|
604
|
+
// List keys with optional wildcard filter
|
|
605
|
+
const allKeys = await bucket.listKeys();
|
|
606
|
+
const userKeys = await bucket.listKeys('user-*');
|
|
142
607
|
```
|
|
143
608
|
|
|
144
|
-
###
|
|
609
|
+
### Watch for Changes
|
|
610
|
+
|
|
611
|
+
Real-time notifications via WebSocket when keys are updated or deleted:
|
|
145
612
|
|
|
146
613
|
```typescript
|
|
614
|
+
const watcher = bucket.watch(
|
|
615
|
+
{
|
|
616
|
+
onUpdate: (event) => {
|
|
617
|
+
// event: KVWatchEvent (type: 'kv_update')
|
|
618
|
+
console.log('Key changed:', event);
|
|
619
|
+
},
|
|
620
|
+
onError: (error) => {
|
|
621
|
+
console.error('Watch error:', error);
|
|
622
|
+
},
|
|
623
|
+
onClose: () => {
|
|
624
|
+
console.log('Watch connection closed');
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
key: 'user.*', // Optional: only watch keys matching pattern
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
// Stop watching
|
|
633
|
+
watcher.stop();
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Config Management
|
|
637
|
+
|
|
638
|
+
Centralized configuration management with set, get, patch, list, delete, and real-time watch.
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
import { ironflow } from '@ironflow/browser';
|
|
642
|
+
|
|
147
643
|
const config = ironflow.configManager();
|
|
148
644
|
|
|
149
|
-
|
|
645
|
+
// Set a config (full document replacement)
|
|
646
|
+
const result = await config.set('app-settings', {
|
|
647
|
+
theme: 'dark',
|
|
648
|
+
locale: 'en',
|
|
649
|
+
maxRetries: 3,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Get a config by name
|
|
150
653
|
const settings = await config.get('app-settings');
|
|
654
|
+
console.log(settings.data); // { theme: 'dark', locale: 'en', maxRetries: 3 }
|
|
655
|
+
console.log(settings.revision); // Revision number
|
|
656
|
+
|
|
657
|
+
// Patch a config (shallow merge)
|
|
151
658
|
await config.patch('app-settings', { locale: 'fr' });
|
|
659
|
+
|
|
660
|
+
// List all configs
|
|
661
|
+
const all = await config.list();
|
|
662
|
+
// Returns ConfigEntry[]
|
|
663
|
+
|
|
664
|
+
// Delete a config (idempotent)
|
|
152
665
|
await config.delete('app-settings');
|
|
666
|
+
|
|
667
|
+
// Watch for real-time config changes
|
|
668
|
+
// Subscribes to system.config.{name}.updated topic
|
|
669
|
+
const sub = await config.watch('app-settings', {
|
|
670
|
+
onEvent: (configResponse) => {
|
|
671
|
+
console.log('Config updated:', configResponse.data);
|
|
672
|
+
},
|
|
673
|
+
onError: (error) => {
|
|
674
|
+
console.error('Watch error:', error);
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
sub.unsubscribe();
|
|
153
679
|
```
|
|
154
680
|
|
|
155
|
-
|
|
681
|
+
## Auth Management
|
|
156
682
|
|
|
157
|
-
|
|
158
|
-
|--------|-------------|
|
|
159
|
-
| `ironflow.listFunctions()` | List registered functions |
|
|
160
|
-
| `ironflow.listWorkers()` | List connected workers |
|
|
161
|
-
| `ironflow.health()` | Health check |
|
|
162
|
-
| `ironflow.getCapabilities()` | Get server capabilities (transports, features, version) |
|
|
683
|
+
### API Keys
|
|
163
684
|
|
|
164
|
-
|
|
685
|
+
```typescript
|
|
686
|
+
import { ironflow } from '@ironflow/browser';
|
|
687
|
+
|
|
688
|
+
// Create an API key
|
|
689
|
+
const keyWithSecret = await ironflow.apiKeys.create({
|
|
690
|
+
name: 'my-service-key',
|
|
691
|
+
envId: 'env_default',
|
|
692
|
+
});
|
|
693
|
+
console.log(keyWithSecret.key); // Only returned once at creation time
|
|
694
|
+
|
|
695
|
+
// List all API keys
|
|
696
|
+
const keys = await ironflow.apiKeys.list();
|
|
697
|
+
|
|
698
|
+
// Get a specific API key
|
|
699
|
+
const key = await ironflow.apiKeys.get('apikey_abc123');
|
|
700
|
+
|
|
701
|
+
// Rotate an API key (returns new secret)
|
|
702
|
+
const rotated = await ironflow.apiKeys.rotate('apikey_abc123');
|
|
703
|
+
console.log(rotated.key); // New secret
|
|
704
|
+
|
|
705
|
+
// Delete an API key
|
|
706
|
+
await ironflow.apiKeys.delete('apikey_abc123');
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Organizations (Enterprise)
|
|
710
|
+
|
|
711
|
+
Requires an Enterprise license. Returns `EnterpriseRequiredError` (HTTP 402) without one.
|
|
165
712
|
|
|
166
713
|
```typescript
|
|
167
|
-
//
|
|
168
|
-
ironflow.
|
|
169
|
-
|
|
714
|
+
// Create an organization
|
|
715
|
+
const org = await ironflow.orgs.create({ name: 'Acme Corp' });
|
|
716
|
+
|
|
717
|
+
// List all organizations
|
|
718
|
+
const orgs = await ironflow.orgs.list();
|
|
719
|
+
|
|
720
|
+
// Get a specific organization
|
|
721
|
+
const org = await ironflow.orgs.get('org_abc123');
|
|
722
|
+
|
|
723
|
+
// Update an organization
|
|
724
|
+
const updated = await ironflow.orgs.update('org_abc123', { name: 'Acme Inc' });
|
|
725
|
+
|
|
726
|
+
// Delete an organization
|
|
727
|
+
await ironflow.orgs.delete('org_abc123');
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Roles (Enterprise)
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
// Create a role
|
|
734
|
+
const role = await ironflow.roles.create({
|
|
735
|
+
name: 'editor',
|
|
736
|
+
org_id: 'org_abc123',
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// List roles (optionally filtered by org)
|
|
740
|
+
const roles = await ironflow.roles.list('org_abc123');
|
|
741
|
+
|
|
742
|
+
// Get a specific role
|
|
743
|
+
const role = await ironflow.roles.get('role_xyz789');
|
|
744
|
+
|
|
745
|
+
// Update a role
|
|
746
|
+
const updated = await ironflow.roles.update('role_xyz789', { name: 'senior-editor' });
|
|
747
|
+
|
|
748
|
+
// Assign a policy to a role
|
|
749
|
+
await ironflow.roles.assignPolicy('role_xyz789', 'policy_abc');
|
|
750
|
+
|
|
751
|
+
// Remove a policy from a role
|
|
752
|
+
await ironflow.roles.removePolicy('role_xyz789', 'policy_abc');
|
|
753
|
+
|
|
754
|
+
// Delete a role
|
|
755
|
+
await ironflow.roles.delete('role_xyz789');
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Policies (Enterprise)
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
// Create a policy
|
|
762
|
+
const policy = await ironflow.policies.create({
|
|
763
|
+
name: 'allow-read',
|
|
764
|
+
effect: 'allow',
|
|
765
|
+
actions: 'read',
|
|
766
|
+
resources: '*',
|
|
767
|
+
org_id: 'org_abc123',
|
|
170
768
|
});
|
|
171
769
|
|
|
172
|
-
//
|
|
173
|
-
ironflow.
|
|
174
|
-
|
|
770
|
+
// List policies (optionally filtered by org)
|
|
771
|
+
const policies = await ironflow.policies.list('org_abc123');
|
|
772
|
+
|
|
773
|
+
// Get a specific policy
|
|
774
|
+
const policy = await ironflow.policies.get('policy_abc');
|
|
775
|
+
|
|
776
|
+
// Update a policy
|
|
777
|
+
const updated = await ironflow.policies.update('policy_abc', {
|
|
778
|
+
name: 'allow-read-write',
|
|
779
|
+
actions: 'read,write',
|
|
175
780
|
});
|
|
176
781
|
|
|
177
|
-
//
|
|
178
|
-
await ironflow.
|
|
179
|
-
ironflow.disconnect();
|
|
782
|
+
// Delete a policy
|
|
783
|
+
await ironflow.policies.delete('policy_abc');
|
|
180
784
|
```
|
|
181
785
|
|
|
786
|
+
## Server Inspection
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
import { ironflow } from '@ironflow/browser';
|
|
790
|
+
|
|
791
|
+
// List registered functions
|
|
792
|
+
const functions = await ironflow.listFunctions();
|
|
793
|
+
|
|
794
|
+
// List connected workers
|
|
795
|
+
const workers = await ironflow.listWorkers();
|
|
796
|
+
|
|
797
|
+
// Health check
|
|
798
|
+
const health = await ironflow.health();
|
|
799
|
+
console.log(health.status); // 'ok'
|
|
800
|
+
console.log(health.timestamp); // ISO 8601
|
|
801
|
+
console.log(health.version); // Server version
|
|
802
|
+
|
|
803
|
+
// Get server capabilities
|
|
804
|
+
const caps = await ironflow.getCapabilities();
|
|
805
|
+
console.log(caps.transports); // ['connectrpc', 'websocket']
|
|
806
|
+
console.log(caps.features); // ['kv', 'projections', 'entity-streams', ...]
|
|
807
|
+
console.log(caps.version); // Server version
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
## React Integration Patterns
|
|
811
|
+
|
|
812
|
+
### Subscription with useEffect Cleanup
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
import { useEffect, useRef, useState } from 'react';
|
|
816
|
+
import { ironflow, type Subscription, type SubscriptionEvent } from '@ironflow/browser';
|
|
817
|
+
|
|
818
|
+
function OrderFeed() {
|
|
819
|
+
const [orders, setOrders] = useState<SubscriptionEvent[]>([]);
|
|
820
|
+
const subRef = useRef<Subscription | null>(null);
|
|
821
|
+
|
|
822
|
+
useEffect(() => {
|
|
823
|
+
let cancelled = false;
|
|
824
|
+
|
|
825
|
+
ironflow.subscribe('events:order.*', {
|
|
826
|
+
onEvent: (event) => {
|
|
827
|
+
if (!cancelled) {
|
|
828
|
+
setOrders((prev) => [...prev, event]);
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
replay: 50,
|
|
832
|
+
}).then((sub) => {
|
|
833
|
+
if (cancelled) {
|
|
834
|
+
sub.unsubscribe();
|
|
835
|
+
} else {
|
|
836
|
+
subRef.current = sub;
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
return () => {
|
|
841
|
+
cancelled = true;
|
|
842
|
+
subRef.current?.unsubscribe();
|
|
843
|
+
subRef.current = null;
|
|
844
|
+
};
|
|
845
|
+
}, []);
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<ul>
|
|
849
|
+
{orders.map((o, i) => (
|
|
850
|
+
<li key={i}>{o.name}: {JSON.stringify(o.data)}</li>
|
|
851
|
+
))}
|
|
852
|
+
</ul>
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### Custom useIronflowSubscription Hook
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
861
|
+
import {
|
|
862
|
+
ironflow,
|
|
863
|
+
type Subscription,
|
|
864
|
+
type SubscriptionEvent,
|
|
865
|
+
type SubscriptionCallbacks,
|
|
866
|
+
type BrowserSubscribeOptions,
|
|
867
|
+
} from '@ironflow/browser';
|
|
868
|
+
|
|
869
|
+
function useIronflowSubscription<T = unknown>(
|
|
870
|
+
pattern: string | null,
|
|
871
|
+
options?: BrowserSubscribeOptions
|
|
872
|
+
) {
|
|
873
|
+
const [events, setEvents] = useState<SubscriptionEvent<T>[]>([]);
|
|
874
|
+
const [error, setError] = useState<Error | null>(null);
|
|
875
|
+
const [connected, setConnected] = useState(false);
|
|
876
|
+
const subRef = useRef<Subscription | null>(null);
|
|
877
|
+
|
|
878
|
+
useEffect(() => {
|
|
879
|
+
if (!pattern) return;
|
|
880
|
+
|
|
881
|
+
let cancelled = false;
|
|
882
|
+
|
|
883
|
+
ironflow.subscribe<T>(pattern, {
|
|
884
|
+
onEvent: (event) => {
|
|
885
|
+
if (!cancelled) {
|
|
886
|
+
setEvents((prev) => [...prev, event]);
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
onError: (err) => {
|
|
890
|
+
if (!cancelled) {
|
|
891
|
+
setError(new Error(err.message));
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
onStateChange: (state) => {
|
|
895
|
+
if (!cancelled) {
|
|
896
|
+
setConnected(state === 'connected');
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
...options,
|
|
900
|
+
}).then((sub) => {
|
|
901
|
+
if (cancelled) {
|
|
902
|
+
sub.unsubscribe();
|
|
903
|
+
} else {
|
|
904
|
+
subRef.current = sub;
|
|
905
|
+
setConnected(true);
|
|
906
|
+
}
|
|
907
|
+
}).catch((err) => {
|
|
908
|
+
if (!cancelled) {
|
|
909
|
+
setError(err);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
return () => {
|
|
914
|
+
cancelled = true;
|
|
915
|
+
subRef.current?.unsubscribe();
|
|
916
|
+
subRef.current = null;
|
|
917
|
+
};
|
|
918
|
+
}, [pattern]);
|
|
919
|
+
|
|
920
|
+
const clear = useCallback(() => setEvents([]), []);
|
|
921
|
+
|
|
922
|
+
return { events, error, connected, clear };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Usage
|
|
926
|
+
function Dashboard() {
|
|
927
|
+
const { events, error, connected } = useIronflowSubscription('system.run.>', {
|
|
928
|
+
replay: 20,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
932
|
+
|
|
933
|
+
return (
|
|
934
|
+
<div>
|
|
935
|
+
<span>{connected ? 'Connected' : 'Disconnected'}</span>
|
|
936
|
+
{events.map((e, i) => (
|
|
937
|
+
<div key={i}>{e.name}</div>
|
|
938
|
+
))}
|
|
939
|
+
</div>
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### Connection State Display
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
import { useEffect, useState } from 'react';
|
|
948
|
+
import { ironflow, type ConnectionState } from '@ironflow/browser';
|
|
949
|
+
|
|
950
|
+
function ConnectionStatus() {
|
|
951
|
+
const [state, setState] = useState<ConnectionState>(ironflow.connectionState);
|
|
952
|
+
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
const unsubscribe = ironflow.onConnectionChange(setState);
|
|
955
|
+
return unsubscribe;
|
|
956
|
+
}, []);
|
|
957
|
+
|
|
958
|
+
const colors: Record<ConnectionState, string> = {
|
|
959
|
+
connected: 'green',
|
|
960
|
+
disconnected: 'red',
|
|
961
|
+
connecting: 'yellow',
|
|
962
|
+
reconnecting: 'orange',
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
return (
|
|
966
|
+
<span style={{ color: colors[state] }}>
|
|
967
|
+
{state}
|
|
968
|
+
</span>
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### App-Level Configuration
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
// app/layout.tsx or main.tsx - configure once at app startup
|
|
977
|
+
import { ironflow } from '@ironflow/browser';
|
|
978
|
+
|
|
979
|
+
ironflow.configure({
|
|
980
|
+
serverUrl: process.env.NEXT_PUBLIC_IRONFLOW_URL ?? 'http://localhost:9123',
|
|
981
|
+
auth: {
|
|
982
|
+
apiKey: process.env.NEXT_PUBLIC_IRONFLOW_API_KEY,
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
## Transport Configuration
|
|
988
|
+
|
|
989
|
+
The browser client supports two transport protocols for real-time subscriptions:
|
|
990
|
+
|
|
991
|
+
### ConnectRPC (Default)
|
|
992
|
+
|
|
993
|
+
Uses HTTP/2 with Protocol Buffers. Preferred for production because it shares the same connection as REST API calls and supports bidirectional streaming.
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
ironflow.configure({
|
|
997
|
+
serverUrl: 'http://localhost:9123',
|
|
998
|
+
transport: 'connectrpc',
|
|
999
|
+
});
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### WebSocket
|
|
1003
|
+
|
|
1004
|
+
Uses a dedicated WebSocket connection. Useful as a fallback or when ConnectRPC is not available.
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
ironflow.configure({
|
|
1008
|
+
serverUrl: 'http://localhost:9123',
|
|
1009
|
+
transport: 'websocket',
|
|
1010
|
+
});
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
The WebSocket URL is derived from `serverUrl` by replacing `http://` with `ws://` and `https://` with `wss://`.
|
|
1014
|
+
|
|
1015
|
+
### Advanced: Custom Transport
|
|
1016
|
+
|
|
1017
|
+
For advanced use cases, transport factories and types are exported:
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import {
|
|
1021
|
+
createWebSocketTransport,
|
|
1022
|
+
createConnectRPCTransport,
|
|
1023
|
+
type Transport,
|
|
1024
|
+
type TransportOptions,
|
|
1025
|
+
type TransportCallbacks,
|
|
1026
|
+
type TransportFactory,
|
|
1027
|
+
} from '@ironflow/browser';
|
|
1028
|
+
|
|
1029
|
+
// Create a transport manually
|
|
1030
|
+
const options: TransportOptions = {
|
|
1031
|
+
auth: { apiKey: 'my-key' },
|
|
1032
|
+
autoReconnect: true,
|
|
1033
|
+
reconnectDelay: 1000,
|
|
1034
|
+
maxReconnectDelay: 30000,
|
|
1035
|
+
reconnectBackoff: 2,
|
|
1036
|
+
environment: 'default',
|
|
1037
|
+
connectionTimeout: 10000,
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const transport = createConnectRPCTransport('http://localhost:9123', options);
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
## Error Handling
|
|
1044
|
+
|
|
1045
|
+
### Error Types
|
|
1046
|
+
|
|
1047
|
+
All error types are re-exported from `@ironflow/core`:
|
|
1048
|
+
|
|
1049
|
+
```typescript
|
|
1050
|
+
import {
|
|
1051
|
+
IronflowError, // Base error class for all Ironflow errors
|
|
1052
|
+
ConnectionError, // Connection failures
|
|
1053
|
+
SubscriptionError, // Subscription failures
|
|
1054
|
+
TimeoutError, // Request timeouts
|
|
1055
|
+
ValidationError, // Invalid response or input validation
|
|
1056
|
+
NotConfiguredError, // Client used before configure() was called
|
|
1057
|
+
} from '@ironflow/browser';
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
Additionally, the REST request helper maps HTTP status codes to specific error types:
|
|
1061
|
+
|
|
1062
|
+
- **401** -> `UnauthenticatedError` -- missing or invalid credentials
|
|
1063
|
+
- **402** -> `EnterpriseRequiredError` -- enterprise license required
|
|
1064
|
+
- **403** -> `UnauthorizedError` -- insufficient permissions
|
|
1065
|
+
|
|
1066
|
+
### Error Utilities
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
import { isRetryable, isIronflowError } from '@ironflow/browser';
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
await ironflow.trigger('process-order', { data: { orderId: '123' } });
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (isIronflowError(error)) {
|
|
1075
|
+
console.log(error.message); // Human-readable message
|
|
1076
|
+
console.log(error.code); // Machine-readable code (e.g., 'HTTP_500', 'TIMEOUT')
|
|
1077
|
+
|
|
1078
|
+
if (isRetryable(error)) {
|
|
1079
|
+
// Safe to retry (5xx errors, timeouts, connection failures)
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
### Error Codes
|
|
1086
|
+
|
|
1087
|
+
Common error codes returned by the client:
|
|
1088
|
+
|
|
1089
|
+
| Code | Description |
|
|
1090
|
+
|------|-------------|
|
|
1091
|
+
| `HTTP_4xx` / `HTTP_5xx` | HTTP status-based errors |
|
|
1092
|
+
| `TIMEOUT` | Request exceeded the configured timeout |
|
|
1093
|
+
| `REQUEST_FAILED` | Network or fetch failure |
|
|
1094
|
+
| `PATCH_FAILED` | Step patch operation failed |
|
|
1095
|
+
| `RESUME_FAILED` | Run resume operation failed |
|
|
1096
|
+
| `NOT_CONFIGURED` | Client used before `configure()` |
|
|
1097
|
+
|
|
182
1098
|
## Browser Compatibility
|
|
183
1099
|
|
|
184
1100
|
- Chrome 80+
|
|
@@ -186,9 +1102,40 @@ ironflow.disconnect();
|
|
|
186
1102
|
- Safari 13.1+
|
|
187
1103
|
- Edge 80+
|
|
188
1104
|
|
|
189
|
-
|
|
1105
|
+
Requires native `fetch`, `WebSocket`, and `AbortController` support.
|
|
1106
|
+
|
|
1107
|
+
## Exported Types
|
|
1108
|
+
|
|
1109
|
+
The package re-exports the following types from `@ironflow/core` for convenience:
|
|
1110
|
+
|
|
1111
|
+
**Run types:** `Run`, `RunStatus`, `RunInfo`, `ListRunsOptions`, `ListRunsResult`
|
|
1112
|
+
|
|
1113
|
+
**Event types:** `IronflowEvent`, `EmitOptions`, `EmitResult`
|
|
1114
|
+
|
|
1115
|
+
**Trigger types:** `TriggerResult`, `TriggerSyncOptions`, `TriggerSyncResult`
|
|
1116
|
+
|
|
1117
|
+
**Subscription types:** `SubscribeOptions`, `Subscription`, `AckableSubscription`, `SubscriptionEvent`, `SubscriptionErrorInfo`, `SubscriptionCallbacks`, `ConnectionState`, `AckHandle`
|
|
1118
|
+
|
|
1119
|
+
**Consumer group types:** `ConsumerGroup`, `ConsumerGroupConfig`, `ConsumerGroupStatus`, `AckMode`, `BackpressureMode`
|
|
1120
|
+
|
|
1121
|
+
**Entity stream types:** `AppendEventInput`, `AppendOptions`, `AppendResult`, `ReadStreamOptions`, `StreamEvent`, `StreamInfo`, `EntitySubscribeOptions`
|
|
1122
|
+
|
|
1123
|
+
**Projection types:** `ProjectionStatusInfo`, `ProjectionStateResult`
|
|
1124
|
+
|
|
1125
|
+
**KV types:** `KVBucketConfig`, `KVBucketInfo`, `KVEntry`, `KVPutResult`, `KVListKeysResult`, `KVListBucketsResult`, `KVWatchEvent`, `KVWatchCallbacks`, `KVWatchOptions`, `KVWatcher`
|
|
1126
|
+
|
|
1127
|
+
**Config types:** `ConfigResponse`, `ConfigEntry`, `ConfigSetResult`, `ConfigWatchCallbacks`
|
|
1128
|
+
|
|
1129
|
+
**Browser-specific types:** `IronflowConfig`, `IronflowConfigOptions`, `ReconnectConfig`, `VisibilityConfig`, `AuthConfig`, `BrowserSubscribeOptions`, `SubscriptionGroup`, `Transport`, `TransportCallbacks`, `TransportFactory`, `TransportOptions`
|
|
1130
|
+
|
|
1131
|
+
**Utilities:** `patterns`, `DEFAULT_SERVER_URL`, `DEFAULT_WS_URL`, `DEFAULT_TIMEOUTS`, `getServerUrl`, `getWebSocketUrl`
|
|
1132
|
+
|
|
1133
|
+
**Classes:** `BrowserKVClient`, `BrowserKVBucketHandle`, `BrowserConfigClient`
|
|
1134
|
+
|
|
1135
|
+
## Links
|
|
190
1136
|
|
|
191
|
-
|
|
1137
|
+
- [Documentation](https://github.com/sahina/ironflow/tree/main/docs)
|
|
1138
|
+
- [GitHub Repository](https://github.com/sahina/ironflow)
|
|
192
1139
|
|
|
193
1140
|
## License
|
|
194
1141
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ironflow/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Browser client for Ironflow event-driven backend platform - real-time subscriptions, workflow triggers, and event emission",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|