@ceylonitsolutions/pushstream-js 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -107
- package/dist/pushstream.min.js +258 -225
- package/package.json +30 -25
- package/pushstream.js +258 -225
package/README.md
CHANGED
|
@@ -1,149 +1,104 @@
|
|
|
1
|
-
# PushStream JavaScript SDK
|
|
1
|
+
# PushStream JavaScript SDK (`sdks` Edition)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JavaScript SDK for PushStream realtime connections and server-side event publishing.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Source
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
7
|
+
- File: `sdks/pushstream-js/pushstream-sdk.js`
|
|
8
|
+
- Class: `PushStream`
|
|
10
9
|
|
|
11
|
-
##
|
|
10
|
+
## Compatibility
|
|
11
|
+
|
|
12
|
+
This SDK matches current PushStream protocol:
|
|
13
|
+
|
|
14
|
+
- WebSocket connect with `app_id` + `app_key`
|
|
15
|
+
- Channel subscribe via `pusher:subscribe`
|
|
16
|
+
- Event publish endpoint: `POST /api/apps/{app_id}/events`
|
|
17
|
+
- Publish auth header: `Authorization: {app_id}:{signature}`
|
|
18
|
+
- Publish `data` field is sent as JSON string
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
This `sdks` version is distributed as source file.
|
|
12
23
|
|
|
13
24
|
### Browser
|
|
14
25
|
|
|
15
26
|
```html
|
|
16
|
-
<script src="pushstream.js"></script>
|
|
17
|
-
<script>
|
|
18
|
-
const client = new PushStream('your-app-key');
|
|
19
|
-
|
|
20
|
-
client.connect().then(socketId => {
|
|
21
|
-
console.log('Connected:', socketId);
|
|
22
|
-
|
|
23
|
-
const channel = client.subscribe('my-channel');
|
|
24
|
-
channel.bind('my-event', (data) => {
|
|
25
|
-
console.log('Received:', data);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
</script>
|
|
27
|
+
<script src="/path/to/pushstream-sdk.js"></script>
|
|
29
28
|
```
|
|
30
29
|
|
|
31
30
|
### Node.js
|
|
32
31
|
|
|
33
|
-
```
|
|
34
|
-
const PushStream = require('
|
|
35
|
-
|
|
36
|
-
const client = new PushStream('your-app-key');
|
|
37
|
-
|
|
38
|
-
client.connect().then(socketId => {
|
|
39
|
-
console.log('Connected:', socketId);
|
|
40
|
-
|
|
41
|
-
const channel = client.subscribe('my-channel');
|
|
42
|
-
channel.bind('my-event', (data) => {
|
|
43
|
-
console.log('Received:', data);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
32
|
+
```js
|
|
33
|
+
const PushStream = require('./pushstream-sdk');
|
|
46
34
|
```
|
|
47
35
|
|
|
48
|
-
##
|
|
36
|
+
## Quick Start
|
|
49
37
|
|
|
50
|
-
```
|
|
51
|
-
const client = new PushStream('
|
|
38
|
+
```js
|
|
39
|
+
const client = new PushStream('APP_KEY', {
|
|
40
|
+
appId: 'APP_ID',
|
|
52
41
|
wsUrl: 'wss://ws.pushstream.ceylonitsolutions.online',
|
|
53
42
|
apiUrl: 'https://api.pushstream.ceylonitsolutions.online'
|
|
54
43
|
});
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## API Reference
|
|
58
44
|
|
|
59
|
-
|
|
45
|
+
await client.connect();
|
|
60
46
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
client.connect()
|
|
66
|
-
.then(socketId => console.log('Connected'))
|
|
67
|
-
.catch(error => console.error('Failed to connect'));
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
#### `subscribe(channelName)`
|
|
71
|
-
Subscribe to a channel.
|
|
72
|
-
|
|
73
|
-
```javascript
|
|
74
|
-
const channel = client.subscribe('my-channel');
|
|
47
|
+
const channel = client.subscribe('public-updates');
|
|
48
|
+
channel.bind('announcement', (payload) => {
|
|
49
|
+
console.log(payload);
|
|
50
|
+
});
|
|
75
51
|
```
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
Unsubscribe from a channel.
|
|
79
|
-
|
|
80
|
-
```javascript
|
|
81
|
-
client.unsubscribe('my-channel');
|
|
82
|
-
```
|
|
53
|
+
## Private/Presence Channel Notes
|
|
83
54
|
|
|
84
|
-
|
|
85
|
-
Close the connection.
|
|
55
|
+
This SDK sends raw subscribe payload. For protected channels (`private-` / `presence-`), you must provide valid channel auth according to server rules.
|
|
86
56
|
|
|
87
|
-
|
|
88
|
-
client.disconnect();
|
|
89
|
-
```
|
|
57
|
+
Recommended approach: use canonical SDK under `sdk/` for built-in auth flow support.
|
|
90
58
|
|
|
91
|
-
|
|
92
|
-
Publish events via REST API.
|
|
59
|
+
## Publish Events
|
|
93
60
|
|
|
94
|
-
```
|
|
61
|
+
```js
|
|
95
62
|
await client.publish(
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
{
|
|
63
|
+
'APP_ID',
|
|
64
|
+
'APP_SECRET',
|
|
65
|
+
'orders',
|
|
66
|
+
'order.created',
|
|
67
|
+
{ id: 101 }
|
|
101
68
|
);
|
|
102
69
|
```
|
|
103
70
|
|
|
104
|
-
|
|
71
|
+
## Core API
|
|
105
72
|
|
|
106
|
-
|
|
107
|
-
Listen for events on the channel.
|
|
73
|
+
### `new PushStream(appKey, options)`
|
|
108
74
|
|
|
109
|
-
|
|
110
|
-
channel.bind('my-event', (data) => {
|
|
111
|
-
console.log(data);
|
|
112
|
-
});
|
|
113
|
-
```
|
|
75
|
+
Options:
|
|
114
76
|
|
|
115
|
-
|
|
116
|
-
|
|
77
|
+
- `appId` (required for WebSocket connect)
|
|
78
|
+
- `wsUrl` (default `wss://ws.pushstream.ceylonitsolutions.online`)
|
|
79
|
+
- `apiUrl` (default `https://api.pushstream.ceylonitsolutions.online`)
|
|
117
80
|
|
|
118
|
-
|
|
119
|
-
channel.unbind('my-event', callback);
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
#### `unsubscribe()`
|
|
123
|
-
Unsubscribe from the channel.
|
|
124
|
-
|
|
125
|
-
```javascript
|
|
126
|
-
channel.unsubscribe();
|
|
127
|
-
```
|
|
81
|
+
### Methods
|
|
128
82
|
|
|
129
|
-
|
|
83
|
+
- `connect()`
|
|
84
|
+
- `subscribe(channelName)`
|
|
85
|
+
- `unsubscribe(channelName)`
|
|
86
|
+
- `disconnect()`
|
|
87
|
+
- `publish(appId, appSecret, channel, event, data)`
|
|
130
88
|
|
|
131
|
-
|
|
132
|
-
- Automatic reconnection with exponential backoff
|
|
133
|
-
- Channel subscriptions
|
|
134
|
-
- Event binding
|
|
135
|
-
- REST API publishing
|
|
136
|
-
- Browser and Node.js support
|
|
89
|
+
## Reconnect Behavior
|
|
137
90
|
|
|
138
|
-
|
|
91
|
+
- Automatic exponential backoff reconnect
|
|
92
|
+
- Maximum attempts: 5
|
|
139
93
|
|
|
140
|
-
|
|
141
|
-
- Modern browser with WebSocket support
|
|
94
|
+
## Security Guidance
|
|
142
95
|
|
|
143
|
-
|
|
96
|
+
- Keep `APP_SECRET` in trusted backend environments only.
|
|
97
|
+
- Use HTTPS/WSS in production.
|
|
98
|
+
- Rotate secrets if compromised.
|
|
144
99
|
|
|
145
|
-
|
|
100
|
+
## Migration Recommendation
|
|
146
101
|
|
|
147
|
-
|
|
102
|
+
For active maintenance and better channel auth support, migrate to:
|
|
148
103
|
|
|
149
|
-
|
|
104
|
+
- `sdk/pushstream-js/README.md`
|
package/dist/pushstream.min.js
CHANGED
|
@@ -1,225 +1,258 @@
|
|
|
1
|
-
class PushStream {
|
|
2
|
-
constructor(appKey, options = {}) {
|
|
3
|
-
this.appKey = appKey;
|
|
4
|
-
this.
|
|
5
|
-
this.
|
|
6
|
-
this.
|
|
7
|
-
this.
|
|
8
|
-
this.
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
1
|
+
class PushStream {
|
|
2
|
+
constructor(appKey, options = {}) {
|
|
3
|
+
this.appKey = appKey;
|
|
4
|
+
this.appId = options.appId;
|
|
5
|
+
this.wsUrl = options.wsUrl || 'wss://ws.pushstream.ceylonitsolutions.online';
|
|
6
|
+
this.apiUrl = options.apiUrl || 'https://api.pushstream.ceylonitsolutions.online';
|
|
7
|
+
this.ws = null;
|
|
8
|
+
this.socketId = null;
|
|
9
|
+
this.channels = new Map();
|
|
10
|
+
this.reconnectAttempts = 0;
|
|
11
|
+
this.maxReconnectAttempts = 5;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (!this.appId || !this.appKey) {
|
|
17
|
+
reject(new Error('appId and appKey are required'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
this.ws = new WebSocket(`${this.wsUrl}?app_id=${this.appId}&app_key=${this.appKey}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
reject(error);
|
|
25
|
+
this.attemptReconnect();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timeout = setTimeout(() => {
|
|
30
|
+
if (!this.socketId) {
|
|
31
|
+
reject(new Error('Connection timeout'));
|
|
32
|
+
this.attemptReconnect();
|
|
33
|
+
}
|
|
34
|
+
}, 10000);
|
|
35
|
+
|
|
36
|
+
this.ws.onopen = () => {
|
|
37
|
+
console.log('[PushStream] Connected');
|
|
38
|
+
console.log('[PushStream] Waiting for connection_established event...');
|
|
39
|
+
this.reconnectAttempts = 0;
|
|
40
|
+
this.startPingInterval();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.ws.onmessage = (event) => {
|
|
44
|
+
console.log('[PushStream] Received message:', event.data);
|
|
45
|
+
const message = JSON.parse(event.data);
|
|
46
|
+
|
|
47
|
+
if (message.event === 'pusher:connection_established') {
|
|
48
|
+
const data = JSON.parse(message.data);
|
|
49
|
+
this.socketId = data.socket_id;
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
resolve(this.socketId);
|
|
52
|
+
} else if (message.event === 'pusher:error') {
|
|
53
|
+
console.error('[PushStream] Error:', message.data);
|
|
54
|
+
} else {
|
|
55
|
+
this.handleMessage(message);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.ws.onclose = (event) => {
|
|
60
|
+
console.log('[PushStream] Disconnected', { code: event.code, reason: event.reason });
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
this.stopPingInterval();
|
|
63
|
+
this.socketId = null;
|
|
64
|
+
this.attemptReconnect();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this.ws.onerror = (error) => {
|
|
68
|
+
console.error('[PushStream] WebSocket error:', error);
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
if (!this.socketId) reject(error);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
attemptReconnect() {
|
|
76
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
77
|
+
console.error('[PushStream] Max reconnection attempts reached');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
82
|
+
this.reconnectAttempts++;
|
|
83
|
+
|
|
84
|
+
console.log(`[PushStream] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
85
|
+
setTimeout(() => this.connect(), delay);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
subscribe(channelName, callbacks = {}) {
|
|
89
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
90
|
+
throw new Error('Not connected');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const channel = new Channel(channelName, this, callbacks);
|
|
94
|
+
this.channels.set(channelName, channel);
|
|
95
|
+
|
|
96
|
+
this.send({
|
|
97
|
+
event: 'pusher:subscribe',
|
|
98
|
+
data: { channel: channelName }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return channel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
unsubscribe(channelName) {
|
|
105
|
+
this.send({
|
|
106
|
+
event: 'pusher:unsubscribe',
|
|
107
|
+
data: { channel: channelName }
|
|
108
|
+
});
|
|
109
|
+
this.channels.delete(channelName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
send(data) {
|
|
113
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
114
|
+
this.ws.send(JSON.stringify(data));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
handleMessage(message) {
|
|
119
|
+
console.log('[PushStream] Handling message:', message);
|
|
120
|
+
const channel = this.channels.get(message.channel);
|
|
121
|
+
if (channel) {
|
|
122
|
+
// Parse data if it's a string
|
|
123
|
+
const eventData = typeof message.data === 'string' ? JSON.parse(message.data) : message.data;
|
|
124
|
+
console.log('[PushStream] Parsed data:', eventData);
|
|
125
|
+
channel.handleEvent(message.event, eventData);
|
|
126
|
+
} else {
|
|
127
|
+
console.warn('[PushStream] No channel found for:', message.channel);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
startPingInterval() {
|
|
132
|
+
this.stopPingInterval();
|
|
133
|
+
this.pingInterval = setInterval(() => {
|
|
134
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
135
|
+
this.send({ event: 'pusher:ping', data: {} });
|
|
136
|
+
}
|
|
137
|
+
}, 30000);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
stopPingInterval() {
|
|
141
|
+
if (this.pingInterval) {
|
|
142
|
+
clearInterval(this.pingInterval);
|
|
143
|
+
this.pingInterval = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
disconnect() {
|
|
148
|
+
this.stopPingInterval();
|
|
149
|
+
if (this.ws) {
|
|
150
|
+
this.ws.close();
|
|
151
|
+
this.ws = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// REST API methods
|
|
156
|
+
async publish(appId, appSecret, channel, event, data) {
|
|
157
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
158
|
+
const body = JSON.stringify({ name: event, channel, data });
|
|
159
|
+
const path = `/api/apps/${appId}/events`;
|
|
160
|
+
const queryString = `auth_timestamp=${timestamp}`;
|
|
161
|
+
const stringToSign = `POST\n${path}\n${queryString}\n${body}`;
|
|
162
|
+
|
|
163
|
+
const signature = await this.hmacSha256(stringToSign, appSecret);
|
|
164
|
+
const authHeader = `${appId}:${signature}`;
|
|
165
|
+
|
|
166
|
+
const response = await fetch(`${this.apiUrl}${path}?${queryString}`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Authorization': authHeader,
|
|
170
|
+
'Content-Type': 'application/json'
|
|
171
|
+
},
|
|
172
|
+
body
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return response.json();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async hmacSha256(message, secret) {
|
|
183
|
+
// Node.js
|
|
184
|
+
if (typeof window === 'undefined') {
|
|
185
|
+
const crypto = require('crypto');
|
|
186
|
+
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Browser
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
const keyData = encoder.encode(secret);
|
|
192
|
+
const messageData = encoder.encode(message);
|
|
193
|
+
|
|
194
|
+
const key = await crypto.subtle.importKey(
|
|
195
|
+
'raw',
|
|
196
|
+
keyData,
|
|
197
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
198
|
+
false,
|
|
199
|
+
['sign']
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const signature = await crypto.subtle.sign('HMAC', key, messageData);
|
|
203
|
+
return Array.from(new Uint8Array(signature))
|
|
204
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
205
|
+
.join('');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
class Channel {
|
|
210
|
+
constructor(name, client, callbacks = {}) {
|
|
211
|
+
this.name = name;
|
|
212
|
+
this.client = client;
|
|
213
|
+
this.callbacks = callbacks;
|
|
214
|
+
this.eventHandlers = new Map();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
bind(event, callback) {
|
|
218
|
+
if (!this.eventHandlers.has(event)) {
|
|
219
|
+
this.eventHandlers.set(event, []);
|
|
220
|
+
}
|
|
221
|
+
this.eventHandlers.get(event).push(callback);
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
unbind(event, callback) {
|
|
226
|
+
if (!this.eventHandlers.has(event)) return;
|
|
227
|
+
|
|
228
|
+
if (callback) {
|
|
229
|
+
const handlers = this.eventHandlers.get(event);
|
|
230
|
+
const index = handlers.indexOf(callback);
|
|
231
|
+
if (index > -1) handlers.splice(index, 1);
|
|
232
|
+
} else {
|
|
233
|
+
this.eventHandlers.delete(event);
|
|
234
|
+
}
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
handleEvent(event, data) {
|
|
239
|
+
const handlers = this.eventHandlers.get(event);
|
|
240
|
+
if (handlers) {
|
|
241
|
+
handlers.forEach(handler => handler(data));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
unsubscribe() {
|
|
246
|
+
this.client.unsubscribe(this.name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Node.js support
|
|
251
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
252
|
+
module.exports = PushStream;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Browser support
|
|
256
|
+
if (typeof window !== 'undefined') {
|
|
257
|
+
window.PushStream = PushStream;
|
|
258
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ceylonitsolutions/pushstream-js",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "PushStream JavaScript SDK for real-time messaging",
|
|
5
|
-
"main": "pushstream.js",
|
|
6
|
-
"browser": "dist/pushstream.min.js",
|
|
7
|
-
"files": [
|
|
8
|
-
"pushstream.js",
|
|
9
|
-
"dist/pushstream.min.js"
|
|
10
|
-
],
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
-
},
|
|
14
|
-
"keywords": [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ceylonitsolutions/pushstream-js",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "PushStream JavaScript SDK for real-time messaging",
|
|
5
|
+
"main": "pushstream.js",
|
|
6
|
+
"browser": "dist/pushstream.min.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"pushstream.js",
|
|
9
|
+
"dist/pushstream.min.js"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"websocket",
|
|
16
|
+
"realtime",
|
|
17
|
+
"pusher",
|
|
18
|
+
"messaging"
|
|
19
|
+
],
|
|
20
|
+
"author": "Ceylon IT Solutions",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/bkkalana/pushstream-js.git"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=14.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/pushstream.js
CHANGED
|
@@ -1,225 +1,258 @@
|
|
|
1
|
-
class PushStream {
|
|
2
|
-
constructor(appKey, options = {}) {
|
|
3
|
-
this.appKey = appKey;
|
|
4
|
-
this.
|
|
5
|
-
this.
|
|
6
|
-
this.
|
|
7
|
-
this.
|
|
8
|
-
this.
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
1
|
+
class PushStream {
|
|
2
|
+
constructor(appKey, options = {}) {
|
|
3
|
+
this.appKey = appKey;
|
|
4
|
+
this.appId = options.appId;
|
|
5
|
+
this.wsUrl = options.wsUrl || 'wss://ws.pushstream.ceylonitsolutions.online';
|
|
6
|
+
this.apiUrl = options.apiUrl || 'https://api.pushstream.ceylonitsolutions.online';
|
|
7
|
+
this.ws = null;
|
|
8
|
+
this.socketId = null;
|
|
9
|
+
this.channels = new Map();
|
|
10
|
+
this.reconnectAttempts = 0;
|
|
11
|
+
this.maxReconnectAttempts = 5;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (!this.appId || !this.appKey) {
|
|
17
|
+
reject(new Error('appId and appKey are required'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
this.ws = new WebSocket(`${this.wsUrl}?app_id=${this.appId}&app_key=${this.appKey}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
reject(error);
|
|
25
|
+
this.attemptReconnect();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timeout = setTimeout(() => {
|
|
30
|
+
if (!this.socketId) {
|
|
31
|
+
reject(new Error('Connection timeout'));
|
|
32
|
+
this.attemptReconnect();
|
|
33
|
+
}
|
|
34
|
+
}, 10000);
|
|
35
|
+
|
|
36
|
+
this.ws.onopen = () => {
|
|
37
|
+
console.log('[PushStream] Connected');
|
|
38
|
+
console.log('[PushStream] Waiting for connection_established event...');
|
|
39
|
+
this.reconnectAttempts = 0;
|
|
40
|
+
this.startPingInterval();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.ws.onmessage = (event) => {
|
|
44
|
+
console.log('[PushStream] Received message:', event.data);
|
|
45
|
+
const message = JSON.parse(event.data);
|
|
46
|
+
|
|
47
|
+
if (message.event === 'pusher:connection_established') {
|
|
48
|
+
const data = JSON.parse(message.data);
|
|
49
|
+
this.socketId = data.socket_id;
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
resolve(this.socketId);
|
|
52
|
+
} else if (message.event === 'pusher:error') {
|
|
53
|
+
console.error('[PushStream] Error:', message.data);
|
|
54
|
+
} else {
|
|
55
|
+
this.handleMessage(message);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.ws.onclose = (event) => {
|
|
60
|
+
console.log('[PushStream] Disconnected', { code: event.code, reason: event.reason });
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
this.stopPingInterval();
|
|
63
|
+
this.socketId = null;
|
|
64
|
+
this.attemptReconnect();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this.ws.onerror = (error) => {
|
|
68
|
+
console.error('[PushStream] WebSocket error:', error);
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
if (!this.socketId) reject(error);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
attemptReconnect() {
|
|
76
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
77
|
+
console.error('[PushStream] Max reconnection attempts reached');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
82
|
+
this.reconnectAttempts++;
|
|
83
|
+
|
|
84
|
+
console.log(`[PushStream] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
85
|
+
setTimeout(() => this.connect(), delay);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
subscribe(channelName, callbacks = {}) {
|
|
89
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
90
|
+
throw new Error('Not connected');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const channel = new Channel(channelName, this, callbacks);
|
|
94
|
+
this.channels.set(channelName, channel);
|
|
95
|
+
|
|
96
|
+
this.send({
|
|
97
|
+
event: 'pusher:subscribe',
|
|
98
|
+
data: { channel: channelName }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return channel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
unsubscribe(channelName) {
|
|
105
|
+
this.send({
|
|
106
|
+
event: 'pusher:unsubscribe',
|
|
107
|
+
data: { channel: channelName }
|
|
108
|
+
});
|
|
109
|
+
this.channels.delete(channelName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
send(data) {
|
|
113
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
114
|
+
this.ws.send(JSON.stringify(data));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
handleMessage(message) {
|
|
119
|
+
console.log('[PushStream] Handling message:', message);
|
|
120
|
+
const channel = this.channels.get(message.channel);
|
|
121
|
+
if (channel) {
|
|
122
|
+
// Parse data if it's a string
|
|
123
|
+
const eventData = typeof message.data === 'string' ? JSON.parse(message.data) : message.data;
|
|
124
|
+
console.log('[PushStream] Parsed data:', eventData);
|
|
125
|
+
channel.handleEvent(message.event, eventData);
|
|
126
|
+
} else {
|
|
127
|
+
console.warn('[PushStream] No channel found for:', message.channel);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
startPingInterval() {
|
|
132
|
+
this.stopPingInterval();
|
|
133
|
+
this.pingInterval = setInterval(() => {
|
|
134
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
135
|
+
this.send({ event: 'pusher:ping', data: {} });
|
|
136
|
+
}
|
|
137
|
+
}, 30000);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
stopPingInterval() {
|
|
141
|
+
if (this.pingInterval) {
|
|
142
|
+
clearInterval(this.pingInterval);
|
|
143
|
+
this.pingInterval = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
disconnect() {
|
|
148
|
+
this.stopPingInterval();
|
|
149
|
+
if (this.ws) {
|
|
150
|
+
this.ws.close();
|
|
151
|
+
this.ws = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// REST API methods
|
|
156
|
+
async publish(appId, appSecret, channel, event, data) {
|
|
157
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
158
|
+
const body = JSON.stringify({ name: event, channel, data });
|
|
159
|
+
const path = `/api/apps/${appId}/events`;
|
|
160
|
+
const queryString = `auth_timestamp=${timestamp}`;
|
|
161
|
+
const stringToSign = `POST\n${path}\n${queryString}\n${body}`;
|
|
162
|
+
|
|
163
|
+
const signature = await this.hmacSha256(stringToSign, appSecret);
|
|
164
|
+
const authHeader = `${appId}:${signature}`;
|
|
165
|
+
|
|
166
|
+
const response = await fetch(`${this.apiUrl}${path}?${queryString}`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Authorization': authHeader,
|
|
170
|
+
'Content-Type': 'application/json'
|
|
171
|
+
},
|
|
172
|
+
body
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return response.json();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async hmacSha256(message, secret) {
|
|
183
|
+
// Node.js
|
|
184
|
+
if (typeof window === 'undefined') {
|
|
185
|
+
const crypto = require('crypto');
|
|
186
|
+
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Browser
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
const keyData = encoder.encode(secret);
|
|
192
|
+
const messageData = encoder.encode(message);
|
|
193
|
+
|
|
194
|
+
const key = await crypto.subtle.importKey(
|
|
195
|
+
'raw',
|
|
196
|
+
keyData,
|
|
197
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
198
|
+
false,
|
|
199
|
+
['sign']
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const signature = await crypto.subtle.sign('HMAC', key, messageData);
|
|
203
|
+
return Array.from(new Uint8Array(signature))
|
|
204
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
205
|
+
.join('');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
class Channel {
|
|
210
|
+
constructor(name, client, callbacks = {}) {
|
|
211
|
+
this.name = name;
|
|
212
|
+
this.client = client;
|
|
213
|
+
this.callbacks = callbacks;
|
|
214
|
+
this.eventHandlers = new Map();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
bind(event, callback) {
|
|
218
|
+
if (!this.eventHandlers.has(event)) {
|
|
219
|
+
this.eventHandlers.set(event, []);
|
|
220
|
+
}
|
|
221
|
+
this.eventHandlers.get(event).push(callback);
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
unbind(event, callback) {
|
|
226
|
+
if (!this.eventHandlers.has(event)) return;
|
|
227
|
+
|
|
228
|
+
if (callback) {
|
|
229
|
+
const handlers = this.eventHandlers.get(event);
|
|
230
|
+
const index = handlers.indexOf(callback);
|
|
231
|
+
if (index > -1) handlers.splice(index, 1);
|
|
232
|
+
} else {
|
|
233
|
+
this.eventHandlers.delete(event);
|
|
234
|
+
}
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
handleEvent(event, data) {
|
|
239
|
+
const handlers = this.eventHandlers.get(event);
|
|
240
|
+
if (handlers) {
|
|
241
|
+
handlers.forEach(handler => handler(data));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
unsubscribe() {
|
|
246
|
+
this.client.unsubscribe(this.name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Node.js support
|
|
251
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
252
|
+
module.exports = PushStream;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Browser support
|
|
256
|
+
if (typeof window !== 'undefined') {
|
|
257
|
+
window.PushStream = PushStream;
|
|
258
|
+
}
|