openkbs-pulse 1.0.7 → 1.0.10

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 (3) hide show
  1. package/README.md +596 -83
  2. package/package.json +1 -1
  3. package/pulse.js +21 -6
package/README.md CHANGED
@@ -1,124 +1,581 @@
1
1
  # OpenKBS Pulse SDK
2
2
 
3
- Real-time WebSocket SDK for browser applications. Similar to Ably/Pusher.
3
+ Real-time WebSocket SDK for browser applications. Similar to Ably/Pusher but built for OpenKBS Elastic Services.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Architecture Overview](#architecture-overview)
8
+ - [How It Works](#how-it-works)
9
+ - [Installation](#installation)
10
+ - [Quick Start](#quick-start)
11
+ - [Channels](#channels)
12
+ - [Presence (Online Users)](#presence-online-users)
13
+ - [Connection Management](#connection-management)
14
+ - [Server-Side Publishing](#server-side-publishing)
15
+ - [API Reference](#api-reference)
16
+ - [Message Format](#message-format)
17
+ - [Security](#security)
18
+ - [Scalability](#scalability)
19
+
20
+ ---
21
+
22
+ ## Architecture Overview
23
+
24
+ ```
25
+ ┌─────────────────────────────────────────────────────────────────────────────┐
26
+ │ Client (Browser) │
27
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
28
+ │ │ Pulse SDK │ │
29
+ │ │ - Manages WebSocket connection │ │
30
+ │ │ - Handles reconnection with exponential backoff │ │
31
+ │ │ - Routes messages to channels │ │
32
+ │ │ - Tracks presence state │ │
33
+ │ └─────────────────────────────────────────────────────────────────────┘ │
34
+ └─────────────────────────────────────────────────────────────────────────────┘
35
+
36
+ │ WebSocket (wss://)
37
+
38
+ ┌─────────────────────────────────────────────────────────────────────────────┐
39
+ │ AWS API Gateway (WebSocket) │
40
+ │ Endpoints: │
41
+ │ - us-east-1: wss://pulse.vpc1.us │
42
+ │ - eu-central-1: wss://pulse.vpc1.eu │
43
+ │ │
44
+ │ Routes: │
45
+ │ - $connect → Lambda (validate token, store connection) │
46
+ │ - $disconnect → Lambda (remove connection) │
47
+ │ - $default → Lambda (handle publish/subscribe/presence) │
48
+ └─────────────────────────────────────────────────────────────────────────────┘
49
+
50
+
51
+ ┌─────────────────────────────────────────────────────────────────────────────┐
52
+ │ Lambda: openkbs-elastic-pulse │
53
+ │ │
54
+ │ Handles: │
55
+ │ - Token validation (RS256 kbToken or ES256 pulseToken) │
56
+ │ - Connection storage in DynamoDB │
57
+ │ - Message broadcasting to channel subscribers │
58
+ │ - Presence queries │
59
+ │ - Stale connection cleanup │
60
+ └─────────────────────────────────────────────────────────────────────────────┘
61
+
62
+
63
+ ┌─────────────────────────────────────────────────────────────────────────────┐
64
+ │ DynamoDB: openkbs-elastic-pulse-connections │
65
+ │ │
66
+ │ Schema: │
67
+ │ - Primary Key: kbId (partition) + connectionId (sort) │
68
+ │ - GSI: channel-index (kbId + channel) - for broadcasting │
69
+ │ - GSI: connectionId-index (connectionId) - for O(1) lookups │
70
+ │ - TTL: 24 hours auto-cleanup │
71
+ │ │
72
+ │ Fields: │
73
+ │ - kbId, connectionId, channel, userId, region, connectedAt, ttl │
74
+ └─────────────────────────────────────────────────────────────────────────────┘
75
+ ```
76
+
77
+ ---
78
+
79
+ ## How It Works
80
+
81
+ ### Connection Flow
82
+
83
+ 1. **Client connects** with `kbId` and `token` via WebSocket
84
+ 2. **Lambda validates token** (JWT with RS256 or ES256)
85
+ 3. **Checks KB has Pulse enabled** (`elasticPulseEnabled` flag)
86
+ 4. **Stores connection** in DynamoDB with channel and user info
87
+ 5. **Client subscribes to channels** by sending `{action: 'subscribe', channel: 'name'}`
88
+ 6. **Messages are broadcast** to all connections in the same kbId + channel
89
+
90
+ ### Message Flow
91
+
92
+ ```
93
+ Client A Lambda Client B
94
+ │ │ │
95
+ │ publish('chat', 'msg', │ │
96
+ │ {text: 'Hi'}) │ │
97
+ │ ─────────────────────────> │
98
+ │ │ │
99
+ │ │ Query DynamoDB for │
100
+ │ │ all connections in │
101
+ │ │ kbId + channel │
102
+ │ │ │
103
+ │ │ postToConnection() │
104
+ │ │ ─────────────────────────>
105
+ │ │ │
106
+ │ │ {type: 'message',│
107
+ │ │ channel: 'chat',│
108
+ │ │ data: {text:'Hi'}}
109
+ ```
110
+
111
+ ### Presence Flow
112
+
113
+ ```
114
+ Client A Lambda Client B
115
+ │ │ │
116
+ │ {action: 'presence', │ │
117
+ │ channel: 'chat'} │ │
118
+ │ ─────────────────────────> │
119
+ │ │ │
120
+ │ │ Query all connections │
121
+ │ │ in kbId + channel │
122
+ │ │ │
123
+ │ {type: 'presence',│ │
124
+ │ action: 'sync', │ │
125
+ │ count: 5, │ │
126
+ │ members: [...]} │ │
127
+ │ <───────────────────────── │
128
+ ```
129
+
130
+ ---
4
131
 
5
132
  ## Installation
6
133
 
7
- ### CDN
134
+ ### CDN (Browser)
135
+
8
136
  ```html
9
137
  <script src="https://cdn.openkbs.com/pulse/pulse.min.js"></script>
10
138
  ```
11
139
 
12
- ### NPM (coming soon)
140
+ ### NPM
141
+
13
142
  ```bash
14
143
  npm install @openkbs/pulse
15
144
  ```
16
145
 
146
+ ```javascript
147
+ import Pulse from '@openkbs/pulse';
148
+ ```
149
+
150
+ ---
151
+
17
152
  ## Quick Start
18
153
 
19
154
  ```javascript
20
- // Initialize
155
+ // Initialize with your KB credentials
21
156
  const pulse = new Pulse({
22
157
  kbId: 'your-kb-id',
23
- token: 'user-pulse-token', // From auth response
24
- region: 'eu-central-1', // Optional: us-east-1 (default), eu-central-1, ap-southeast-1
158
+ token: 'user-pulse-token', // From auth response or Elastic Function
159
+ region: 'us-east-1', // Optional: us-east-1 (default), eu-central-1
25
160
  debug: true // Optional: enable console logging
26
161
  });
27
162
 
28
163
  // Subscribe to a channel
29
- const channel = pulse.channel('posts');
164
+ const channel = pulse.channel('notifications');
30
165
 
31
166
  // Listen for specific events
32
- channel.subscribe('new_post', (data) => {
33
- console.log('New post:', data);
167
+ channel.subscribe('new_order', (data) => {
168
+ console.log('New order:', data);
34
169
  });
35
170
 
36
- // Listen for all events
37
- channel.subscribe((data, message) => {
38
- console.log('Any event:', message.event, data);
171
+ // Publish an event (broadcasts to all subscribers)
172
+ channel.publish('new_order', {
173
+ orderId: '12345',
174
+ amount: 99.99
39
175
  });
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Channels
181
+
182
+ Channels are logical groupings for messages. Each KB can have unlimited channels.
183
+
184
+ ### Creating/Getting a Channel
185
+
186
+ ```javascript
187
+ const channel = pulse.channel('channel-name');
188
+ ```
189
+
190
+ Channels are created on-demand. The same channel instance is returned for the same name.
40
191
 
41
- // Publish event
42
- channel.publish('new_post', {
43
- title: 'Hello World',
44
- content: 'My first post'
192
+ ### Subscribing to Events
193
+
194
+ ```javascript
195
+ // Subscribe to a specific event type
196
+ const unsubscribe = channel.subscribe('event_name', (data, rawMessage) => {
197
+ console.log('Event data:', data);
198
+ console.log('Full message:', rawMessage);
199
+ });
200
+
201
+ // Subscribe to ALL events on the channel
202
+ channel.subscribe((data, rawMessage) => {
203
+ console.log('Any event:', rawMessage.event, data);
45
204
  });
205
+
206
+ // Unsubscribe
207
+ unsubscribe();
46
208
  ```
47
209
 
210
+ ### Publishing Events
211
+
212
+ ```javascript
213
+ // Publish to channel (broadcasts to all OTHER subscribers)
214
+ channel.publish('event_name', {
215
+ any: 'data',
216
+ you: 'want'
217
+ });
218
+ ```
219
+
220
+ Note: The sender does NOT receive their own published messages.
221
+
222
+ ### Unsubscribing from Channel
223
+
224
+ ```javascript
225
+ channel.unsubscribe(); // Removes all event listeners
226
+ ```
227
+
228
+ ---
229
+
48
230
  ## Presence (Online Users)
49
231
 
232
+ Track who's currently online in a channel.
233
+
234
+ ### Subscribe to Presence Changes
235
+
50
236
  ```javascript
51
237
  const channel = pulse.channel('chat-room');
52
238
 
53
- // Track who's online
239
+ // Get notified when member list changes
54
240
  channel.presence.subscribe((members) => {
55
- console.log('Online users:', members.length);
241
+ console.log('Online count:', members.length);
242
+ members.forEach(m => {
243
+ console.log(`- ${m.userId} (connected at ${m.connectedAt})`);
244
+ });
56
245
  });
57
246
 
58
- // Enter presence with user data
247
+ // Get current member count (may be higher than members.length if > 100)
248
+ console.log('Total online:', channel.presence.count);
249
+ ```
250
+
251
+ ### Enter Presence
252
+
253
+ ```javascript
254
+ // Enter with custom data
59
255
  channel.presence.enter({
60
- name: 'John',
61
- avatar: 'https://...'
256
+ name: 'John Doe',
257
+ avatar: 'https://example.com/avatar.jpg',
258
+ status: 'online'
62
259
  });
260
+ ```
63
261
 
64
- // Listen for members joining/leaving
262
+ ### Listen for Join/Leave Events
263
+
264
+ ```javascript
65
265
  channel.presence.onEnter((member) => {
66
- console.log('User joined:', member.data.name);
266
+ console.log('User joined:', member.userId, member.data);
67
267
  });
68
268
 
69
269
  channel.presence.onLeave((member) => {
70
- console.log('User left:', member.data.name);
270
+ console.log('User left:', member.userId);
71
271
  });
272
+ ```
72
273
 
73
- // Update your presence data
74
- channel.presence.update({ status: 'away' });
274
+ ### Update Presence Data
75
275
 
76
- // Leave presence
276
+ ```javascript
277
+ channel.presence.update({
278
+ status: 'away',
279
+ lastSeen: Date.now()
280
+ });
281
+ ```
282
+
283
+ ### Leave Presence
284
+
285
+ ```javascript
77
286
  channel.presence.leave();
78
287
  ```
79
288
 
80
- ## Connection State
289
+ ### Presence Member Object
81
290
 
82
291
  ```javascript
83
- // Check connection state
84
- console.log(pulse.state); // 'connected', 'disconnected', 'connecting', etc.
292
+ {
293
+ connectionId: 'abc123', // Unique connection ID
294
+ userId: 'user@example.com', // From token
295
+ connectedAt: 1703952000000, // Connection timestamp
296
+ data: { ... } // Custom data from enter()
297
+ }
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Connection Management
303
+
304
+ ### Connection States
305
+
306
+ | State | Description |
307
+ |-------|-------------|
308
+ | `disconnected` | Not connected to server |
309
+ | `connecting` | Establishing WebSocket connection |
310
+ | `connected` | Connected and ready |
311
+ | `reconnecting` | Lost connection, attempting to reconnect |
312
+ | `failed` | Connection attempt failed |
313
+ | `disconnecting` | Closing connection |
314
+
315
+ ### Monitoring Connection State
316
+
317
+ ```javascript
318
+ // Get current state
319
+ console.log(pulse.state); // 'connected', 'disconnected', etc.
85
320
  console.log(pulse.isConnected); // true/false
86
321
 
87
322
  // Listen for state changes
88
- pulse.onStateChange((state) => {
89
- if (state === 'connected') {
90
- console.log('Online!');
91
- } else if (state === 'disconnected') {
92
- console.log('Offline');
323
+ const unsubscribe = pulse.onStateChange((state) => {
324
+ switch (state) {
325
+ case 'connected':
326
+ console.log('Online!');
327
+ break;
328
+ case 'disconnected':
329
+ console.log('Offline');
330
+ break;
331
+ case 'reconnecting':
332
+ console.log('Reconnecting...');
333
+ break;
93
334
  }
94
335
  });
95
336
 
96
- // Manual connect/disconnect
337
+ // Stop listening
338
+ unsubscribe();
339
+ ```
340
+
341
+ ### Manual Connect/Disconnect
342
+
343
+ ```javascript
344
+ // Disconnect (stops reconnection attempts)
97
345
  pulse.disconnect();
346
+
347
+ // Reconnect
98
348
  pulse.connect();
99
349
  ```
100
350
 
351
+ ### Automatic Reconnection
352
+
353
+ The SDK automatically reconnects with exponential backoff:
354
+ - 1s → 2s → 5s → 10s → 30s (max)
355
+
356
+ On reconnection, all channel subscriptions are automatically restored.
357
+
358
+ ---
359
+
360
+ ## Server-Side Publishing
361
+
362
+ Publish messages from Elastic Functions or Node.js servers.
363
+
364
+ ### Using Elastic Functions
365
+
366
+ ```javascript
367
+ // In your Elastic Function
368
+ export default async function handler(event, context) {
369
+ // Publish to all connected clients
370
+ await context.pulse.publish('notifications', 'new_alert', {
371
+ message: 'Server update available',
372
+ severity: 'info'
373
+ });
374
+
375
+ // Get presence info
376
+ const { count, members } = await context.pulse.presence('chat-room');
377
+ console.log(`${count} users online`);
378
+
379
+ return { success: true };
380
+ }
381
+ ```
382
+
383
+ ### Using Server SDK
384
+
385
+ ```javascript
386
+ import pulse from '@openkbs/pulse/server';
387
+
388
+ // Generate token for client authentication
389
+ const { token, endpoint, region } = await pulse.getToken(kbId, apiKey, userId);
390
+
391
+ // Publish from server
392
+ await pulse.publish('channel', 'event', { data: 'value' });
393
+
394
+ // Get presence
395
+ const { count, members } = await pulse.presence('channel');
396
+ ```
397
+
398
+ ---
399
+
101
400
  ## API Reference
102
401
 
103
- ### Pulse Constructor Options
402
+ ### Pulse Constructor
403
+
404
+ ```javascript
405
+ new Pulse(options)
406
+ ```
104
407
 
105
408
  | Option | Type | Default | Description |
106
409
  |--------|------|---------|-------------|
107
- | kbId | string | required | Your KB ID |
108
- | token | string | required | User's pulse token (from auth) |
109
- | region | string | 'us-east-1' | Server region |
110
- | endpoint | string | auto | Custom WebSocket endpoint |
111
- | debug | boolean | false | Enable debug logging |
112
- | autoConnect | boolean | true | Connect automatically |
410
+ | `kbId` | string | **required** | Your Knowledge Base ID |
411
+ | `token` | string | **required** | JWT token for authentication |
412
+ | `region` | string | `'us-east-1'` | Server region (`us-east-1`, `eu-central-1`) |
413
+ | `endpoint` | string | auto | Custom WebSocket endpoint URL |
414
+ | `channel` | string | `'default'` | Default channel to connect to |
415
+ | `debug` | boolean | `false` | Enable console debug logging |
416
+ | `autoConnect` | boolean | `true` | Connect automatically on instantiation |
113
417
 
114
- ### Connection States
418
+ ### Pulse Instance Methods
419
+
420
+ | Method | Returns | Description |
421
+ |--------|---------|-------------|
422
+ | `channel(name)` | `PulseChannel` | Get or create a channel |
423
+ | `connect()` | void | Connect to WebSocket server |
424
+ | `disconnect()` | void | Disconnect from server |
425
+ | `onStateChange(callback)` | unsubscribe fn | Listen for connection state changes |
426
+
427
+ ### Pulse Instance Properties
428
+
429
+ | Property | Type | Description |
430
+ |----------|------|-------------|
431
+ | `state` | string | Current connection state |
432
+ | `isConnected` | boolean | Whether currently connected |
433
+
434
+ ### PulseChannel Methods
435
+
436
+ | Method | Returns | Description |
437
+ |--------|---------|-------------|
438
+ | `subscribe(event, callback)` | unsubscribe fn | Subscribe to specific event |
439
+ | `subscribe(callback)` | unsubscribe fn | Subscribe to all events |
440
+ | `publish(event, data)` | boolean | Publish event to channel |
441
+ | `unsubscribe()` | void | Unsubscribe from channel |
442
+
443
+ ### PulsePresence Methods
444
+
445
+ | Method | Returns | Description |
446
+ |--------|---------|-------------|
447
+ | `subscribe(callback)` | unsubscribe fn | Subscribe to member list changes |
448
+ | `onEnter(callback)` | unsubscribe fn | Subscribe to member join events |
449
+ | `onLeave(callback)` | unsubscribe fn | Subscribe to member leave events |
450
+ | `enter(data)` | void | Enter presence with optional data |
451
+ | `leave()` | void | Leave presence |
452
+ | `update(data)` | void | Update presence data |
453
+
454
+ ### PulsePresence Properties
115
455
 
116
- - `disconnected` - Not connected
117
- - `connecting` - Establishing connection
118
- - `connected` - Connected and ready
119
- - `reconnecting` - Lost connection, attempting to reconnect
120
- - `failed` - Connection failed
121
- - `disconnecting` - Closing connection
456
+ | Property | Type | Description |
457
+ |----------|------|-------------|
458
+ | `members` | array | Current member list (max 100) |
459
+ | `count` | number | Total member count |
460
+
461
+ ---
462
+
463
+ ## Message Format
464
+
465
+ ### Client → Server
466
+
467
+ ```javascript
468
+ // Subscribe to channel
469
+ { action: 'subscribe', channel: 'channel-name' }
470
+
471
+ // Unsubscribe from channel
472
+ { action: 'unsubscribe', channel: 'channel-name' }
473
+
474
+ // Publish message
475
+ { action: 'publish', channel: 'channel-name', event: 'event-name', data: {...} }
476
+
477
+ // Request presence
478
+ { action: 'presence', channel: 'channel-name' }
479
+
480
+ // Enter presence
481
+ { action: 'presence_enter', channel: 'channel-name', data: {...} }
482
+
483
+ // Leave presence
484
+ { action: 'presence_leave', channel: 'channel-name' }
485
+
486
+ // Update presence
487
+ { action: 'presence_update', channel: 'channel-name', data: {...} }
488
+
489
+ // Ping (heartbeat)
490
+ { action: 'ping' }
491
+ ```
492
+
493
+ ### Server → Client
494
+
495
+ ```javascript
496
+ // Message broadcast
497
+ { type: 'message', channel: 'channel-name', data: {...}, timestamp: 1703952000000 }
498
+
499
+ // Presence sync (full member list)
500
+ { type: 'presence', action: 'sync', channel: 'channel-name', count: 5, members: [...] }
501
+
502
+ // Presence enter
503
+ { type: 'presence', action: 'enter', channel: 'channel-name', member: {...} }
504
+
505
+ // Presence leave
506
+ { type: 'presence', action: 'leave', channel: 'channel-name', member: {...} }
507
+
508
+ // Pong response
509
+ { type: 'pong', timestamp: 1703952000000 }
510
+
511
+ // Error
512
+ { type: 'error', error: 'Error message' }
513
+ ```
514
+
515
+ ---
516
+
517
+ ## Security
518
+
519
+ ### Authentication
520
+
521
+ Pulse uses JWT tokens for authentication:
522
+
523
+ 1. **KB Token (RS256)**: Standard OpenKBS user token
524
+ 2. **Pulse Token (ES256)**: Generated by Elastic Functions for specific users
525
+ 3. **Public Chat Token (ES256)**: For anonymous/public access
526
+
527
+ Token must contain:
528
+ - `kbId`: Must match the connection's kbId
529
+ - `userId` or `email` or `chatId`: User identifier
530
+
531
+ ### Authorization
532
+
533
+ - Connections are validated against the KB's `elasticPulseEnabled` flag
534
+ - Each connection is scoped to a single KB (cross-KB messaging not allowed)
535
+ - Channels within a KB are not authenticated (any valid token can access any channel)
536
+
537
+ ### Connection Security
538
+
539
+ - All connections use WSS (WebSocket Secure)
540
+ - Connections expire after 24 hours (TTL)
541
+ - Stale connections are automatically cleaned up
542
+
543
+ ---
544
+
545
+ ## Scalability
546
+
547
+ ### Design for Scale
548
+
549
+ The Pulse backend is designed to handle:
550
+ - **Many KBs (applications)**: Each KB is isolated
551
+ - **Hundreds of concurrent users per KB**: Efficient per-channel queries
552
+ - **Large channels**: Paginated broadcasting (100 connections per batch)
553
+
554
+ ### DynamoDB Indexes
555
+
556
+ | Index | Keys | Purpose |
557
+ |-------|------|---------|
558
+ | Primary | `kbId` + `connectionId` | Unique connection lookup |
559
+ | `channel-index` | `kbId` + `channel` | Broadcast to channel subscribers |
560
+ | `connectionId-index` | `connectionId` | O(1) connection lookup for messages |
561
+
562
+ ### Limits
563
+
564
+ | Resource | Limit |
565
+ |----------|-------|
566
+ | Presence member list | 100 members (count is accurate) |
567
+ | Connection TTL | 24 hours |
568
+ | Message size | 32 KB (API Gateway limit) |
569
+ | Broadcast batch | 100 concurrent sends |
570
+
571
+ ### Best Practices
572
+
573
+ 1. **Use specific channels**: Don't put all users in one channel
574
+ 2. **Clean up presence**: Call `leave()` when user navigates away
575
+ 3. **Handle reconnection**: Subscribe handlers survive reconnection automatically
576
+ 4. **Monitor state**: Show connection status to users
577
+
578
+ ---
122
579
 
123
580
  ## Full Example
124
581
 
@@ -126,14 +583,25 @@ pulse.connect();
126
583
  <!DOCTYPE html>
127
584
  <html>
128
585
  <head>
129
- <title>Pulse Example</title>
586
+ <title>Pulse Chat Example</title>
130
587
  <script src="https://cdn.openkbs.com/pulse/pulse.min.js"></script>
588
+ <style>
589
+ .status { padding: 5px 10px; border-radius: 10px; }
590
+ .connected { background: #4CAF50; color: white; }
591
+ .disconnected { background: #f44336; color: white; }
592
+ #messages { height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
593
+ </style>
131
594
  </head>
132
595
  <body>
133
- <div id="status">Connecting...</div>
134
- <div id="online">Online: 0</div>
596
+ <h1>Pulse Chat</h1>
597
+ <div>
598
+ <span id="status" class="status disconnected">Connecting...</span>
599
+ <span id="online">👥 0 online</span>
600
+ </div>
601
+
135
602
  <div id="messages"></div>
136
603
 
604
+ <input type="text" id="name" placeholder="Your name" value="Anonymous">
137
605
  <input type="text" id="input" placeholder="Type message...">
138
606
  <button onclick="send()">Send</button>
139
607
 
@@ -145,62 +613,107 @@ pulse.connect();
145
613
  });
146
614
 
147
615
  const channel = pulse.channel('chat');
616
+ const statusEl = document.getElementById('status');
617
+ const onlineEl = document.getElementById('online');
618
+ const messagesEl = document.getElementById('messages');
148
619
 
149
620
  // Connection status
150
621
  pulse.onStateChange((state) => {
151
- document.getElementById('status').textContent = state;
622
+ statusEl.textContent = state;
623
+ statusEl.className = 'status ' + (state === 'connected' ? 'connected' : 'disconnected');
152
624
  });
153
625
 
154
- // Presence
626
+ // Presence tracking
155
627
  channel.presence.subscribe((members) => {
156
- document.getElementById('online').textContent = 'Online: ' + members.length;
628
+ onlineEl.textContent = '👥 ' + members.length + ' online';
157
629
  });
158
- channel.presence.enter({ name: 'User' });
159
630
 
160
- // Messages
631
+ channel.presence.onEnter((member) => {
632
+ addMessage('system', `${member.userId} joined`);
633
+ });
634
+
635
+ channel.presence.onLeave((member) => {
636
+ addMessage('system', `${member.userId} left`);
637
+ });
638
+
639
+ // Enter presence when connected
640
+ pulse.onStateChange((state) => {
641
+ if (state === 'connected') {
642
+ channel.presence.enter({ name: document.getElementById('name').value });
643
+ }
644
+ });
645
+
646
+ // Message handling
161
647
  channel.subscribe('message', (data) => {
162
- const div = document.createElement('div');
163
- div.textContent = data.name + ': ' + data.text;
164
- document.getElementById('messages').appendChild(div);
648
+ addMessage(data.name, data.text);
165
649
  });
166
650
 
651
+ function addMessage(name, text) {
652
+ const div = document.createElement('div');
653
+ div.innerHTML = '<strong>' + name + ':</strong> ' + text;
654
+ messagesEl.appendChild(div);
655
+ messagesEl.scrollTop = messagesEl.scrollHeight;
656
+ }
657
+
167
658
  function send() {
168
659
  const input = document.getElementById('input');
169
- channel.publish('message', {
170
- name: 'User',
171
- text: input.value
172
- });
173
- input.value = '';
660
+ const name = document.getElementById('name').value || 'Anonymous';
661
+
662
+ if (input.value.trim()) {
663
+ channel.publish('message', { name, text: input.value });
664
+ addMessage(name, input.value); // Show own message
665
+ input.value = '';
666
+ }
174
667
  }
668
+
669
+ // Send on Enter key
670
+ document.getElementById('input').addEventListener('keypress', (e) => {
671
+ if (e.key === 'Enter') send();
672
+ });
673
+
674
+ // Leave presence on page unload
675
+ window.addEventListener('beforeunload', () => {
676
+ channel.presence.leave();
677
+ });
175
678
  </script>
176
679
  </body>
177
680
  </html>
178
681
  ```
179
682
 
180
- ## Server SDK
683
+ ---
181
684
 
182
- For server-side usage (Elastic Functions, Node.js):
685
+ ## Troubleshooting
183
686
 
184
- ```javascript
185
- import pulse from 'openkbs-pulse/server';
687
+ ### Connection Issues
186
688
 
187
- // Get a token for client WebSocket auth
188
- const { token, endpoint, region } = await pulse.getToken(kbId, apiKey, userId);
689
+ **Problem**: `Invalid token` error on connect
690
+ - Ensure token is valid JWT with correct `kbId`
691
+ - Check token hasn't expired
692
+ - Verify KB has `elasticPulseEnabled: true`
189
693
 
190
- // Publish to channel
191
- await pulse.publish('channel', 'event', { data });
694
+ **Problem**: Connection keeps reconnecting
695
+ - Check network connectivity
696
+ - Verify WebSocket endpoint is correct for your region
697
+ - Enable `debug: true` to see detailed logs
192
698
 
193
- // Get presence info
194
- const { count, members } = await pulse.presence('channel');
195
- ```
699
+ ### Presence Issues
700
+
701
+ **Problem**: Presence count shows 0
702
+ - Request presence after subscribing: `{action: 'presence', channel: 'name'}`
703
+ - Ensure channel name matches exactly (case-sensitive)
704
+
705
+ **Problem**: Members list is truncated
706
+ - By design, only 100 members are returned
707
+ - Use `channel.presence.count` for accurate total
708
+
709
+ ### Message Issues
196
710
 
197
- ### Server SDK Functions
711
+ **Problem**: Not receiving published messages
712
+ - Verify you're subscribed to the correct channel and event
713
+ - Publishers don't receive their own messages
714
+ - Check `debug: true` logs for incoming messages
198
715
 
199
- | Function | Parameters | Description |
200
- |----------|------------|-------------|
201
- | `getToken(kbId, apiKey, userId)` | kbId, apiKey (required), userId (default: 'anonymous') | Get WebSocket auth token |
202
- | `publish(channel, event, data)` | All required | Publish message to channel |
203
- | `presence(channel)` | channel name | Get online count and members |
716
+ ---
204
717
 
205
718
  ## License
206
719
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openkbs-pulse",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "Real-time WebSocket SDK for OpenKBS",
5
5
  "main": "pulse.js",
6
6
  "types": "pulse.d.ts",
package/pulse.js CHANGED
@@ -174,15 +174,25 @@
174
174
  try {
175
175
  const msg = JSON.parse(data);
176
176
 
177
- // Handle server message format: {type: 'message', data: {...}}
177
+ // Handle server message format: {type: 'message', channel: 'x', data: {...}}
178
178
  if (msg.type === 'message' && msg.data) {
179
- // Route to the connected channel
180
- const channel = this._channels[this._defaultChannel];
179
+ // Route to the correct channel based on msg.channel
180
+ const channelName = msg.channel || this._defaultChannel;
181
+ const channel = this._channels[channelName];
181
182
  if (channel) {
182
183
  channel._handleMessage(msg.data);
183
184
  }
184
185
  }
185
186
 
187
+ // Handle presence response: {type: 'presence', channel: 'x', action: 'sync', members: [...]}
188
+ if (msg.type === 'presence') {
189
+ const channelName = msg.channel || this._defaultChannel;
190
+ const channel = this._channels[channelName];
191
+ if (channel && channel.presence) {
192
+ channel.presence._handleMessage(msg);
193
+ }
194
+ }
195
+
186
196
  // Handle system messages
187
197
  if (msg.type === 'error') {
188
198
  this._log('Server error:', msg.error);
@@ -352,6 +362,7 @@
352
362
  constructor(channel) {
353
363
  this._channel = channel;
354
364
  this._members = [];
365
+ this._count = 0;
355
366
  this._subscribers = [];
356
367
  this._enterSubscribers = [];
357
368
  this._leaveSubscribers = [];
@@ -437,24 +448,28 @@
437
448
  }
438
449
 
439
450
  /**
440
- * Get member count
451
+ * Get member count (may be higher than members.length if list is limited)
441
452
  */
442
453
  get count() {
443
- return this._members.length;
454
+ return this._count || this._members.length;
444
455
  }
445
456
 
446
457
  // Internal methods
447
458
 
448
459
  _handleMessage(msg) {
449
- if (msg.action === 'sync') {
460
+ // Handle sync (full member list) - can come from action='sync' or just type='presence'
461
+ if (msg.action === 'sync' || (msg.type === 'presence' && msg.members)) {
450
462
  this._members = msg.members || [];
463
+ this._count = msg.count !== undefined ? msg.count : this._members.length;
451
464
  this._notifySubscribers();
452
465
  } else if (msg.action === 'enter') {
453
466
  this._members.push(msg.member);
467
+ this._count = (this._count || 0) + 1;
454
468
  this._notifySubscribers();
455
469
  this._enterSubscribers.forEach(cb => cb(msg.member));
456
470
  } else if (msg.action === 'leave') {
457
471
  this._members = this._members.filter(m => m.connectionId !== msg.member.connectionId);
472
+ this._count = Math.max(0, (this._count || 1) - 1);
458
473
  this._notifySubscribers();
459
474
  this._leaveSubscribers.forEach(cb => cb(msg.member));
460
475
  }