reachlo 1.2.2 → 1.3.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/index.js +118 -14
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -20,11 +20,16 @@ class Reachlo {
|
|
|
20
20
|
this.lastPong = Date.now();
|
|
21
21
|
this.reconnecting = false; // Prevent duplicate events
|
|
22
22
|
|
|
23
|
+
// Protocol v2 State
|
|
24
|
+
this.msgId = 0;
|
|
25
|
+
this.ackPending = new Map(); // id -> { resolve, reject, timeout }
|
|
26
|
+
|
|
23
27
|
// Lifecycle Listeners
|
|
24
28
|
this.listeners = {
|
|
25
29
|
connect: new Set(),
|
|
26
30
|
disconnect: new Set(),
|
|
27
|
-
reconnecting: new Set()
|
|
31
|
+
reconnecting: new Set(),
|
|
32
|
+
error: new Set()
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -78,9 +83,15 @@ class Reachlo {
|
|
|
78
83
|
|
|
79
84
|
// Resubscribe active channels + flush pending
|
|
80
85
|
const toSubscribe = new Set([...this.channels.keys(), ...this.pendingSubs]);
|
|
81
|
-
toSubscribe.forEach(
|
|
82
|
-
this.
|
|
83
|
-
|
|
86
|
+
toSubscribe.forEach(channelName => {
|
|
87
|
+
const channel = this.channels.get(channelName);
|
|
88
|
+
// Pass capabilities if available (if it was already created)
|
|
89
|
+
// If it was just pendingSub (no channel obj yet? Wait, pendingSub is only added if channel obj created)
|
|
90
|
+
// Yes, logic: create Channel obj -> if socket open send, else add to pending.
|
|
91
|
+
// So channel obj always exists when using pendingSubs for resubscribe
|
|
92
|
+
const options = channel ? channel.options : {};
|
|
93
|
+
this._send({ type: 'subscribe', channel: channelName, options: options });
|
|
94
|
+
});
|
|
84
95
|
this.pendingSubs.clear();
|
|
85
96
|
|
|
86
97
|
// Flush Offline Buffer (if enabled)
|
|
@@ -96,6 +107,14 @@ class Reachlo {
|
|
|
96
107
|
this.socket.onclose = () => {
|
|
97
108
|
console.log("Reachlo: Connection closed");
|
|
98
109
|
this._stopHeartbeat();
|
|
110
|
+
|
|
111
|
+
// SDK Review Fix: ACK Memory Leak Protection
|
|
112
|
+
this.ackPending.forEach(({ reject, timeout }) => {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
reject(new Error('Disconnected'));
|
|
115
|
+
});
|
|
116
|
+
this.ackPending.clear();
|
|
117
|
+
|
|
99
118
|
this._emitLifecycle('disconnect');
|
|
100
119
|
|
|
101
120
|
if (this.shouldReconnect) {
|
|
@@ -121,8 +140,27 @@ class Reachlo {
|
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
if (payload.
|
|
125
|
-
this.
|
|
143
|
+
if (payload.type === 'ack') {
|
|
144
|
+
const pending = this.ackPending.get(payload.id);
|
|
145
|
+
if (pending) {
|
|
146
|
+
pending.resolve();
|
|
147
|
+
clearTimeout(pending.timeout);
|
|
148
|
+
this.ackPending.delete(payload.id);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const channelObj = this.channels.get(payload.channel);
|
|
154
|
+
if (channelObj) {
|
|
155
|
+
if (payload.type === 'publish') {
|
|
156
|
+
channelObj._emit(payload.data);
|
|
157
|
+
} else if (payload.type === 'backpressure') {
|
|
158
|
+
channelObj._emitEvent('backpressure', payload);
|
|
159
|
+
} else if (payload.type === 'join') {
|
|
160
|
+
channelObj._emitEvent('join', payload);
|
|
161
|
+
} else if (payload.type === 'leave') {
|
|
162
|
+
channelObj._emitEvent('leave', payload);
|
|
163
|
+
}
|
|
126
164
|
}
|
|
127
165
|
} catch (e) {
|
|
128
166
|
console.error("Reachlo: Message parse error", e);
|
|
@@ -154,7 +192,7 @@ class Reachlo {
|
|
|
154
192
|
this.shouldReconnect = false;
|
|
155
193
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
156
194
|
this._stopHeartbeat();
|
|
157
|
-
|
|
195
|
+
|
|
158
196
|
|
|
159
197
|
if (this.socket) {
|
|
160
198
|
this.socket.close();
|
|
@@ -164,13 +202,13 @@ class Reachlo {
|
|
|
164
202
|
this.offlineBuffer = [];
|
|
165
203
|
}
|
|
166
204
|
|
|
167
|
-
channel(channelName) {
|
|
205
|
+
channel(channelName, options = {}) {
|
|
168
206
|
if (!this.channels.has(channelName)) {
|
|
169
|
-
const channel = new ReachloChannel(channelName, this);
|
|
207
|
+
const channel = new ReachloChannel(channelName, this, options);
|
|
170
208
|
this.channels.set(channelName, channel);
|
|
171
209
|
|
|
172
210
|
if (this.socket?.readyState === 1) { // WebSocket.OPEN is 1
|
|
173
|
-
this._send({ type: 'subscribe', channel: channelName });
|
|
211
|
+
this._send({ type: 'subscribe', channel: channelName, options: options });
|
|
174
212
|
} else {
|
|
175
213
|
this.pendingSubs.add(channelName);
|
|
176
214
|
}
|
|
@@ -194,10 +232,16 @@ class Reachlo {
|
|
|
194
232
|
}
|
|
195
233
|
|
|
196
234
|
class ReachloChannel {
|
|
197
|
-
constructor(name, client) {
|
|
235
|
+
constructor(name, client, options = {}) {
|
|
198
236
|
this.name = name;
|
|
199
237
|
this.client = client;
|
|
238
|
+
this.options = options; // { type: 'durable' | 'presence' | 'stream' }
|
|
200
239
|
this.callbacks = new Set();
|
|
240
|
+
this.eventListeners = {
|
|
241
|
+
join: new Set(),
|
|
242
|
+
leave: new Set(),
|
|
243
|
+
backpressure: new Set()
|
|
244
|
+
};
|
|
201
245
|
}
|
|
202
246
|
|
|
203
247
|
subscribe(cb) {
|
|
@@ -205,16 +249,76 @@ class ReachloChannel {
|
|
|
205
249
|
return () => this.callbacks.delete(cb); // unsubscribe handle
|
|
206
250
|
}
|
|
207
251
|
|
|
208
|
-
|
|
209
|
-
|
|
252
|
+
on(event, cb) {
|
|
253
|
+
if (this.eventListeners[event]) {
|
|
254
|
+
this.eventListeners[event].add(cb);
|
|
255
|
+
}
|
|
256
|
+
return () => this.off(event, cb);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
off(event, cb) {
|
|
260
|
+
if (this.eventListeners[event]) {
|
|
261
|
+
this.eventListeners[event].delete(cb);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
publish(data, options = {}) {
|
|
266
|
+
const payload = {
|
|
267
|
+
type: 'publish',
|
|
268
|
+
channel: this.name,
|
|
269
|
+
data
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (options.delivery === 'ack') {
|
|
273
|
+
payload.delivery = 'ack';
|
|
274
|
+
const id = ++this.client.msgId;
|
|
275
|
+
payload.id = id;
|
|
276
|
+
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const timeoutMs = options.timeout || 5000;
|
|
279
|
+
const timeout = setTimeout(() => {
|
|
280
|
+
this.client.ackPending.delete(id);
|
|
281
|
+
reject(new Error('ACK timeout'));
|
|
282
|
+
}, timeoutMs);
|
|
283
|
+
|
|
284
|
+
this.client.ackPending.set(id, { resolve, reject, timeout });
|
|
285
|
+
|
|
286
|
+
const sentOrQueued = this.client._send(payload);
|
|
287
|
+
if (!sentOrQueued) {
|
|
288
|
+
clearTimeout(timeout);
|
|
289
|
+
this.client.ackPending.delete(id);
|
|
290
|
+
reject(new Error('Socket not connected'));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Fire-and-forget
|
|
296
|
+
this.client._send(payload);
|
|
297
|
+
return Promise.resolve();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
sync(lastSeenId = 0) {
|
|
301
|
+
this.client._send({
|
|
302
|
+
type: 'sync',
|
|
303
|
+
channel: this.name,
|
|
304
|
+
lastSeenId
|
|
305
|
+
});
|
|
210
306
|
}
|
|
211
307
|
|
|
212
308
|
_emit(data) {
|
|
213
309
|
// Microtask Delivery to prevent Event Loop Blocking
|
|
214
310
|
this.callbacks.forEach(cb => {
|
|
215
|
-
queueMicrotask(() => cb(data));
|
|
311
|
+
queueMicrotask(() => cb(data));
|
|
216
312
|
});
|
|
217
313
|
}
|
|
314
|
+
|
|
315
|
+
_emitEvent(event, payload) {
|
|
316
|
+
if (this.eventListeners[event]) {
|
|
317
|
+
this.eventListeners[event].forEach(cb => {
|
|
318
|
+
queueMicrotask(() => cb(payload));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
218
322
|
}
|
|
219
323
|
|
|
220
324
|
export default Reachlo;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reachlo",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Streaming-native real-time infrastructure. Ordered channels, durable replay, presence, backpressure, and ACK delivery.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Reachlo Team",
|