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.
- package/README.md +182 -0
- package/package.json +20 -0
- package/pulse.d.ts +50 -0
- 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);
|