openkbs-pulse 1.0.2

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 (4) hide show
  1. package/README.md +182 -0
  2. package/package.json +20 -0
  3. package/pulse.d.ts +50 -0
  4. package/pulse.js +477 -0
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # OpenKBS Pulse SDK
2
+
3
+ Real-time WebSocket SDK for browser applications. Similar to Ably/Pusher.
4
+
5
+ ## Installation
6
+
7
+ ### CDN
8
+ ```html
9
+ <script src="https://cdn.openkbs.com/pulse/pulse.min.js"></script>
10
+ ```
11
+
12
+ ### NPM (coming soon)
13
+ ```bash
14
+ npm install @openkbs/pulse
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```javascript
20
+ // Initialize
21
+ const pulse = new Pulse({
22
+ 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
25
+ debug: true // Optional: enable console logging
26
+ });
27
+
28
+ // Subscribe to a channel
29
+ const channel = pulse.channel('posts');
30
+
31
+ // Listen for specific events
32
+ channel.subscribe('new_post', (data) => {
33
+ console.log('New post:', data);
34
+ });
35
+
36
+ // Listen for all events
37
+ channel.subscribe((data, message) => {
38
+ console.log('Any event:', message.event, data);
39
+ });
40
+
41
+ // Publish event
42
+ channel.publish('new_post', {
43
+ title: 'Hello World',
44
+ content: 'My first post'
45
+ });
46
+ ```
47
+
48
+ ## Presence (Online Users)
49
+
50
+ ```javascript
51
+ const channel = pulse.channel('chat-room');
52
+
53
+ // Track who's online
54
+ channel.presence.subscribe((members) => {
55
+ console.log('Online users:', members.length);
56
+ });
57
+
58
+ // Enter presence with user data
59
+ channel.presence.enter({
60
+ name: 'John',
61
+ avatar: 'https://...'
62
+ });
63
+
64
+ // Listen for members joining/leaving
65
+ channel.presence.onEnter((member) => {
66
+ console.log('User joined:', member.data.name);
67
+ });
68
+
69
+ channel.presence.onLeave((member) => {
70
+ console.log('User left:', member.data.name);
71
+ });
72
+
73
+ // Update your presence data
74
+ channel.presence.update({ status: 'away' });
75
+
76
+ // Leave presence
77
+ channel.presence.leave();
78
+ ```
79
+
80
+ ## Connection State
81
+
82
+ ```javascript
83
+ // Check connection state
84
+ console.log(pulse.state); // 'connected', 'disconnected', 'connecting', etc.
85
+ console.log(pulse.isConnected); // true/false
86
+
87
+ // 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');
93
+ }
94
+ });
95
+
96
+ // Manual connect/disconnect
97
+ pulse.disconnect();
98
+ pulse.connect();
99
+ ```
100
+
101
+ ## API Reference
102
+
103
+ ### Pulse Constructor Options
104
+
105
+ | Option | Type | Default | Description |
106
+ |--------|------|---------|-------------|
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 |
113
+
114
+ ### Connection States
115
+
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
122
+
123
+ ## Full Example
124
+
125
+ ```html
126
+ <!DOCTYPE html>
127
+ <html>
128
+ <head>
129
+ <title>Pulse Example</title>
130
+ <script src="https://cdn.openkbs.com/pulse/pulse.min.js"></script>
131
+ </head>
132
+ <body>
133
+ <div id="status">Connecting...</div>
134
+ <div id="online">Online: 0</div>
135
+ <div id="messages"></div>
136
+
137
+ <input type="text" id="input" placeholder="Type message...">
138
+ <button onclick="send()">Send</button>
139
+
140
+ <script>
141
+ const pulse = new Pulse({
142
+ kbId: 'YOUR_KB_ID',
143
+ token: 'USER_TOKEN',
144
+ debug: true
145
+ });
146
+
147
+ const channel = pulse.channel('chat');
148
+
149
+ // Connection status
150
+ pulse.onStateChange((state) => {
151
+ document.getElementById('status').textContent = state;
152
+ });
153
+
154
+ // Presence
155
+ channel.presence.subscribe((members) => {
156
+ document.getElementById('online').textContent = 'Online: ' + members.length;
157
+ });
158
+ channel.presence.enter({ name: 'User' });
159
+
160
+ // Messages
161
+ channel.subscribe('message', (data) => {
162
+ const div = document.createElement('div');
163
+ div.textContent = data.name + ': ' + data.text;
164
+ document.getElementById('messages').appendChild(div);
165
+ });
166
+
167
+ function send() {
168
+ const input = document.getElementById('input');
169
+ channel.publish('message', {
170
+ name: 'User',
171
+ text: input.value
172
+ });
173
+ input.value = '';
174
+ }
175
+ </script>
176
+ </body>
177
+ </html>
178
+ ```
179
+
180
+ ## License
181
+
182
+ MIT
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "openkbs-pulse",
3
+ "version": "1.0.2",
4
+ "description": "Real-time WebSocket SDK for OpenKBS",
5
+ "main": "pulse.js",
6
+ "types": "pulse.d.ts",
7
+ "files": [
8
+ "pulse.js",
9
+ "pulse.d.ts"
10
+ ],
11
+ "scripts": {
12
+ "deploy:cdn": "./deploy.sh",
13
+ "publish:patch": "npm version patch && npm publish --access public",
14
+ "publish:minor": "npm version minor && npm publish --access public",
15
+ "publish:major": "npm version major && npm publish --access public"
16
+ },
17
+ "keywords": ["websocket", "realtime", "pubsub", "openkbs"],
18
+ "author": "OpenKBS",
19
+ "license": "MIT"
20
+ }
package/pulse.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ declare class Pulse {
2
+ constructor(options: PulseOptions);
3
+ connect(): void;
4
+ disconnect(): void;
5
+ channel(name: string): PulseChannel;
6
+ onStateChange(callback: (state: ConnectionState) => void): () => void;
7
+ readonly state: ConnectionState;
8
+ readonly isConnected: boolean;
9
+ }
10
+
11
+ interface PulseOptions {
12
+ kbId: string;
13
+ token: string;
14
+ channel?: string;
15
+ region?: 'us-east-1' | 'eu-central-1';
16
+ endpoint?: string;
17
+ debug?: boolean;
18
+ autoConnect?: boolean;
19
+ }
20
+
21
+ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'disconnecting' | 'failed';
22
+
23
+ declare class PulseChannel {
24
+ readonly name: string;
25
+ readonly presence: PulsePresence;
26
+ subscribe(callback: (data: any, msg: any) => void): () => void;
27
+ subscribe(event: string, callback: (data: any, msg: any) => void): () => void;
28
+ publish(event: string, data: any): boolean;
29
+ unsubscribe(): void;
30
+ }
31
+
32
+ declare class PulsePresence {
33
+ readonly members: PresenceMember[];
34
+ readonly count: number;
35
+ subscribe(callback: (members: PresenceMember[]) => void): () => void;
36
+ onEnter(callback: (member: PresenceMember) => void): () => void;
37
+ onLeave(callback: (member: PresenceMember) => void): () => void;
38
+ enter(data?: Record<string, any>): void;
39
+ leave(): void;
40
+ update(data: Record<string, any>): void;
41
+ }
42
+
43
+ interface PresenceMember {
44
+ connectionId: string;
45
+ userId?: string;
46
+ data?: Record<string, any>;
47
+ }
48
+
49
+ export = Pulse;
50
+ export as namespace Pulse;
package/pulse.js ADDED
@@ -0,0 +1,477 @@
1
+ /**
2
+ * OpenKBS Pulse v1.0.2 - Real-time WebSocket SDK
3
+ *
4
+ * Usage:
5
+ * const pulse = new Pulse({ kbId: 'your-kb-id', token: 'user-token' });
6
+ *
7
+ * const channel = pulse.channel('posts');
8
+ * channel.subscribe('new_post', (data) => console.log('New post:', data));
9
+ * channel.publish('new_post', { title: 'Hello' });
10
+ *
11
+ * channel.presence.subscribe((members) => console.log('Online:', members.length));
12
+ * channel.presence.enter({ name: 'John' });
13
+ */
14
+
15
+ (function(global) {
16
+ 'use strict';
17
+
18
+ const PULSE_ENDPOINTS = {
19
+ 'us-east-1': 'wss://pulse.vpc1.us',
20
+ 'eu-central-1': 'wss://pulse.vpc1.eu'
21
+ };
22
+
23
+ const DEFAULT_REGION = 'us-east-1';
24
+ const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000];
25
+
26
+ /**
27
+ * Main Pulse client
28
+ */
29
+ class Pulse {
30
+ constructor(options = {}) {
31
+ if (!options.kbId) {
32
+ throw new Error('Pulse: kbId is required');
33
+ }
34
+ if (!options.token) {
35
+ throw new Error('Pulse: token is required');
36
+ }
37
+
38
+ this.kbId = options.kbId;
39
+ this.token = options.token;
40
+ this._defaultChannel = options.channel || 'default';
41
+ this.region = options.region || DEFAULT_REGION;
42
+ this.endpoint = options.endpoint || PULSE_ENDPOINTS[this.region] || PULSE_ENDPOINTS[DEFAULT_REGION];
43
+ this.debug = options.debug || false;
44
+
45
+ this._channels = {};
46
+ this._ws = null;
47
+ this._reconnectAttempt = 0;
48
+ this._intentionalClose = false;
49
+ this._connectionState = 'disconnected';
50
+ this._stateListeners = [];
51
+ this._messageQueue = [];
52
+
53
+ // Auto-connect unless disabled
54
+ if (options.autoConnect !== false) {
55
+ this.connect();
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Connect to Pulse WebSocket server
61
+ */
62
+ connect() {
63
+ if (this._ws && this._ws.readyState === WebSocket.OPEN) {
64
+ this._log('Already connected');
65
+ return;
66
+ }
67
+
68
+ this._intentionalClose = false;
69
+ this._setConnectionState('connecting');
70
+
71
+ const url = `${this.endpoint}?kbId=${this.kbId}&token=${this.token}&channel=${this._defaultChannel}`;
72
+ this._log('Connecting to:', url);
73
+
74
+ try {
75
+ this._ws = new WebSocket(url);
76
+ this._setupWebSocket();
77
+ } catch (err) {
78
+ this._log('Connection error:', err);
79
+ this._setConnectionState('failed');
80
+ this._scheduleReconnect();
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Disconnect from server
86
+ */
87
+ disconnect() {
88
+ this._intentionalClose = true;
89
+ this._setConnectionState('disconnecting');
90
+
91
+ if (this._ws) {
92
+ this._ws.close(1000, 'Client disconnect');
93
+ this._ws = null;
94
+ }
95
+
96
+ this._setConnectionState('disconnected');
97
+ }
98
+
99
+ /**
100
+ * Get or create a channel
101
+ */
102
+ channel(name) {
103
+ if (!this._channels[name]) {
104
+ this._channels[name] = new PulseChannel(this, name);
105
+ }
106
+ return this._channels[name];
107
+ }
108
+
109
+ /**
110
+ * Subscribe to connection state changes
111
+ */
112
+ onStateChange(callback) {
113
+ this._stateListeners.push(callback);
114
+ // Immediately call with current state
115
+ callback(this._connectionState);
116
+ return () => {
117
+ this._stateListeners = this._stateListeners.filter(cb => cb !== callback);
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get current connection state
123
+ */
124
+ get state() {
125
+ return this._connectionState;
126
+ }
127
+
128
+ /**
129
+ * Check if connected
130
+ */
131
+ get isConnected() {
132
+ return this._connectionState === 'connected';
133
+ }
134
+
135
+ // Internal methods
136
+
137
+ _setupWebSocket() {
138
+ this._ws.onopen = () => {
139
+ this._log('Connected');
140
+ this._reconnectAttempt = 0;
141
+ this._setConnectionState('connected');
142
+
143
+ // Resubscribe to all channels
144
+ Object.values(this._channels).forEach(channel => {
145
+ channel._resubscribe();
146
+ });
147
+
148
+ // Flush message queue
149
+ this._flushMessageQueue();
150
+ };
151
+
152
+ this._ws.onclose = (e) => {
153
+ this._log('Disconnected:', e.code, e.reason);
154
+ this._setConnectionState('disconnected');
155
+
156
+ if (!this._intentionalClose) {
157
+ this._scheduleReconnect();
158
+ }
159
+ };
160
+
161
+ this._ws.onerror = (err) => {
162
+ this._log('WebSocket error:', err);
163
+ this._setConnectionState('failed');
164
+ };
165
+
166
+ this._ws.onmessage = (e) => {
167
+ this._handleMessage(e.data);
168
+ };
169
+ }
170
+
171
+ _handleMessage(data) {
172
+ this._log('Received:', data);
173
+
174
+ try {
175
+ const msg = JSON.parse(data);
176
+
177
+ // Handle server message format: {type: 'message', data: {...}}
178
+ if (msg.type === 'message' && msg.data) {
179
+ // Route to the connected channel
180
+ const channel = this._channels[this._defaultChannel];
181
+ if (channel) {
182
+ channel._handleMessage(msg.data);
183
+ }
184
+ }
185
+
186
+ // Handle system messages
187
+ if (msg.type === 'error') {
188
+ this._log('Server error:', msg.error);
189
+ }
190
+ } catch (err) {
191
+ this._log('Parse error:', err);
192
+ }
193
+ }
194
+
195
+ _send(data) {
196
+ if (this._ws && this._ws.readyState === WebSocket.OPEN) {
197
+ this._ws.send(JSON.stringify(data));
198
+ return true;
199
+ } else {
200
+ // Queue message for when connected
201
+ this._messageQueue.push(data);
202
+ return false;
203
+ }
204
+ }
205
+
206
+ _flushMessageQueue() {
207
+ while (this._messageQueue.length > 0) {
208
+ const msg = this._messageQueue.shift();
209
+ this._send(msg);
210
+ }
211
+ }
212
+
213
+ _scheduleReconnect() {
214
+ const delay = RECONNECT_DELAYS[Math.min(this._reconnectAttempt, RECONNECT_DELAYS.length - 1)];
215
+ this._reconnectAttempt++;
216
+
217
+ this._log(`Reconnecting in ${delay}ms (attempt ${this._reconnectAttempt})`);
218
+ this._setConnectionState('reconnecting');
219
+
220
+ setTimeout(() => {
221
+ if (!this._intentionalClose) {
222
+ this.connect();
223
+ }
224
+ }, delay);
225
+ }
226
+
227
+ _setConnectionState(state) {
228
+ if (this._connectionState !== state) {
229
+ this._connectionState = state;
230
+ this._stateListeners.forEach(cb => cb(state));
231
+ }
232
+ }
233
+
234
+ _log(...args) {
235
+ if (this.debug) {
236
+ console.log('[Pulse]', ...args);
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Channel for pub/sub messaging
243
+ */
244
+ class PulseChannel {
245
+ constructor(pulse, name) {
246
+ this._pulse = pulse;
247
+ this.name = name;
248
+ this._subscribers = {};
249
+ this._allSubscribers = [];
250
+ this._subscribed = false;
251
+
252
+ // Presence sub-object
253
+ this.presence = new PulsePresence(this);
254
+ }
255
+
256
+ /**
257
+ * Subscribe to a specific event type
258
+ */
259
+ subscribe(event, callback) {
260
+ if (typeof event === 'function') {
261
+ // Subscribe to all events
262
+ callback = event;
263
+ this._allSubscribers.push(callback);
264
+ } else {
265
+ // Subscribe to specific event
266
+ if (!this._subscribers[event]) {
267
+ this._subscribers[event] = [];
268
+ }
269
+ this._subscribers[event].push(callback);
270
+ }
271
+
272
+ // Send subscribe message to server
273
+ if (!this._subscribed) {
274
+ this._pulse._send({
275
+ action: 'subscribe',
276
+ channel: this.name
277
+ });
278
+ this._subscribed = true;
279
+ }
280
+
281
+ // Return unsubscribe function
282
+ return () => {
283
+ if (typeof event === 'function') {
284
+ this._allSubscribers = this._allSubscribers.filter(cb => cb !== callback);
285
+ } else {
286
+ this._subscribers[event] = (this._subscribers[event] || []).filter(cb => cb !== callback);
287
+ }
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Publish a message to the channel
293
+ */
294
+ publish(event, data) {
295
+ return this._pulse._send({
296
+ action: 'publish',
297
+ channel: this.name,
298
+ event: event,
299
+ data: data
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Unsubscribe from channel
305
+ */
306
+ unsubscribe() {
307
+ this._subscribers = {};
308
+ this._allSubscribers = [];
309
+ this._subscribed = false;
310
+
311
+ this._pulse._send({
312
+ action: 'unsubscribe',
313
+ channel: this.name
314
+ });
315
+ }
316
+
317
+ // Internal methods
318
+
319
+ _handleMessage(msg) {
320
+ const event = msg.event || msg.type;
321
+ // Use msg itself as data if no data field (server sends {type: 'new_post', post: {...}})
322
+ const data = msg.data !== undefined ? msg.data : msg;
323
+
324
+ // Call specific event subscribers
325
+ if (event && this._subscribers[event]) {
326
+ this._subscribers[event].forEach(cb => cb(data, msg));
327
+ }
328
+
329
+ // Call all-event subscribers
330
+ this._allSubscribers.forEach(cb => cb(data, msg));
331
+
332
+ // Handle presence events
333
+ if (msg.type === 'presence') {
334
+ this.presence._handleMessage(msg);
335
+ }
336
+ }
337
+
338
+ _resubscribe() {
339
+ if (this._subscribed) {
340
+ this._pulse._send({
341
+ action: 'subscribe',
342
+ channel: this.name
343
+ });
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Presence tracking for a channel
350
+ */
351
+ class PulsePresence {
352
+ constructor(channel) {
353
+ this._channel = channel;
354
+ this._members = [];
355
+ this._subscribers = [];
356
+ this._enterSubscribers = [];
357
+ this._leaveSubscribers = [];
358
+ this._myData = null;
359
+ }
360
+
361
+ /**
362
+ * Subscribe to presence changes
363
+ */
364
+ subscribe(callback) {
365
+ this._subscribers.push(callback);
366
+
367
+ // Return current members immediately
368
+ if (this._members.length > 0) {
369
+ callback(this._members);
370
+ }
371
+
372
+ return () => {
373
+ this._subscribers = this._subscribers.filter(cb => cb !== callback);
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Subscribe to member enter events
379
+ */
380
+ onEnter(callback) {
381
+ this._enterSubscribers.push(callback);
382
+ return () => {
383
+ this._enterSubscribers = this._enterSubscribers.filter(cb => cb !== callback);
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Subscribe to member leave events
389
+ */
390
+ onLeave(callback) {
391
+ this._leaveSubscribers.push(callback);
392
+ return () => {
393
+ this._leaveSubscribers = this._leaveSubscribers.filter(cb => cb !== callback);
394
+ };
395
+ }
396
+
397
+ /**
398
+ * Enter presence with optional data
399
+ */
400
+ enter(data = {}) {
401
+ this._myData = data;
402
+ this._channel._pulse._send({
403
+ action: 'presence_enter',
404
+ channel: this._channel.name,
405
+ data: data
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Leave presence
411
+ */
412
+ leave() {
413
+ this._myData = null;
414
+ this._channel._pulse._send({
415
+ action: 'presence_leave',
416
+ channel: this._channel.name
417
+ });
418
+ }
419
+
420
+ /**
421
+ * Update presence data
422
+ */
423
+ update(data) {
424
+ this._myData = data;
425
+ this._channel._pulse._send({
426
+ action: 'presence_update',
427
+ channel: this._channel.name,
428
+ data: data
429
+ });
430
+ }
431
+
432
+ /**
433
+ * Get current members
434
+ */
435
+ get members() {
436
+ return this._members;
437
+ }
438
+
439
+ /**
440
+ * Get member count
441
+ */
442
+ get count() {
443
+ return this._members.length;
444
+ }
445
+
446
+ // Internal methods
447
+
448
+ _handleMessage(msg) {
449
+ if (msg.action === 'sync') {
450
+ this._members = msg.members || [];
451
+ this._notifySubscribers();
452
+ } else if (msg.action === 'enter') {
453
+ this._members.push(msg.member);
454
+ this._notifySubscribers();
455
+ this._enterSubscribers.forEach(cb => cb(msg.member));
456
+ } else if (msg.action === 'leave') {
457
+ this._members = this._members.filter(m => m.connectionId !== msg.member.connectionId);
458
+ this._notifySubscribers();
459
+ this._leaveSubscribers.forEach(cb => cb(msg.member));
460
+ }
461
+ }
462
+
463
+ _notifySubscribers() {
464
+ this._subscribers.forEach(cb => cb(this._members));
465
+ }
466
+ }
467
+
468
+ // Export for different module systems
469
+ if (typeof module !== 'undefined' && module.exports) {
470
+ module.exports = Pulse;
471
+ } else if (typeof define === 'function' && define.amd) {
472
+ define([], function() { return Pulse; });
473
+ } else {
474
+ global.Pulse = Pulse;
475
+ }
476
+
477
+ })(typeof window !== 'undefined' ? window : this);