@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.
Files changed (2) hide show
  1. package/README.md +1045 -98
  2. package/package.json +1 -1
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,1072 @@ 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
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
- // Subscribe to events
22
- const sub = ironflow.subscribe('events:order.*', {
23
- onEvent: (event) => console.log('Order:', event),
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
30
125
 
31
- // Emit events
32
- await ironflow.emit('order.approved', { orderId: '123' }, { version: 1 });
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
- ## 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,
52
157
 
53
- The client auto-detects the best available transport, or you can choose explicitly:
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.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',
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
- // Subscribe to real-time stream updates
105
- const sub = ironflow.streams.subscribe('order-123', {
106
- onEvent: (event) => console.log('Stream event:', event),
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
- // Get stream metadata
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
- ### Projections
444
+ ### Subscribe to Stream Updates
114
445
 
115
446
  ```typescript
116
- // Get projection state
117
- const state = await ironflow.getProjection('order-totals', {
118
- partition: 'region-us',
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
- // 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),
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
- // Management
128
- const status = await ironflow.getProjectionStatus('order-totals');
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
- await ironflow.rebuildProjection('order-totals', { dryRun: true });
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
- ### KV Store
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
- const bucket = kv.bucket('my-bucket');
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
- await bucket.put('key', { value: 'data' });
140
- const entry = await bucket.get('key');
141
- await bucket.delete('key');
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
- ### Config Management
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
- await config.set('app-settings', { theme: 'dark', locale: 'en' });
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
- ### Server Inspection
681
+ ## Auth Management
156
682
 
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) |
683
+ ### API Keys
163
684
 
164
- ## Connection Management
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
- // Monitor connection state
168
- ironflow.onConnectionChange((state) => {
169
- console.log('Connection:', state); // 'connected' | 'disconnected' | 'reconnecting'
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
- // Global error handler
173
- ironflow.onError((error) => {
174
- console.error('Client error:', error);
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
- // Manual connect/disconnect
178
- await ironflow.connect();
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
- ## Documentation
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
- For the full API reference, see the [Browser Package Documentation](https://ironflow.dev/docs/api-reference/js-sdk/browser).
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.7.1",
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",