pingerchips-js 2.0.0 → 2.1.1
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 +57 -46
- package/index.js +82 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pingerchips-js
|
|
2
2
|
|
|
3
3
|
Real-time WebSocket client for Pingerchips.
|
|
4
4
|
|
|
@@ -10,17 +10,29 @@ npm install pingerchips-js
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
### Basic
|
|
13
|
+
### Basic connection
|
|
14
14
|
|
|
15
15
|
```javascript
|
|
16
16
|
import Pingerchips from 'pingerchips-js';
|
|
17
17
|
|
|
18
|
+
// Connects to queue.pingerchips.com in production by default
|
|
19
|
+
const client = new Pingerchips('your_app_key');
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Pass `debug: "false"` to force the production endpoint, or override with `endpoint`:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const client = new Pingerchips('your_app_key', {
|
|
26
|
+
debug: "false" // uses wss://queue.pingerchips.com/socket
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// or explicit:
|
|
18
30
|
const client = new Pingerchips('your_app_key', {
|
|
19
|
-
endpoint: 'wss://
|
|
31
|
+
endpoint: 'wss://your-own-deployment.com/socket'
|
|
20
32
|
});
|
|
21
33
|
```
|
|
22
34
|
|
|
23
|
-
###
|
|
35
|
+
### Public channels
|
|
24
36
|
|
|
25
37
|
```javascript
|
|
26
38
|
const channel = await client.subscribe('lobby');
|
|
@@ -29,77 +41,76 @@ channel.bind('message', (data) => {
|
|
|
29
41
|
console.log('Received:', data);
|
|
30
42
|
});
|
|
31
43
|
|
|
44
|
+
// Send client event
|
|
32
45
|
channel.trigger('message', { text: 'Hello!' });
|
|
33
46
|
```
|
|
34
47
|
|
|
35
|
-
###
|
|
48
|
+
### Private channels
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
Requires an `authEndpoint` on your server (see `pingerchips-js-server`):
|
|
38
51
|
|
|
39
52
|
```javascript
|
|
40
53
|
const client = new Pingerchips('your_app_key', {
|
|
41
|
-
endpoint: 'wss://pinger-processor.pingerchips.com/socket',
|
|
42
54
|
authEndpoint: 'https://your-server.com/auth',
|
|
43
|
-
authInfo: {
|
|
44
|
-
userId: '123',
|
|
45
|
-
token: 'user-session-token'
|
|
46
|
-
}
|
|
55
|
+
authInfo: { token: 'user-session-token' } // forwarded to your auth endpoint
|
|
47
56
|
});
|
|
48
57
|
|
|
49
|
-
const
|
|
58
|
+
const channel = await client.subscribe('private-chat');
|
|
50
59
|
```
|
|
51
60
|
|
|
52
|
-
###
|
|
61
|
+
### Presence channels
|
|
53
62
|
|
|
54
63
|
```javascript
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log('User joined:', data.user_info);
|
|
64
|
+
const client = new Pingerchips('your_app_key', {
|
|
65
|
+
authEndpoint: 'https://your-server.com/auth',
|
|
66
|
+
authInfo: { token: 'user-session-token' }
|
|
59
67
|
});
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Configuration Options
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|--------|------|-------------|
|
|
66
|
-
| `endpoint` | string | WebSocket endpoint URL |
|
|
67
|
-
| `authEndpoint` | string | Your server's auth endpoint for private/presence channels |
|
|
68
|
-
| `authInfo` | object | User authentication info passed to your auth endpoint |
|
|
69
|
-
| `authHeaders` | object | Additional headers for auth requests |
|
|
69
|
+
const channel = await client.subscribe('presence-lobby');
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## Channel Methods
|
|
76
|
-
|
|
77
|
-
### `bind(event, callback)`
|
|
78
|
-
|
|
79
|
-
Listen for events on a channel.
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
channel.bind('message', (data) => {
|
|
83
|
-
console.log(data);
|
|
71
|
+
channel.bind('presence_diff', ({ joins, leaves }) => {
|
|
72
|
+
console.log('Joined:', joins);
|
|
73
|
+
console.log('Left:', leaves);
|
|
84
74
|
});
|
|
85
75
|
```
|
|
86
76
|
|
|
87
|
-
###
|
|
77
|
+
### Reconnection
|
|
78
|
+
|
|
79
|
+
The client automatically reconnects on disconnect. All previously subscribed channels are re-joined transparently — no extra handling needed.
|
|
88
80
|
|
|
89
|
-
|
|
81
|
+
### Socket ID
|
|
90
82
|
|
|
91
83
|
```javascript
|
|
92
|
-
|
|
84
|
+
// Available after first channel join
|
|
85
|
+
const socketId = client.getSocketId();
|
|
93
86
|
```
|
|
94
87
|
|
|
95
|
-
###
|
|
96
|
-
|
|
97
|
-
Stop listening for an event.
|
|
88
|
+
### Unsubscribe
|
|
98
89
|
|
|
99
90
|
```javascript
|
|
100
|
-
|
|
91
|
+
client.unsubscribe('lobby');
|
|
101
92
|
```
|
|
102
93
|
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `endpoint` | string | auto | WebSocket endpoint. Defaults to `wss://queue.pingerchips.com/socket` (prod) or `ws://localhost:4000/socket` (dev) |
|
|
99
|
+
| `debug` | string | — | Set to `"false"` to use production endpoint without providing `endpoint` explicitly |
|
|
100
|
+
| `authEndpoint` | string | — | Your server's auth URL for private/presence channels |
|
|
101
|
+
| `authInfo` | object | `{}` | Forwarded to your auth endpoint in the request body |
|
|
102
|
+
| `authHeaders` | object | — | Extra headers added to auth requests |
|
|
103
|
+
| `params` | object | — | Extra params sent with the WebSocket connection |
|
|
104
|
+
|
|
105
|
+
## Channel API
|
|
106
|
+
|
|
107
|
+
| Method | Description |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `bind(event, callback)` | Listen for an event |
|
|
110
|
+
| `unbind(event)` | Stop listening for an event |
|
|
111
|
+
| `trigger(event, data)` | Send a client event |
|
|
112
|
+
| `leave()` | Leave the channel |
|
|
113
|
+
|
|
103
114
|
## License
|
|
104
115
|
|
|
105
116
|
MIT
|
package/index.js
CHANGED
|
@@ -7,87 +7,124 @@ export class Pingerchips {
|
|
|
7
7
|
this.socket = null;
|
|
8
8
|
this.channels = {};
|
|
9
9
|
this.socketId = null;
|
|
10
|
-
this.authInfo = options.authInfo || {};
|
|
10
|
+
this.authInfo = options.authInfo || {};
|
|
11
|
+
this._endpoint = this._resolveEndpoint();
|
|
11
12
|
|
|
12
13
|
this.connect();
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
_resolveEndpoint() {
|
|
17
|
+
return (
|
|
18
|
+
this.options.endpoint ||
|
|
19
|
+
(this.options.debug === "false"
|
|
20
|
+
? "wss://queue.pingerchips.com/socket"
|
|
21
|
+
: "ws://localhost:4000/socket")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
connect() {
|
|
16
26
|
const params = { app_key: this.key, ...this.options.params };
|
|
17
|
-
const endpoint =
|
|
18
|
-
this.options.endpoint || this.options.debug === "false"
|
|
19
|
-
? "wss://pinger-processor.pingerchips.com/socket"
|
|
20
|
-
: "ws://localhost:4000/socket";
|
|
21
27
|
|
|
22
|
-
this.socket = new Socket(
|
|
28
|
+
this.socket = new Socket(this._endpoint, { params });
|
|
23
29
|
this.socket.connect();
|
|
24
30
|
|
|
25
31
|
this.socket.onOpen(() => {
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
32
|
+
// Socket ID comes from the server via socket params on open —
|
|
33
|
+
// read it directly from the socket transport if available,
|
|
34
|
+
// otherwise it is set when the first channel join receives socket_id.
|
|
35
|
+
const transportParams = this.socket.params();
|
|
36
|
+
if (transportParams?.socket_id) {
|
|
37
|
+
this.socketId = transportParams.socket_id;
|
|
38
|
+
}
|
|
30
39
|
});
|
|
31
40
|
|
|
32
41
|
this.socket.onClose(() => {
|
|
33
|
-
|
|
42
|
+
// Clear socket ID on disconnect — will be re-acquired on reconnect
|
|
43
|
+
this.socketId = null;
|
|
44
|
+
this._resubscribeOnReconnect();
|
|
34
45
|
});
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
// Re-join all previously subscribed channels after reconnect.
|
|
49
|
+
// Phoenix Socket handles reconnect automatically; we re-subscribe channels
|
|
50
|
+
// once the socket is open again.
|
|
51
|
+
_resubscribeOnReconnect() {
|
|
52
|
+
const channelNames = Object.keys(this.channels);
|
|
53
|
+
if (channelNames.length === 0) return;
|
|
54
|
+
|
|
55
|
+
// Clear stale channel references — they are bound to the old socket connection
|
|
56
|
+
this.channels = {};
|
|
57
|
+
|
|
58
|
+
const attemptResubscribe = () => {
|
|
59
|
+
if (this.socket.isConnected()) {
|
|
60
|
+
channelNames.forEach((name) => {
|
|
61
|
+
this.subscribe(name).catch(() => {
|
|
62
|
+
// Silently retry on next reconnect cycle
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
// Wait for next open event
|
|
67
|
+
this.socket.onOpen(() => {
|
|
68
|
+
channelNames.forEach((name) => {
|
|
69
|
+
this.subscribe(name).catch(() => {});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
attemptResubscribe();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async subscribe(channelName, options = {}) {
|
|
38
79
|
if (this.channels[channelName]) {
|
|
39
80
|
return this.channels[channelName];
|
|
40
81
|
}
|
|
41
82
|
|
|
42
|
-
// Topic format: "app:{app_key}:room:{channel_name}"
|
|
43
83
|
const topic = `app:${this.key}:room:${channelName}`;
|
|
44
|
-
|
|
45
|
-
// Check if authentication is needed
|
|
46
84
|
const isPrivate = channelName.startsWith("private-");
|
|
47
85
|
const isPresence = channelName.startsWith("presence-");
|
|
48
86
|
|
|
49
87
|
let joinParams = {};
|
|
50
88
|
|
|
51
89
|
if (isPrivate || isPresence) {
|
|
52
|
-
// Need to authenticate via user's auth endpoint
|
|
53
90
|
if (!this.options.authEndpoint) {
|
|
54
91
|
throw new Error(
|
|
55
|
-
"authEndpoint must be configured for private/presence channels"
|
|
92
|
+
"authEndpoint must be configured for private/presence channels",
|
|
56
93
|
);
|
|
57
94
|
}
|
|
58
95
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.error(`Failed to authenticate for ${channelName}:`, error);
|
|
64
|
-
throw error;
|
|
96
|
+
if (!this.socketId) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Socket ID not yet available. Ensure socket is connected before subscribing to private/presence channels.",
|
|
99
|
+
);
|
|
65
100
|
}
|
|
101
|
+
|
|
102
|
+
joinParams = await this.authenticate(channelName);
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
const channel = this.socket.channel(topic, joinParams);
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
channel
|
|
109
|
+
.join()
|
|
110
|
+
.receive("ok", (resp) => {
|
|
111
|
+
if (resp.socket_id && !this.socketId) {
|
|
112
|
+
this.socketId = resp.socket_id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const wrapper = new ChannelWrapper(channel);
|
|
116
|
+
this.channels[channelName] = wrapper;
|
|
117
|
+
resolve(wrapper);
|
|
118
|
+
})
|
|
119
|
+
.receive("error", (resp) => {
|
|
120
|
+
reject(new Error(`Failed to join channel: ${JSON.stringify(resp)}`));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
82
123
|
}
|
|
83
124
|
|
|
84
125
|
async authenticate(channelName) {
|
|
85
|
-
// Call user's auth endpoint with socket_id, channel_name, and auth_info
|
|
86
|
-
// User's server will use Pingerchips.authenticate() to sign the data
|
|
87
|
-
const socketId = this.getSocketId();
|
|
88
|
-
|
|
89
126
|
const body = {
|
|
90
|
-
socket_id: socketId,
|
|
127
|
+
socket_id: this.socketId,
|
|
91
128
|
channel_name: channelName,
|
|
92
129
|
auth_info: this.authInfo,
|
|
93
130
|
};
|
|
@@ -108,26 +145,20 @@ export class Pingerchips {
|
|
|
108
145
|
throw new Error(error.error || "Authentication failed");
|
|
109
146
|
}
|
|
110
147
|
|
|
111
|
-
|
|
112
|
-
return await response.json();
|
|
148
|
+
return response.json();
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
getSocketId() {
|
|
116
|
-
// Phoenix socket generates a unique ref for each connection
|
|
117
|
-
// We can use the socket's internal state or generate a compatible ID
|
|
118
|
-
// For now, use a simple approach: use the socket's makeRef() output
|
|
119
152
|
if (!this.socketId) {
|
|
120
|
-
|
|
121
|
-
.
|
|
122
|
-
|
|
153
|
+
throw new Error(
|
|
154
|
+
"Socket ID not available. Connect and join a channel first.",
|
|
155
|
+
);
|
|
123
156
|
}
|
|
124
157
|
return this.socketId;
|
|
125
158
|
}
|
|
126
159
|
|
|
127
160
|
getHttpEndpoint() {
|
|
128
|
-
|
|
129
|
-
// Convert ws:// to http:// and wss:// to https://
|
|
130
|
-
return wsEndpoint
|
|
161
|
+
return this._endpoint
|
|
131
162
|
.replace("ws://", "http://")
|
|
132
163
|
.replace("wss://", "https://")
|
|
133
164
|
.replace("/socket", "");
|