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.
- package/README.md +596 -83
- package/package.json +1 -1
- 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
|
|
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: '
|
|
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('
|
|
164
|
+
const channel = pulse.channel('notifications');
|
|
30
165
|
|
|
31
166
|
// Listen for specific events
|
|
32
|
-
channel.subscribe('
|
|
33
|
-
console.log('New
|
|
167
|
+
channel.subscribe('new_order', (data) => {
|
|
168
|
+
console.log('New order:', data);
|
|
34
169
|
});
|
|
35
170
|
|
|
36
|
-
//
|
|
37
|
-
channel.
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
//
|
|
239
|
+
// Get notified when member list changes
|
|
54
240
|
channel.presence.subscribe((members) => {
|
|
55
|
-
console.log('Online
|
|
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
|
-
//
|
|
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
|
-
|
|
262
|
+
### Listen for Join/Leave Events
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
65
265
|
channel.presence.onEnter((member) => {
|
|
66
|
-
console.log('User joined:', member.data
|
|
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.
|
|
270
|
+
console.log('User left:', member.userId);
|
|
71
271
|
});
|
|
272
|
+
```
|
|
72
273
|
|
|
73
|
-
|
|
74
|
-
channel.presence.update({ status: 'away' });
|
|
274
|
+
### Update Presence Data
|
|
75
275
|
|
|
76
|
-
|
|
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
|
-
|
|
289
|
+
### Presence Member Object
|
|
81
290
|
|
|
82
291
|
```javascript
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
108
|
-
| token | string | required |
|
|
109
|
-
| region | string | 'us-east-1' | Server region |
|
|
110
|
-
| endpoint | string | auto | Custom WebSocket endpoint |
|
|
111
|
-
|
|
|
112
|
-
|
|
|
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
|
-
###
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
<
|
|
134
|
-
<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
|
-
|
|
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
|
-
|
|
628
|
+
onlineEl.textContent = '👥 ' + members.length + ' online';
|
|
157
629
|
});
|
|
158
|
-
channel.presence.enter({ name: 'User' });
|
|
159
630
|
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
683
|
+
---
|
|
181
684
|
|
|
182
|
-
|
|
685
|
+
## Troubleshooting
|
|
183
686
|
|
|
184
|
-
|
|
185
|
-
import pulse from 'openkbs-pulse/server';
|
|
687
|
+
### Connection Issues
|
|
186
688
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
|
180
|
-
const
|
|
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
|
-
|
|
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
|
}
|