@ironflow/browser 0.7.1 → 0.9.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 CHANGED
@@ -1,6 +1,27 @@
1
1
  # @ironflow/browser
2
2
 
3
- 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, and config management for web applications.
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,1097 @@ Browser client for [Ironflow](https://github.com/sahina/ironflow), an event-driv
8
29
  npm install @ironflow/browser
9
30
  ```
10
31
 
11
- ## Quick Start
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
- // Configure once at app startup
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
20
84
 
21
- // Subscribe to events
22
- const sub = ironflow.subscribe('events:order.*', {
23
- onEvent: (event) => console.log('Order:', event),
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
91
+
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
- // Trigger a workflow
27
- const run = await ironflow.trigger('process-order', {
28
- data: { orderId: '123' },
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
125
+
126
+ ### Basic Subscription
30
127
 
31
- // Emit events
32
- await ironflow.emit('order.approved', { orderId: '123' }, { version: 1 });
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
- ## Features
147
+ ### Subscription Options
39
148
 
40
- - **Real-time subscriptions** with WebSocket or ConnectRPC transport (auto-detected)
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
- ## Transport Configuration
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,
157
+
158
+ // Include event metadata (timestamp, sequence)
159
+ includeMetadata: true,
52
160
 
53
- The client auto-detects the best available transport, or you can choose explicitly:
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.configure({
57
- serverUrl: 'http://localhost:9123',
58
- transport: 'connectrpc', // or 'websocket'
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
- // Or auto-detect
62
- const transport = await ironflow.detectTransport();
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
- ## Key APIs
257
+ ### Consumer Groups
66
258
 
67
- ### Events & Subscriptions
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
- | Method | Description |
70
- |--------|-------------|
71
- | `ironflow.subscribe(pattern, callbacks)` | Subscribe to event patterns |
72
- | `ironflow.emit(eventName, data, options?)` | Emit an event |
73
- | `ironflow.joinConsumerGroup(group, pattern, callbacks)` | Join load-balanced consumer group |
74
- | `ironflow.subscriptionGroup()` | Create batch subscription manager |
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
- ### Workflow Operations
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
- | Method | Description |
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
- ### Entity Streams (Event Sourcing)
280
+ Alternatively, use `subscribe` directly with `consumerGroup` and `ackMode` options:
89
281
 
90
282
  ```typescript
91
- // Append an event to an entity stream
92
- await ironflow.streams.append('order-123', {
93
- eventName: 'item.added',
94
- data: { sku: 'ABC', qty: 2 },
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
- // Read events from a stream
99
- const events = await ironflow.streams.read('order-123', {
100
- direction: 'forward',
101
- limit: 50,
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',
102
303
  });
103
304
 
104
- // Subscribe to real-time stream updates
105
- const sub = ironflow.streams.subscribe('order-123', {
106
- onEvent: (event) => console.log('Stream event:', event),
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' },
331
+ });
332
+
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
+ ### Scoped Injection
388
+
389
+ ```typescript
390
+ // Pause a running workflow at the next step boundary
391
+ await ironflow.pauseRun("run_abc123");
392
+
393
+ // Get the paused state with completed steps
394
+ const state = await ironflow.getPausedState("run_abc123");
395
+ for (const step of state.steps) {
396
+ console.log(step.name, step.output, step.injected);
397
+ }
398
+
399
+ // Inject modified output
400
+ const result = await ironflow.injectStepOutput(
401
+ "run_abc123",
402
+ "step_xyz",
403
+ { corrected: true },
404
+ "Fix calculation error"
405
+ );
406
+ console.log("Previous output:", result.previousOutput);
407
+
408
+ // Resume with injected data
409
+ await ironflow.resumeRun("run_abc123");
410
+ ```
411
+
412
+ ## Entity Streams (Event Sourcing)
413
+
414
+ Entity streams store domain events per entity with optimistic concurrency control.
415
+
416
+ ### Append Events
417
+
418
+ ```typescript
419
+ import { ironflow } from '@ironflow/browser';
420
+
421
+ const result = await ironflow.streams.append('order-123', {
422
+ name: 'order.created', // Event name
423
+ data: { total: 99.99 }, // Event payload
424
+ entityType: 'order', // Entity type (required)
425
+ }, {
426
+ expectedVersion: 0, // Optimistic concurrency (-1 = any, 0 = must not exist)
427
+ idempotencyKey: 'create-order-123', // Deduplication
428
+ version: 1, // Event schema version (default: 1)
107
429
  });
108
430
 
109
- // Get stream metadata
431
+ console.log(result.entityVersion); // New entity version after append
432
+ console.log(result.eventId); // Unique event ID
433
+ ```
434
+
435
+ ### Read Stream
436
+
437
+ ```typescript
438
+ const { events, totalCount } = await ironflow.streams.read('order-123', {
439
+ direction: 'forward', // 'forward' (default) | 'backward'
440
+ limit: 50, // Max events to return (0 = all)
441
+ fromVersion: 0, // Start from this version (0 = beginning)
442
+ });
443
+
444
+ for (const event of events) {
445
+ console.log(event.id); // Event ID
446
+ console.log(event.name); // 'order.created'
447
+ console.log(event.data); // { total: 99.99 }
448
+ console.log(event.entityVersion); // Version number
449
+ console.log(event.version); // Schema version
450
+ console.log(event.timestamp); // ISO 8601 timestamp
451
+ console.log(event.source); // Optional source identifier
452
+ console.log(event.metadata); // Optional metadata
453
+ }
454
+ ```
455
+
456
+ ### Get Stream Info
457
+
458
+ ```typescript
110
459
  const info = await ironflow.streams.getInfo('order-123');
460
+
461
+ console.log(info.entityId); // 'order-123'
462
+ console.log(info.entityType); // 'order'
463
+ console.log(info.version); // Current version number
464
+ console.log(info.eventCount); // Total events in stream
465
+ console.log(info.createdAt); // ISO 8601 timestamp
466
+ console.log(info.updatedAt); // ISO 8601 timestamp
111
467
  ```
112
468
 
113
- ### Projections
469
+ ### Subscribe to Stream Updates
114
470
 
115
471
  ```typescript
116
- // Get projection state
117
- const state = await ironflow.getProjection('order-totals', {
118
- partition: 'region-us',
472
+ const sub = await ironflow.streams.subscribe('order-123', {
473
+ entityType: 'order', // Required
474
+ onEvent: (event) => {
475
+ console.log('Stream event:', event.name, event.data);
476
+ },
477
+ onError: (error) => {
478
+ console.error('Stream subscription error:', error);
479
+ },
480
+ replay: 100, // Replay last 100 events
119
481
  });
120
482
 
121
- // Subscribe to real-time projection updates
122
- const sub = ironflow.subscribeToProjection('order-totals', {
123
- onUpdate: (state, event) => console.log('Updated:', state),
124
- onError: (err) => console.error(err),
483
+ // Cleanup
484
+ sub.unsubscribe();
485
+ ```
486
+
487
+ The subscription pattern is automatically constructed as `entity:{entityType}.{entityId}.>`.
488
+
489
+ ## Projections
490
+
491
+ Projections build read models from event streams, maintained server-side.
492
+
493
+ ### Get Projection State
494
+
495
+ ```typescript
496
+ import { ironflow } from '@ironflow/browser';
497
+
498
+ // Get global projection state
499
+ const result = await ironflow.getProjection<{ totalOrders: number }>('order-stats');
500
+
501
+ console.log(result.name); // 'order-stats'
502
+ console.log(result.state); // { totalOrders: 42 }
503
+ console.log(result.partition); // '__global__' or partition key
504
+ console.log(result.lastEventId); // Last processed event ID
505
+ console.log(result.lastEventTime); // Date
506
+ console.log(result.version); // Projection version
507
+ console.log(result.mode); // 'managed' | 'external'
508
+
509
+ // Get partitioned projection state
510
+ const result = await ironflow.getProjection('order-stats', {
511
+ partition: 'customer-123',
125
512
  });
513
+ ```
514
+
515
+ ### Subscribe to Projection Updates
516
+
517
+ ```typescript
518
+ const sub = await ironflow.subscribeToProjection<{ totalOrders: number }>(
519
+ 'order-stats',
520
+ {
521
+ onUpdate: (state, event) => {
522
+ console.log('New state:', state); // { totalOrders: 43 }
523
+ console.log('Triggered by:', event.id, event.name);
524
+ },
525
+ onError: (error) => {
526
+ console.error('Projection error:', error);
527
+ },
528
+ },
529
+ {
530
+ partition: 'customer-123', // Optional: subscribe to specific partition
531
+ replay: 10, // Optional: replay last N updates
532
+ }
533
+ );
534
+
535
+ sub.unsubscribe();
536
+ ```
537
+
538
+ Without a partition, subscribes to `system.projection.{name}.>` (all partitions). With a partition, subscribes to `system.projection.{name}.{partition}.updated`.
539
+
540
+ ### Projection Management
126
541
 
127
- // Management
128
- const status = await ironflow.getProjectionStatus('order-totals');
542
+ ```typescript
543
+ // List all projections
129
544
  const projections = await ironflow.listProjections();
130
- await ironflow.rebuildProjection('order-totals', { dryRun: true });
545
+ for (const p of projections) {
546
+ console.log(p.name, p.status, p.mode, p.lag);
547
+ }
548
+
549
+ // Get detailed status of a projection
550
+ const status = await ironflow.getProjectionStatus('order-stats');
551
+ console.log(status.name); // 'order-stats'
552
+ console.log(status.status); // 'active' | 'rebuilding' | 'paused' | 'error'
553
+ console.log(status.mode); // 'managed' | 'external'
554
+ console.log(status.lastEventSeq); // Last processed sequence number
555
+ console.log(status.lag); // Number of unprocessed events
556
+ console.log(status.errorMessage); // Error message if status is 'error'
557
+ console.log(status.updatedAt); // Date
558
+
559
+ // Trigger a rebuild
560
+ const result = await ironflow.rebuildProjection('order-stats', {
561
+ partition: 'customer-123', // Optional: rebuild specific partition
562
+ fromEventId: 'evt_abc', // Optional: rebuild from specific event
563
+ dryRun: true, // Optional: validate without rebuilding
564
+ });
565
+ console.log(result.status); // 'rebuilding' | 'dry_run_ok'
131
566
  ```
132
567
 
133
- ### KV Store
568
+ ## KV Store
569
+
570
+ Distributed key-value storage backed by NATS JetStream with bucket management, TTL, compare-and-swap, and real-time watch.
571
+
572
+ ### Getting a KV Client
134
573
 
135
574
  ```typescript
575
+ import { ironflow } from '@ironflow/browser';
576
+
136
577
  const kv = ironflow.kv();
137
- const bucket = kv.bucket('my-bucket');
578
+ ```
138
579
 
139
- await bucket.put('key', { value: 'data' });
140
- const entry = await bucket.get('key');
141
- await bucket.delete('key');
580
+ ### Bucket Management
581
+
582
+ ```typescript
583
+ // Create a bucket
584
+ const bucketInfo = await kv.createBucket({
585
+ name: 'sessions',
586
+ description: 'User session store', // Optional
587
+ ttlSeconds: 3600, // Optional: auto-expire keys (0 = no expiry)
588
+ maxValueSize: 1024 * 1024, // Optional: max value size in bytes
589
+ maxBytes: 100 * 1024 * 1024, // Optional: max total bucket size in bytes
590
+ history: 5, // Optional: historical values per key (default: 1)
591
+ });
592
+
593
+ // List all buckets
594
+ const buckets = await kv.listBuckets();
595
+ // Returns KVBucketInfo[]
596
+
597
+ // Get bucket info
598
+ const info = await kv.getBucketInfo('sessions');
599
+
600
+ // Delete a bucket
601
+ await kv.deleteBucket('sessions');
602
+ ```
603
+
604
+ ### Key Operations
605
+
606
+ ```typescript
607
+ const bucket = kv.bucket('sessions');
608
+
609
+ // Put a value (unconditional write)
610
+ const { revision } = await bucket.put('user-123', { token: 'abc', expiresAt: '...' });
611
+
612
+ // Get a value
613
+ const entry = await bucket.get('user-123');
614
+ console.log(entry.value); // The stored value
615
+ console.log(entry.revision); // Revision number for CAS
616
+
617
+ // Create only if key does not exist (if-not-exists)
618
+ const { revision } = await bucket.create('user-456', { token: 'def' });
619
+
620
+ // Update only if revision matches (compare-and-swap)
621
+ const { revision: newRev } = await bucket.update('user-123', { token: 'xyz' }, entry.revision);
622
+
623
+ // Soft delete (tombstone)
624
+ await bucket.delete('user-123');
625
+
626
+ // Hard delete (purge key and all history)
627
+ await bucket.purge('user-123');
628
+
629
+ // List keys with optional wildcard filter
630
+ const allKeys = await bucket.listKeys();
631
+ const userKeys = await bucket.listKeys('user-*');
142
632
  ```
143
633
 
144
- ### Config Management
634
+ ### Watch for Changes
635
+
636
+ Real-time notifications via WebSocket when keys are updated or deleted:
637
+
638
+ ```typescript
639
+ const watcher = bucket.watch(
640
+ {
641
+ onUpdate: (event) => {
642
+ // event: KVWatchEvent (type: 'kv_update')
643
+ console.log('Key changed:', event);
644
+ },
645
+ onError: (error) => {
646
+ console.error('Watch error:', error);
647
+ },
648
+ onClose: () => {
649
+ console.log('Watch connection closed');
650
+ },
651
+ },
652
+ {
653
+ key: 'user.*', // Optional: only watch keys matching pattern
654
+ }
655
+ );
656
+
657
+ // Stop watching
658
+ watcher.stop();
659
+ ```
660
+
661
+ ## Config Management
662
+
663
+ Centralized configuration management with set, get, patch, list, delete, and real-time watch.
145
664
 
146
665
  ```typescript
666
+ import { ironflow } from '@ironflow/browser';
667
+
147
668
  const config = ironflow.configManager();
148
669
 
149
- await config.set('app-settings', { theme: 'dark', locale: 'en' });
670
+ // Set a config (full document replacement)
671
+ const result = await config.set('app-settings', {
672
+ theme: 'dark',
673
+ locale: 'en',
674
+ maxRetries: 3,
675
+ });
676
+
677
+ // Get a config by name
150
678
  const settings = await config.get('app-settings');
679
+ console.log(settings.data); // { theme: 'dark', locale: 'en', maxRetries: 3 }
680
+ console.log(settings.revision); // Revision number
681
+
682
+ // Patch a config (shallow merge)
151
683
  await config.patch('app-settings', { locale: 'fr' });
684
+
685
+ // List all configs
686
+ const all = await config.list();
687
+ // Returns ConfigEntry[]
688
+
689
+ // Delete a config (idempotent)
152
690
  await config.delete('app-settings');
691
+
692
+ // Watch for real-time config changes
693
+ // Subscribes to system.config.{name}.updated topic
694
+ const sub = await config.watch('app-settings', {
695
+ onEvent: (configResponse) => {
696
+ console.log('Config updated:', configResponse.data);
697
+ },
698
+ onError: (error) => {
699
+ console.error('Watch error:', error);
700
+ },
701
+ });
702
+
703
+ sub.unsubscribe();
153
704
  ```
154
705
 
155
- ### Server Inspection
706
+ ## Auth Management
156
707
 
157
- | Method | Description |
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) |
708
+ ### API Keys
163
709
 
164
- ## Connection Management
710
+ ```typescript
711
+ import { ironflow } from '@ironflow/browser';
712
+
713
+ // Create an API key
714
+ const keyWithSecret = await ironflow.apiKeys.create({
715
+ name: 'my-service-key',
716
+ envId: 'env_default',
717
+ });
718
+ console.log(keyWithSecret.key); // Only returned once at creation time
719
+
720
+ // List all API keys
721
+ const keys = await ironflow.apiKeys.list();
722
+
723
+ // Get a specific API key
724
+ const key = await ironflow.apiKeys.get('apikey_abc123');
725
+
726
+ // Rotate an API key (returns new secret)
727
+ const rotated = await ironflow.apiKeys.rotate('apikey_abc123');
728
+ console.log(rotated.key); // New secret
729
+
730
+ // Delete an API key
731
+ await ironflow.apiKeys.delete('apikey_abc123');
732
+ ```
733
+
734
+ ### Organizations (Enterprise)
735
+
736
+ Requires an Enterprise license. Returns `EnterpriseRequiredError` (HTTP 402) without one.
165
737
 
166
738
  ```typescript
167
- // Monitor connection state
168
- ironflow.onConnectionChange((state) => {
169
- console.log('Connection:', state); // 'connected' | 'disconnected' | 'reconnecting'
739
+ // Create an organization
740
+ const org = await ironflow.orgs.create({ name: 'Acme Corp' });
741
+
742
+ // List all organizations
743
+ const orgs = await ironflow.orgs.list();
744
+
745
+ // Get a specific organization
746
+ const org = await ironflow.orgs.get('org_abc123');
747
+
748
+ // Update an organization
749
+ const updated = await ironflow.orgs.update('org_abc123', { name: 'Acme Inc' });
750
+
751
+ // Delete an organization
752
+ await ironflow.orgs.delete('org_abc123');
753
+ ```
754
+
755
+ ### Roles (Enterprise)
756
+
757
+ ```typescript
758
+ // Create a role
759
+ const role = await ironflow.roles.create({
760
+ name: 'editor',
761
+ org_id: 'org_abc123',
170
762
  });
171
763
 
172
- // Global error handler
173
- ironflow.onError((error) => {
174
- console.error('Client error:', error);
764
+ // List roles (optionally filtered by org)
765
+ const roles = await ironflow.roles.list('org_abc123');
766
+
767
+ // Get a specific role
768
+ const role = await ironflow.roles.get('role_xyz789');
769
+
770
+ // Update a role
771
+ const updated = await ironflow.roles.update('role_xyz789', { name: 'senior-editor' });
772
+
773
+ // Assign a policy to a role
774
+ await ironflow.roles.assignPolicy('role_xyz789', 'policy_abc');
775
+
776
+ // Remove a policy from a role
777
+ await ironflow.roles.removePolicy('role_xyz789', 'policy_abc');
778
+
779
+ // Delete a role
780
+ await ironflow.roles.delete('role_xyz789');
781
+ ```
782
+
783
+ ### Policies (Enterprise)
784
+
785
+ ```typescript
786
+ // Create a policy
787
+ const policy = await ironflow.policies.create({
788
+ name: 'allow-read',
789
+ effect: 'allow',
790
+ actions: 'read',
791
+ resources: '*',
792
+ org_id: 'org_abc123',
175
793
  });
176
794
 
177
- // Manual connect/disconnect
178
- await ironflow.connect();
179
- ironflow.disconnect();
795
+ // List policies (optionally filtered by org)
796
+ const policies = await ironflow.policies.list('org_abc123');
797
+
798
+ // Get a specific policy
799
+ const policy = await ironflow.policies.get('policy_abc');
800
+
801
+ // Update a policy
802
+ const updated = await ironflow.policies.update('policy_abc', {
803
+ name: 'allow-read-write',
804
+ actions: 'read,write',
805
+ });
806
+
807
+ // Delete a policy
808
+ await ironflow.policies.delete('policy_abc');
809
+ ```
810
+
811
+ ## Server Inspection
812
+
813
+ ```typescript
814
+ import { ironflow } from '@ironflow/browser';
815
+
816
+ // List registered functions
817
+ const functions = await ironflow.listFunctions();
818
+
819
+ // List connected workers
820
+ const workers = await ironflow.listWorkers();
821
+
822
+ // Health check
823
+ const health = await ironflow.health();
824
+ console.log(health.status); // 'ok'
825
+ console.log(health.timestamp); // ISO 8601
826
+ console.log(health.version); // Server version
827
+
828
+ // Get server capabilities
829
+ const caps = await ironflow.getCapabilities();
830
+ console.log(caps.transports); // ['connectrpc', 'websocket']
831
+ console.log(caps.features); // ['kv', 'projections', 'entity-streams', ...]
832
+ console.log(caps.version); // Server version
833
+ ```
834
+
835
+ ## React Integration Patterns
836
+
837
+ ### Subscription with useEffect Cleanup
838
+
839
+ ```typescript
840
+ import { useEffect, useRef, useState } from 'react';
841
+ import { ironflow, type Subscription, type SubscriptionEvent } from '@ironflow/browser';
842
+
843
+ function OrderFeed() {
844
+ const [orders, setOrders] = useState<SubscriptionEvent[]>([]);
845
+ const subRef = useRef<Subscription | null>(null);
846
+
847
+ useEffect(() => {
848
+ let cancelled = false;
849
+
850
+ ironflow.subscribe('events:order.*', {
851
+ onEvent: (event) => {
852
+ if (!cancelled) {
853
+ setOrders((prev) => [...prev, event]);
854
+ }
855
+ },
856
+ replay: 50,
857
+ }).then((sub) => {
858
+ if (cancelled) {
859
+ sub.unsubscribe();
860
+ } else {
861
+ subRef.current = sub;
862
+ }
863
+ });
864
+
865
+ return () => {
866
+ cancelled = true;
867
+ subRef.current?.unsubscribe();
868
+ subRef.current = null;
869
+ };
870
+ }, []);
871
+
872
+ return (
873
+ <ul>
874
+ {orders.map((o, i) => (
875
+ <li key={i}>{o.name}: {JSON.stringify(o.data)}</li>
876
+ ))}
877
+ </ul>
878
+ );
879
+ }
880
+ ```
881
+
882
+ ### Custom useIronflowSubscription Hook
883
+
884
+ ```typescript
885
+ import { useEffect, useRef, useState, useCallback } from 'react';
886
+ import {
887
+ ironflow,
888
+ type Subscription,
889
+ type SubscriptionEvent,
890
+ type SubscriptionCallbacks,
891
+ type BrowserSubscribeOptions,
892
+ } from '@ironflow/browser';
893
+
894
+ function useIronflowSubscription<T = unknown>(
895
+ pattern: string | null,
896
+ options?: BrowserSubscribeOptions
897
+ ) {
898
+ const [events, setEvents] = useState<SubscriptionEvent<T>[]>([]);
899
+ const [error, setError] = useState<Error | null>(null);
900
+ const [connected, setConnected] = useState(false);
901
+ const subRef = useRef<Subscription | null>(null);
902
+
903
+ useEffect(() => {
904
+ if (!pattern) return;
905
+
906
+ let cancelled = false;
907
+
908
+ ironflow.subscribe<T>(pattern, {
909
+ onEvent: (event) => {
910
+ if (!cancelled) {
911
+ setEvents((prev) => [...prev, event]);
912
+ }
913
+ },
914
+ onError: (err) => {
915
+ if (!cancelled) {
916
+ setError(new Error(err.message));
917
+ }
918
+ },
919
+ onStateChange: (state) => {
920
+ if (!cancelled) {
921
+ setConnected(state === 'connected');
922
+ }
923
+ },
924
+ ...options,
925
+ }).then((sub) => {
926
+ if (cancelled) {
927
+ sub.unsubscribe();
928
+ } else {
929
+ subRef.current = sub;
930
+ setConnected(true);
931
+ }
932
+ }).catch((err) => {
933
+ if (!cancelled) {
934
+ setError(err);
935
+ }
936
+ });
937
+
938
+ return () => {
939
+ cancelled = true;
940
+ subRef.current?.unsubscribe();
941
+ subRef.current = null;
942
+ };
943
+ }, [pattern]);
944
+
945
+ const clear = useCallback(() => setEvents([]), []);
946
+
947
+ return { events, error, connected, clear };
948
+ }
949
+
950
+ // Usage
951
+ function Dashboard() {
952
+ const { events, error, connected } = useIronflowSubscription('system.run.>', {
953
+ replay: 20,
954
+ });
955
+
956
+ if (error) return <div>Error: {error.message}</div>;
957
+
958
+ return (
959
+ <div>
960
+ <span>{connected ? 'Connected' : 'Disconnected'}</span>
961
+ {events.map((e, i) => (
962
+ <div key={i}>{e.name}</div>
963
+ ))}
964
+ </div>
965
+ );
966
+ }
967
+ ```
968
+
969
+ ### Connection State Display
970
+
971
+ ```typescript
972
+ import { useEffect, useState } from 'react';
973
+ import { ironflow, type ConnectionState } from '@ironflow/browser';
974
+
975
+ function ConnectionStatus() {
976
+ const [state, setState] = useState<ConnectionState>(ironflow.connectionState);
977
+
978
+ useEffect(() => {
979
+ const unsubscribe = ironflow.onConnectionChange(setState);
980
+ return unsubscribe;
981
+ }, []);
982
+
983
+ const colors: Record<ConnectionState, string> = {
984
+ connected: 'green',
985
+ disconnected: 'red',
986
+ connecting: 'yellow',
987
+ reconnecting: 'orange',
988
+ };
989
+
990
+ return (
991
+ <span style={{ color: colors[state] }}>
992
+ {state}
993
+ </span>
994
+ );
995
+ }
996
+ ```
997
+
998
+ ### App-Level Configuration
999
+
1000
+ ```typescript
1001
+ // app/layout.tsx or main.tsx - configure once at app startup
1002
+ import { ironflow } from '@ironflow/browser';
1003
+
1004
+ ironflow.configure({
1005
+ serverUrl: process.env.NEXT_PUBLIC_IRONFLOW_URL ?? 'http://localhost:9123',
1006
+ auth: {
1007
+ apiKey: process.env.NEXT_PUBLIC_IRONFLOW_API_KEY,
1008
+ },
1009
+ });
1010
+ ```
1011
+
1012
+ ## Transport Configuration
1013
+
1014
+ The browser client supports two transport protocols for real-time subscriptions:
1015
+
1016
+ ### ConnectRPC (Default)
1017
+
1018
+ Uses HTTP/2 with Protocol Buffers. Preferred for production because it shares the same connection as REST API calls and supports bidirectional streaming.
1019
+
1020
+ ```typescript
1021
+ ironflow.configure({
1022
+ serverUrl: 'http://localhost:9123',
1023
+ transport: 'connectrpc',
1024
+ });
1025
+ ```
1026
+
1027
+ ### WebSocket
1028
+
1029
+ Uses a dedicated WebSocket connection. Useful as a fallback or when ConnectRPC is not available.
1030
+
1031
+ ```typescript
1032
+ ironflow.configure({
1033
+ serverUrl: 'http://localhost:9123',
1034
+ transport: 'websocket',
1035
+ });
1036
+ ```
1037
+
1038
+ The WebSocket URL is derived from `serverUrl` by replacing `http://` with `ws://` and `https://` with `wss://`.
1039
+
1040
+ ### Advanced: Custom Transport
1041
+
1042
+ For advanced use cases, transport factories and types are exported:
1043
+
1044
+ ```typescript
1045
+ import {
1046
+ createWebSocketTransport,
1047
+ createConnectRPCTransport,
1048
+ type Transport,
1049
+ type TransportOptions,
1050
+ type TransportCallbacks,
1051
+ type TransportFactory,
1052
+ } from '@ironflow/browser';
1053
+
1054
+ // Create a transport manually
1055
+ const options: TransportOptions = {
1056
+ auth: { apiKey: 'my-key' },
1057
+ autoReconnect: true,
1058
+ reconnectDelay: 1000,
1059
+ maxReconnectDelay: 30000,
1060
+ reconnectBackoff: 2,
1061
+ environment: 'default',
1062
+ connectionTimeout: 10000,
1063
+ };
1064
+
1065
+ const transport = createConnectRPCTransport('http://localhost:9123', options);
1066
+ ```
1067
+
1068
+ ## Error Handling
1069
+
1070
+ ### Error Types
1071
+
1072
+ All error types are re-exported from `@ironflow/core`:
1073
+
1074
+ ```typescript
1075
+ import {
1076
+ IronflowError, // Base error class for all Ironflow errors
1077
+ ConnectionError, // Connection failures
1078
+ SubscriptionError, // Subscription failures
1079
+ TimeoutError, // Request timeouts
1080
+ ValidationError, // Invalid response or input validation
1081
+ NotConfiguredError, // Client used before configure() was called
1082
+ } from '@ironflow/browser';
1083
+ ```
1084
+
1085
+ Additionally, the REST request helper maps HTTP status codes to specific error types:
1086
+
1087
+ - **401** -> `UnauthenticatedError` -- missing or invalid credentials
1088
+ - **402** -> `EnterpriseRequiredError` -- enterprise license required
1089
+ - **403** -> `UnauthorizedError` -- insufficient permissions
1090
+
1091
+ ### Error Utilities
1092
+
1093
+ ```typescript
1094
+ import { isRetryable, isIronflowError } from '@ironflow/browser';
1095
+
1096
+ try {
1097
+ await ironflow.trigger('process-order', { data: { orderId: '123' } });
1098
+ } catch (error) {
1099
+ if (isIronflowError(error)) {
1100
+ console.log(error.message); // Human-readable message
1101
+ console.log(error.code); // Machine-readable code (e.g., 'HTTP_500', 'TIMEOUT')
1102
+
1103
+ if (isRetryable(error)) {
1104
+ // Safe to retry (5xx errors, timeouts, connection failures)
1105
+ }
1106
+ }
1107
+ }
180
1108
  ```
181
1109
 
1110
+ ### Error Codes
1111
+
1112
+ Common error codes returned by the client:
1113
+
1114
+ | Code | Description |
1115
+ |------|-------------|
1116
+ | `HTTP_4xx` / `HTTP_5xx` | HTTP status-based errors |
1117
+ | `TIMEOUT` | Request exceeded the configured timeout |
1118
+ | `REQUEST_FAILED` | Network or fetch failure |
1119
+ | `PATCH_FAILED` | Step patch operation failed |
1120
+ | `RESUME_FAILED` | Run resume operation failed |
1121
+ | `NOT_CONFIGURED` | Client used before `configure()` |
1122
+
182
1123
  ## Browser Compatibility
183
1124
 
184
1125
  - Chrome 80+
@@ -186,9 +1127,40 @@ ironflow.disconnect();
186
1127
  - Safari 13.1+
187
1128
  - Edge 80+
188
1129
 
189
- ## Documentation
1130
+ Requires native `fetch`, `WebSocket`, and `AbortController` support.
1131
+
1132
+ ## Exported Types
1133
+
1134
+ The package re-exports the following types from `@ironflow/core` for convenience:
1135
+
1136
+ **Run types:** `Run`, `RunStatus`, `RunInfo`, `ListRunsOptions`, `ListRunsResult`
1137
+
1138
+ **Event types:** `IronflowEvent`, `EmitOptions`, `EmitResult`
1139
+
1140
+ **Trigger types:** `TriggerResult`, `TriggerSyncOptions`, `TriggerSyncResult`
1141
+
1142
+ **Subscription types:** `SubscribeOptions`, `Subscription`, `AckableSubscription`, `SubscriptionEvent`, `SubscriptionErrorInfo`, `SubscriptionCallbacks`, `ConnectionState`, `AckHandle`
1143
+
1144
+ **Consumer group types:** `ConsumerGroup`, `ConsumerGroupConfig`, `ConsumerGroupStatus`, `AckMode`, `BackpressureMode`
1145
+
1146
+ **Entity stream types:** `AppendEventInput`, `AppendOptions`, `AppendResult`, `ReadStreamOptions`, `StreamEvent`, `StreamInfo`, `EntitySubscribeOptions`
1147
+
1148
+ **Projection types:** `ProjectionStatusInfo`, `ProjectionStateResult`
1149
+
1150
+ **KV types:** `KVBucketConfig`, `KVBucketInfo`, `KVEntry`, `KVPutResult`, `KVListKeysResult`, `KVListBucketsResult`, `KVWatchEvent`, `KVWatchCallbacks`, `KVWatchOptions`, `KVWatcher`
1151
+
1152
+ **Config types:** `ConfigResponse`, `ConfigEntry`, `ConfigSetResult`, `ConfigWatchCallbacks`
1153
+
1154
+ **Browser-specific types:** `IronflowConfig`, `IronflowConfigOptions`, `ReconnectConfig`, `VisibilityConfig`, `AuthConfig`, `BrowserSubscribeOptions`, `SubscriptionGroup`, `Transport`, `TransportCallbacks`, `TransportFactory`, `TransportOptions`
1155
+
1156
+ **Utilities:** `patterns`, `DEFAULT_SERVER_URL`, `DEFAULT_WS_URL`, `DEFAULT_TIMEOUTS`, `getServerUrl`, `getWebSocketUrl`
1157
+
1158
+ **Classes:** `BrowserKVClient`, `BrowserKVBucketHandle`, `BrowserConfigClient`
1159
+
1160
+ ## Links
190
1161
 
191
- For the full API reference, see the [Browser Package Documentation](https://ironflow.dev/docs/api-reference/js-sdk/browser).
1162
+ - [Documentation](https://github.com/sahina/ironflow/tree/main/docs)
1163
+ - [GitHub Repository](https://github.com/sahina/ironflow)
192
1164
 
193
1165
  ## License
194
1166