@ceylonitsolutions/pushstream-js 1.0.2 → 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 CHANGED
@@ -1,149 +1,104 @@
1
- # PushStream JavaScript SDK
1
+ # PushStream JavaScript SDK (`sdks` Edition)
2
2
 
3
- Real-time messaging SDK for JavaScript/Node.js applications.
3
+ JavaScript SDK for PushStream realtime connections and server-side event publishing.
4
4
 
5
- ## Installation
5
+ ## Source
6
6
 
7
- ```bash
8
- npm install @ceylonitsolutions/pushstream-js
9
- ```
7
+ - File: `sdks/pushstream-js/pushstream-sdk.js`
8
+ - Class: `PushStream`
10
9
 
11
- ## Quick Start
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
- ```javascript
34
- const PushStream = require('@ceylonitsolutions/pushstream-js');
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
- ## Configuration
36
+ ## Quick Start
49
37
 
50
- ```javascript
51
- const client = new PushStream('your-app-key', {
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
- ### Client Methods
45
+ await client.connect();
60
46
 
61
- #### `connect()`
62
- Establishes WebSocket connection.
63
-
64
- ```javascript
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
- #### `unsubscribe(channelName)`
78
- Unsubscribe from a channel.
79
-
80
- ```javascript
81
- client.unsubscribe('my-channel');
82
- ```
53
+ ## Private/Presence Channel Notes
83
54
 
84
- #### `disconnect()`
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
- ```javascript
88
- client.disconnect();
89
- ```
57
+ Recommended approach: use canonical SDK under `sdk/` for built-in auth flow support.
90
58
 
91
- #### `publish(appId, appSecret, channel, event, data)`
92
- Publish events via REST API.
59
+ ## Publish Events
93
60
 
94
- ```javascript
61
+ ```js
95
62
  await client.publish(
96
- 'app-id',
97
- 'app-secret',
98
- 'my-channel',
99
- 'my-event',
100
- { message: 'Hello' }
63
+ 'APP_ID',
64
+ 'APP_SECRET',
65
+ 'orders',
66
+ 'order.created',
67
+ { id: 101 }
101
68
  );
102
69
  ```
103
70
 
104
- ### Channel Methods
71
+ ## Core API
105
72
 
106
- #### `bind(event, callback)`
107
- Listen for events on the channel.
73
+ ### `new PushStream(appKey, options)`
108
74
 
109
- ```javascript
110
- channel.bind('my-event', (data) => {
111
- console.log(data);
112
- });
113
- ```
75
+ Options:
114
76
 
115
- #### `unbind(event, callback)`
116
- Remove event listener.
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
- ```javascript
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
- ## Features
83
+ - `connect()`
84
+ - `subscribe(channelName)`
85
+ - `unsubscribe(channelName)`
86
+ - `disconnect()`
87
+ - `publish(appId, appSecret, channel, event, data)`
130
88
 
131
- - WebSocket real-time messaging
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
- ## Requirements
91
+ - Automatic exponential backoff reconnect
92
+ - Maximum attempts: 5
139
93
 
140
- - Node.js >= 14.0.0 (for Node.js usage)
141
- - Modern browser with WebSocket support
94
+ ## Security Guidance
142
95
 
143
- ## License
96
+ - Keep `APP_SECRET` in trusted backend environments only.
97
+ - Use HTTPS/WSS in production.
98
+ - Rotate secrets if compromised.
144
99
 
145
- MIT
100
+ ## Migration Recommendation
146
101
 
147
- ## Author
102
+ For active maintenance and better channel auth support, migrate to:
148
103
 
149
- Ceylon IT Solutions
104
+ - `sdk/pushstream-js/README.md`
@@ -1,225 +1,258 @@
1
- class PushStream {
2
- constructor(appKey, options = {}) {
3
- this.appKey = appKey;
4
- this.wsUrl = options.wsUrl || 'wss://ws.pushstream.ceylonitsolutions.online';
5
- this.apiUrl = options.apiUrl || 'https://api.pushstream.ceylonitsolutions.online';
6
- this.ws = null;
7
- this.socketId = null;
8
- this.channels = new Map();
9
- this.reconnectAttempts = 0;
10
- this.maxReconnectAttempts = 5;
11
- }
12
-
13
- connect() {
14
- return new Promise((resolve, reject) => {
15
- try {
16
- this.ws = new WebSocket(`${this.wsUrl}?appKey=${this.appKey}`);
17
- } catch (error) {
18
- reject(error);
19
- this.attemptReconnect();
20
- return;
21
- }
22
-
23
- const timeout = setTimeout(() => {
24
- if (!this.socketId) {
25
- reject(new Error('Connection timeout'));
26
- this.attemptReconnect();
27
- }
28
- }, 10000);
29
-
30
- this.ws.onopen = () => {
31
- console.log('[PushStream] Connected');
32
- this.reconnectAttempts = 0;
33
- };
34
-
35
- this.ws.onmessage = (event) => {
36
- const message = JSON.parse(event.data);
37
-
38
- if (message.event === 'pusher:connection_established') {
39
- const data = JSON.parse(message.data);
40
- this.socketId = data.socket_id;
41
- clearTimeout(timeout);
42
- resolve(this.socketId);
43
- } else if (message.event === 'pusher:error') {
44
- console.error('[PushStream] Error:', message.data);
45
- } else {
46
- this.handleMessage(message);
47
- }
48
- };
49
-
50
- this.ws.onclose = () => {
51
- console.log('[PushStream] Disconnected');
52
- clearTimeout(timeout);
53
- this.socketId = null;
54
- this.attemptReconnect();
55
- };
56
-
57
- this.ws.onerror = (error) => {
58
- console.error('[PushStream] WebSocket error:', error);
59
- clearTimeout(timeout);
60
- if (!this.socketId) reject(error);
61
- };
62
- });
63
- }
64
-
65
- attemptReconnect() {
66
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
67
- console.error('[PushStream] Max reconnection attempts reached');
68
- return;
69
- }
70
-
71
- const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
72
- this.reconnectAttempts++;
73
-
74
- console.log(`[PushStream] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
75
- setTimeout(() => this.connect(), delay);
76
- }
77
-
78
- subscribe(channelName, callbacks = {}) {
79
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
80
- throw new Error('Not connected');
81
- }
82
-
83
- const channel = new Channel(channelName, this, callbacks);
84
- this.channels.set(channelName, channel);
85
-
86
- this.send({
87
- event: 'pusher:subscribe',
88
- data: { channel: channelName }
89
- });
90
-
91
- return channel;
92
- }
93
-
94
- unsubscribe(channelName) {
95
- this.send({
96
- event: 'pusher:unsubscribe',
97
- data: { channel: channelName }
98
- });
99
- this.channels.delete(channelName);
100
- }
101
-
102
- send(data) {
103
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
104
- this.ws.send(JSON.stringify(data));
105
- }
106
- }
107
-
108
- handleMessage(message) {
109
- const channel = this.channels.get(message.channel);
110
- if (channel) {
111
- channel.handleEvent(message.event, JSON.parse(message.data));
112
- }
113
- }
114
-
115
- disconnect() {
116
- if (this.ws) {
117
- this.ws.close();
118
- this.ws = null;
119
- }
120
- }
121
-
122
- // REST API methods
123
- async publish(appId, appSecret, channel, event, data) {
124
- const timestamp = Math.floor(Date.now() / 1000);
125
- const body = JSON.stringify({ name: event, channel, data });
126
- const path = `/api/apps/${appId}/events`;
127
- const queryString = `auth_timestamp=${timestamp}`;
128
- const stringToSign = `POST\n${path}\n${queryString}\n${body}`;
129
-
130
- const signature = await this.hmacSha256(stringToSign, appSecret);
131
- const authHeader = `${appId}:${signature}`;
132
-
133
- const response = await fetch(`${this.apiUrl}${path}?${queryString}`, {
134
- method: 'POST',
135
- headers: {
136
- 'Authorization': authHeader,
137
- 'Content-Type': 'application/json'
138
- },
139
- body
140
- });
141
-
142
- if (!response.ok) {
143
- throw new Error(`HTTP ${response.status}: ${await response.text()}`);
144
- }
145
-
146
- return response.json();
147
- }
148
-
149
- async hmacSha256(message, secret) {
150
- // Node.js
151
- if (typeof window === 'undefined') {
152
- const crypto = require('crypto');
153
- return crypto.createHmac('sha256', secret).update(message).digest('hex');
154
- }
155
-
156
- // Browser
157
- const encoder = new TextEncoder();
158
- const keyData = encoder.encode(secret);
159
- const messageData = encoder.encode(message);
160
-
161
- const key = await crypto.subtle.importKey(
162
- 'raw',
163
- keyData,
164
- { name: 'HMAC', hash: 'SHA-256' },
165
- false,
166
- ['sign']
167
- );
168
-
169
- const signature = await crypto.subtle.sign('HMAC', key, messageData);
170
- return Array.from(new Uint8Array(signature))
171
- .map(b => b.toString(16).padStart(2, '0'))
172
- .join('');
173
- }
174
- }
175
-
176
- class Channel {
177
- constructor(name, client, callbacks = {}) {
178
- this.name = name;
179
- this.client = client;
180
- this.callbacks = callbacks;
181
- this.eventHandlers = new Map();
182
- }
183
-
184
- bind(event, callback) {
185
- if (!this.eventHandlers.has(event)) {
186
- this.eventHandlers.set(event, []);
187
- }
188
- this.eventHandlers.get(event).push(callback);
189
- return this;
190
- }
191
-
192
- unbind(event, callback) {
193
- if (!this.eventHandlers.has(event)) return;
194
-
195
- if (callback) {
196
- const handlers = this.eventHandlers.get(event);
197
- const index = handlers.indexOf(callback);
198
- if (index > -1) handlers.splice(index, 1);
199
- } else {
200
- this.eventHandlers.delete(event);
201
- }
202
- return this;
203
- }
204
-
205
- handleEvent(event, data) {
206
- const handlers = this.eventHandlers.get(event);
207
- if (handlers) {
208
- handlers.forEach(handler => handler(data));
209
- }
210
- }
211
-
212
- unsubscribe() {
213
- this.client.unsubscribe(this.name);
214
- }
215
- }
216
-
217
- // Node.js support
218
- if (typeof module !== 'undefined' && module.exports) {
219
- module.exports = PushStream;
220
- }
221
-
222
- // Browser support
223
- if (typeof window !== 'undefined') {
224
- window.PushStream = PushStream;
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": "1.0.2",
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": ["websocket", "realtime", "pusher", "messaging"],
15
- "author": "Ceylon IT Solutions",
16
- "license": "MIT",
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/bkkalana/pushstream-js.git"
20
- },
21
- "dependencies": {},
22
- "engines": {
23
- "node": ">=14.0.0"
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.wsUrl = options.wsUrl || 'wss://ws.pushstream.ceylonitsolutions.online';
5
- this.apiUrl = options.apiUrl || 'https://api.pushstream.ceylonitsolutions.online';
6
- this.ws = null;
7
- this.socketId = null;
8
- this.channels = new Map();
9
- this.reconnectAttempts = 0;
10
- this.maxReconnectAttempts = 5;
11
- }
12
-
13
- connect() {
14
- return new Promise((resolve, reject) => {
15
- try {
16
- this.ws = new WebSocket(`${this.wsUrl}?appKey=${this.appKey}`);
17
- } catch (error) {
18
- reject(error);
19
- this.attemptReconnect();
20
- return;
21
- }
22
-
23
- const timeout = setTimeout(() => {
24
- if (!this.socketId) {
25
- reject(new Error('Connection timeout'));
26
- this.attemptReconnect();
27
- }
28
- }, 10000);
29
-
30
- this.ws.onopen = () => {
31
- console.log('[PushStream] Connected');
32
- this.reconnectAttempts = 0;
33
- };
34
-
35
- this.ws.onmessage = (event) => {
36
- const message = JSON.parse(event.data);
37
-
38
- if (message.event === 'pusher:connection_established') {
39
- const data = JSON.parse(message.data);
40
- this.socketId = data.socket_id;
41
- clearTimeout(timeout);
42
- resolve(this.socketId);
43
- } else if (message.event === 'pusher:error') {
44
- console.error('[PushStream] Error:', message.data);
45
- } else {
46
- this.handleMessage(message);
47
- }
48
- };
49
-
50
- this.ws.onclose = () => {
51
- console.log('[PushStream] Disconnected');
52
- clearTimeout(timeout);
53
- this.socketId = null;
54
- this.attemptReconnect();
55
- };
56
-
57
- this.ws.onerror = (error) => {
58
- console.error('[PushStream] WebSocket error:', error);
59
- clearTimeout(timeout);
60
- if (!this.socketId) reject(error);
61
- };
62
- });
63
- }
64
-
65
- attemptReconnect() {
66
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
67
- console.error('[PushStream] Max reconnection attempts reached');
68
- return;
69
- }
70
-
71
- const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
72
- this.reconnectAttempts++;
73
-
74
- console.log(`[PushStream] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
75
- setTimeout(() => this.connect(), delay);
76
- }
77
-
78
- subscribe(channelName, callbacks = {}) {
79
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
80
- throw new Error('Not connected');
81
- }
82
-
83
- const channel = new Channel(channelName, this, callbacks);
84
- this.channels.set(channelName, channel);
85
-
86
- this.send({
87
- event: 'pusher:subscribe',
88
- data: { channel: channelName }
89
- });
90
-
91
- return channel;
92
- }
93
-
94
- unsubscribe(channelName) {
95
- this.send({
96
- event: 'pusher:unsubscribe',
97
- data: { channel: channelName }
98
- });
99
- this.channels.delete(channelName);
100
- }
101
-
102
- send(data) {
103
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
104
- this.ws.send(JSON.stringify(data));
105
- }
106
- }
107
-
108
- handleMessage(message) {
109
- const channel = this.channels.get(message.channel);
110
- if (channel) {
111
- channel.handleEvent(message.event, JSON.parse(message.data));
112
- }
113
- }
114
-
115
- disconnect() {
116
- if (this.ws) {
117
- this.ws.close();
118
- this.ws = null;
119
- }
120
- }
121
-
122
- // REST API methods
123
- async publish(appId, appSecret, channel, event, data) {
124
- const timestamp = Math.floor(Date.now() / 1000);
125
- const body = JSON.stringify({ name: event, channel, data });
126
- const path = `/api/apps/${appId}/events`;
127
- const queryString = `auth_timestamp=${timestamp}`;
128
- const stringToSign = `POST\n${path}\n${queryString}\n${body}`;
129
-
130
- const signature = await this.hmacSha256(stringToSign, appSecret);
131
- const authHeader = `${appId}:${signature}`;
132
-
133
- const response = await fetch(`${this.apiUrl}${path}?${queryString}`, {
134
- method: 'POST',
135
- headers: {
136
- 'Authorization': authHeader,
137
- 'Content-Type': 'application/json'
138
- },
139
- body
140
- });
141
-
142
- if (!response.ok) {
143
- throw new Error(`HTTP ${response.status}: ${await response.text()}`);
144
- }
145
-
146
- return response.json();
147
- }
148
-
149
- async hmacSha256(message, secret) {
150
- // Node.js
151
- if (typeof window === 'undefined') {
152
- const crypto = require('crypto');
153
- return crypto.createHmac('sha256', secret).update(message).digest('hex');
154
- }
155
-
156
- // Browser
157
- const encoder = new TextEncoder();
158
- const keyData = encoder.encode(secret);
159
- const messageData = encoder.encode(message);
160
-
161
- const key = await crypto.subtle.importKey(
162
- 'raw',
163
- keyData,
164
- { name: 'HMAC', hash: 'SHA-256' },
165
- false,
166
- ['sign']
167
- );
168
-
169
- const signature = await crypto.subtle.sign('HMAC', key, messageData);
170
- return Array.from(new Uint8Array(signature))
171
- .map(b => b.toString(16).padStart(2, '0'))
172
- .join('');
173
- }
174
- }
175
-
176
- class Channel {
177
- constructor(name, client, callbacks = {}) {
178
- this.name = name;
179
- this.client = client;
180
- this.callbacks = callbacks;
181
- this.eventHandlers = new Map();
182
- }
183
-
184
- bind(event, callback) {
185
- if (!this.eventHandlers.has(event)) {
186
- this.eventHandlers.set(event, []);
187
- }
188
- this.eventHandlers.get(event).push(callback);
189
- return this;
190
- }
191
-
192
- unbind(event, callback) {
193
- if (!this.eventHandlers.has(event)) return;
194
-
195
- if (callback) {
196
- const handlers = this.eventHandlers.get(event);
197
- const index = handlers.indexOf(callback);
198
- if (index > -1) handlers.splice(index, 1);
199
- } else {
200
- this.eventHandlers.delete(event);
201
- }
202
- return this;
203
- }
204
-
205
- handleEvent(event, data) {
206
- const handlers = this.eventHandlers.get(event);
207
- if (handlers) {
208
- handlers.forEach(handler => handler(data));
209
- }
210
- }
211
-
212
- unsubscribe() {
213
- this.client.unsubscribe(this.name);
214
- }
215
- }
216
-
217
- // Node.js support
218
- if (typeof module !== 'undefined' && module.exports) {
219
- module.exports = PushStream;
220
- }
221
-
222
- // Browser support
223
- if (typeof window !== 'undefined') {
224
- window.PushStream = PushStream;
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
+ }