faye-redis-ng 1.0.1 → 1.1.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.
@@ -1,7 +1,34 @@
1
- const redis = require('redis');
1
+ import { createClient } from 'redis';
2
+ import type {
3
+ EngineOptions,
4
+ FayeMessage,
5
+ FayeServer,
6
+ RedisClient,
7
+ CallbackContext,
8
+ ClientCallback,
9
+ ExistsCallback,
10
+ EmptyCallback
11
+ } from './types';
2
12
 
3
13
  class Engine {
4
- constructor(server, options = {}) {
14
+ private readonly DEFAULT_HOST = 'localhost';
15
+ private readonly DEFAULT_PORT = 6379;
16
+ private readonly DEFAULT_DATABASE = 0;
17
+ private readonly DEFAULT_GC = 60;
18
+ private readonly LOCK_TIMEOUT = 120;
19
+
20
+ private _server: FayeServer;
21
+ private _options: EngineOptions;
22
+ private _ns: string;
23
+ private _messageChannel: string;
24
+ private _closeChannel: string;
25
+ private _redis: RedisClient | null = null;
26
+ private _subscriber: RedisClient | null = null;
27
+ private _initialized = false;
28
+ private _subscriptionsSetUp = false;
29
+ private _gc: NodeJS.Timeout;
30
+
31
+ constructor(server: FayeServer, options: EngineOptions = {}) {
5
32
  this._server = server;
6
33
  this._options = options;
7
34
 
@@ -9,11 +36,6 @@ class Engine {
9
36
  this._messageChannel = this._ns + '/notifications/messages';
10
37
  this._closeChannel = this._ns + '/notifications/close';
11
38
 
12
- // Initialize clients (will be connected in _initializeClients)
13
- this._redis = null;
14
- this._subscriber = null;
15
- this._initialized = false;
16
-
17
39
  // Start initialization
18
40
  this._initializeClients().catch(err => {
19
41
  console.error('Failed to initialize Redis clients:', err);
@@ -23,7 +45,7 @@ class Engine {
23
45
  this._gc = setInterval(() => this.gc(), gc * 1000);
24
46
  }
25
47
 
26
- async _initializeClients() {
48
+ private async _initializeClients(): Promise<void> {
27
49
  const host = this._options.host || this.DEFAULT_HOST;
28
50
  const port = this._options.port || this.DEFAULT_PORT;
29
51
  const db = this._options.database || this.DEFAULT_DATABASE;
@@ -33,20 +55,31 @@ class Engine {
33
55
  const clientConfig = {
34
56
  database: db,
35
57
  ...(auth && { password: auth }),
36
- ...(socket && { socket: { path: socket, reconnectStrategy: this._reconnectStrategy.bind(this) } }),
37
- ...(!socket && { socket: { host, port, reconnectStrategy: this._reconnectStrategy.bind(this) } })
58
+ ...(socket && {
59
+ socket: {
60
+ path: socket,
61
+ reconnectStrategy: this._reconnectStrategy.bind(this)
62
+ }
63
+ }),
64
+ ...(!socket && {
65
+ socket: {
66
+ host,
67
+ port,
68
+ reconnectStrategy: this._reconnectStrategy.bind(this)
69
+ }
70
+ })
38
71
  };
39
72
 
40
- this._redis = redis.createClient(clientConfig);
41
- this._subscriber = redis.createClient(clientConfig);
73
+ this._redis = createClient(clientConfig);
74
+ this._subscriber = createClient(clientConfig);
42
75
 
43
76
  // Set up error handlers
44
- this._redis.on('error', (err) => {
77
+ this._redis.on('error', (err: Error) => {
45
78
  console.error('Redis client error:', err);
46
79
  this._server.trigger('error', err);
47
80
  });
48
81
 
49
- this._subscriber.on('error', (err) => {
82
+ this._subscriber.on('error', (err: Error) => {
50
83
  console.error('Redis subscriber error:', err);
51
84
  this._server.trigger('error', err);
52
85
  });
@@ -69,23 +102,27 @@ class Engine {
69
102
 
70
103
  this._subscriber.on('ready', async () => {
71
104
  console.log('Redis subscriber ready');
72
- // Re-subscribe after reconnection
73
- try {
74
- await this._subscriber.subscribe(this._messageChannel, (message) => {
75
- this.emptyQueue(message);
76
- });
77
- await this._subscriber.subscribe(this._closeChannel, (message) => {
78
- this._server.trigger('close', message);
79
- });
80
- this._initialized = true;
81
- } catch (err) {
82
- console.error('Error re-subscribing after reconnection:', err);
105
+ // Only re-subscribe after reconnection (not on initial connection)
106
+ if (this._subscriptionsSetUp) {
107
+ console.log('Redis subscriber reconnected, re-subscribing...');
108
+ try {
109
+ await this._subscriber!.subscribe(this._messageChannel, (message) => {
110
+ this.emptyQueue(message);
111
+ });
112
+ await this._subscriber!.subscribe(this._closeChannel, (message) => {
113
+ this._server.trigger('close', message);
114
+ });
115
+ this._initialized = true;
116
+ } catch (err) {
117
+ console.error('Error re-subscribing after reconnection:', err);
118
+ }
83
119
  }
84
120
  });
85
121
 
86
122
  await this._redis.connect();
87
123
  await this._subscriber.connect();
88
124
 
125
+ // Initial subscription (only once)
89
126
  await this._subscriber.subscribe(this._messageChannel, (message) => {
90
127
  this.emptyQueue(message);
91
128
  });
@@ -94,10 +131,11 @@ class Engine {
94
131
  this._server.trigger('close', message);
95
132
  });
96
133
 
134
+ this._subscriptionsSetUp = true;
97
135
  this._initialized = true;
98
136
  }
99
137
 
100
- _reconnectStrategy(retries) {
138
+ private _reconnectStrategy(retries: number): number | Error {
101
139
  // Exponential backoff with max delay of 10 seconds
102
140
  if (retries > 20) {
103
141
  // After 20 retries, give up (roughly 2 minutes)
@@ -109,27 +147,31 @@ class Engine {
109
147
  return delay;
110
148
  }
111
149
 
112
- async _waitForInit() {
150
+ private async _waitForInit(): Promise<void> {
113
151
  while (!this._initialized) {
114
152
  await new Promise(resolve => setTimeout(resolve, 10));
115
153
  }
116
154
  }
117
155
 
118
- static create(server, options) {
156
+ static create(server: FayeServer, options?: EngineOptions): Engine {
119
157
  return new this(server, options);
120
158
  }
121
159
 
122
- async disconnect() {
123
- await this._subscriber.unsubscribe();
124
- await this._redis.quit();
125
- await this._subscriber.quit();
160
+ async disconnect(): Promise<void> {
161
+ if (this._subscriber) {
162
+ await this._subscriber.unsubscribe();
163
+ await this._subscriber.quit();
164
+ }
165
+ if (this._redis) {
166
+ await this._redis.quit();
167
+ }
126
168
  clearInterval(this._gc);
127
169
  }
128
170
 
129
- createClient(callback, context) {
171
+ createClient(callback: ClientCallback, context: CallbackContext): void {
130
172
  this._waitForInit().then(async () => {
131
173
  const clientId = this._server.generateId();
132
- const added = await this._redis.zAdd(this._ns + '/clients', {
174
+ const added = await this._redis!.zAdd(this._ns + '/clients', {
133
175
  score: 0,
134
176
  value: clientId
135
177
  }, { NX: true });
@@ -147,22 +189,22 @@ class Engine {
147
189
  });
148
190
  }
149
191
 
150
- clientExists(clientId, callback, context) {
192
+ clientExists(clientId: string, callback: ExistsCallback, context: CallbackContext): void {
151
193
  this._waitForInit().then(async () => {
152
194
  const cutoff = new Date().getTime() - (1000 * 1.6 * this._server.timeout);
153
- const score = await this._redis.zScore(this._ns + '/clients', clientId);
154
- callback.call(context, score ? parseInt(score, 10) > cutoff : false);
195
+ const score = await this._redis!.zScore(this._ns + '/clients', clientId);
196
+ callback.call(context, score ? parseInt(score.toString(), 10) > cutoff : false);
155
197
  }).catch(err => {
156
198
  console.error('Error checking client existence:', err);
157
199
  callback.call(context, false);
158
200
  });
159
201
  }
160
202
 
161
- destroyClient(clientId, callback, context) {
203
+ destroyClient(clientId: string, callback?: EmptyCallback, context?: CallbackContext): void {
162
204
  this._waitForInit().then(async () => {
163
- const channels = await this._redis.sMembers(this._ns + '/clients/' + clientId + '/channels');
205
+ const channels = await this._redis!.sMembers(this._ns + '/clients/' + clientId + '/channels');
164
206
 
165
- const multi = this._redis.multi();
207
+ const multi = this._redis!.multi();
166
208
  multi.zAdd(this._ns + '/clients', { score: 0, value: clientId });
167
209
 
168
210
  for (const channel of channels) {
@@ -192,7 +234,7 @@ class Engine {
192
234
  });
193
235
  }
194
236
 
195
- ping(clientId) {
237
+ ping(clientId: string): void {
196
238
  const timeout = this._server.timeout;
197
239
  if (typeof timeout !== 'number') return;
198
240
 
@@ -200,20 +242,20 @@ class Engine {
200
242
 
201
243
  this._server.debug('Ping ?, ?', clientId, time);
202
244
  this._waitForInit().then(async () => {
203
- await this._redis.zAdd(this._ns + '/clients', { score: time, value: clientId });
245
+ await this._redis!.zAdd(this._ns + '/clients', { score: time, value: clientId });
204
246
  }).catch(err => {
205
247
  console.error('Error pinging client:', err);
206
248
  });
207
249
  }
208
250
 
209
- subscribe(clientId, channel, callback, context) {
251
+ subscribe(clientId: string, channel: string, callback?: EmptyCallback, context?: CallbackContext): void {
210
252
  this._waitForInit().then(async () => {
211
- const added = await this._redis.sAdd(this._ns + '/clients/' + clientId + '/channels', channel);
253
+ const added = await this._redis!.sAdd(this._ns + '/clients/' + clientId + '/channels', channel);
212
254
  if (added === 1) {
213
255
  this._server.trigger('subscribe', clientId, channel);
214
256
  }
215
257
 
216
- await this._redis.sAdd(this._ns + '/channels' + channel, clientId);
258
+ await this._redis!.sAdd(this._ns + '/channels' + channel, clientId);
217
259
  this._server.debug('Subscribed client ? to channel ?', clientId, channel);
218
260
  if (callback) callback.call(context);
219
261
  }).catch(err => {
@@ -222,14 +264,14 @@ class Engine {
222
264
  });
223
265
  }
224
266
 
225
- unsubscribe(clientId, channel, callback, context) {
267
+ unsubscribe(clientId: string, channel: string, callback?: EmptyCallback, context?: CallbackContext): void {
226
268
  this._waitForInit().then(async () => {
227
- const removed = await this._redis.sRem(this._ns + '/clients/' + clientId + '/channels', channel);
269
+ const removed = await this._redis!.sRem(this._ns + '/clients/' + clientId + '/channels', channel);
228
270
  if (removed === 1) {
229
271
  this._server.trigger('unsubscribe', clientId, channel);
230
272
  }
231
273
 
232
- await this._redis.sRem(this._ns + '/channels' + channel, clientId);
274
+ await this._redis!.sRem(this._ns + '/channels' + channel, clientId);
233
275
  this._server.debug('Unsubscribed client ? from channel ?', clientId, channel);
234
276
  if (callback) callback.call(context);
235
277
  }).catch(err => {
@@ -238,28 +280,28 @@ class Engine {
238
280
  });
239
281
  }
240
282
 
241
- publish(message, channels) {
283
+ publish(message: FayeMessage, channels: string[]): void {
242
284
  this._server.debug('Publishing message ?', message);
243
285
 
244
286
  this._waitForInit().then(async () => {
245
287
  const jsonMessage = JSON.stringify(message);
246
288
  const keys = channels.map(c => this._ns + '/channels' + c);
247
289
 
248
- const clients = await this._redis.sUnion(keys);
290
+ const clients = await this._redis!.sUnion(keys);
249
291
 
250
292
  for (const clientId of clients) {
251
293
  const queue = this._ns + '/clients/' + clientId + '/messages';
252
294
 
253
295
  this._server.debug('Queueing for client ?: ?', clientId, message);
254
- await this._redis.rPush(queue, jsonMessage);
255
- await this._redis.publish(this._messageChannel, clientId);
296
+ await this._redis!.rPush(queue, jsonMessage);
297
+ await this._redis!.publish(this._messageChannel, clientId);
256
298
 
257
- const exists = await new Promise((resolve) => {
299
+ const exists = await new Promise<boolean>((resolve) => {
258
300
  this.clientExists(clientId, resolve, null);
259
301
  });
260
302
 
261
303
  if (!exists) {
262
- await this._redis.del(queue);
304
+ await this._redis!.del(queue);
263
305
  }
264
306
  }
265
307
 
@@ -269,21 +311,21 @@ class Engine {
269
311
  });
270
312
  }
271
313
 
272
- emptyQueue(clientId) {
314
+ emptyQueue(clientId: string): void {
273
315
  if (!this._server.hasConnection(clientId)) return;
274
316
 
275
317
  this._waitForInit().then(async () => {
276
318
  const key = this._ns + '/clients/' + clientId + '/messages';
277
- const multi = this._redis.multi();
319
+ const multi = this._redis!.multi();
278
320
 
279
321
  multi.lRange(key, 0, -1);
280
322
  multi.del(key);
281
323
 
282
324
  const results = await multi.exec();
283
- const jsonMessages = results[0];
325
+ const jsonMessages = results[0] as string[];
284
326
 
285
327
  if (jsonMessages && jsonMessages.length > 0) {
286
- const messages = jsonMessages.map(json => JSON.parse(json));
328
+ const messages = jsonMessages.map(json => JSON.parse(json) as FayeMessage);
287
329
  this._server.deliver(clientId, messages);
288
330
  }
289
331
  }).catch(err => {
@@ -291,14 +333,14 @@ class Engine {
291
333
  });
292
334
  }
293
335
 
294
- gc() {
336
+ gc(): void {
295
337
  const timeout = this._server.timeout;
296
338
  if (typeof timeout !== 'number') return;
297
339
 
298
340
  this._withLock('gc', async (releaseLock) => {
299
341
  const cutoff = new Date().getTime() - 1000 * 2 * timeout;
300
342
 
301
- const clients = await this._redis.zRangeByScore(this._ns + '/clients', 0, cutoff);
343
+ const clients = await this._redis!.zRangeByScore(this._ns + '/clients', 0, cutoff);
302
344
 
303
345
  if (clients.length === 0) {
304
346
  releaseLock();
@@ -317,30 +359,30 @@ class Engine {
317
359
  });
318
360
  }
319
361
 
320
- _withLock(lockName, callback) {
362
+ private _withLock(lockName: string, callback: (releaseLock: () => Promise<void>) => void): void {
321
363
  this._waitForInit().then(async () => {
322
364
  const lockKey = this._ns + '/locks/' + lockName;
323
365
  const currentTime = new Date().getTime();
324
366
  const expiry = currentTime + this.LOCK_TIMEOUT * 1000 + 1;
325
367
 
326
- const releaseLock = async () => {
368
+ const releaseLock = async (): Promise<void> => {
327
369
  if (new Date().getTime() < expiry) {
328
- await this._redis.del(lockKey);
370
+ await this._redis!.del(lockKey);
329
371
  }
330
372
  };
331
373
 
332
- const set = await this._redis.setNX(lockKey, expiry.toString());
374
+ const set = await this._redis!.setNX(lockKey, expiry.toString());
333
375
  if (set) {
334
376
  return callback.call(this, releaseLock);
335
377
  }
336
378
 
337
- const timeout = await this._redis.get(lockKey);
379
+ const timeout = await this._redis!.get(lockKey);
338
380
  if (!timeout) return;
339
381
 
340
382
  const lockTimeout = parseInt(timeout, 10);
341
383
  if (currentTime < lockTimeout) return;
342
384
 
343
- const oldValue = await this._redis.set(lockKey, expiry.toString(), { GET: true });
385
+ const oldValue = await this._redis!.set(lockKey, expiry.toString(), { GET: true });
344
386
  if (oldValue === timeout) {
345
387
  callback.call(this, releaseLock);
346
388
  }
@@ -350,10 +392,6 @@ class Engine {
350
392
  }
351
393
  }
352
394
 
353
- Engine.prototype.DEFAULT_HOST = 'localhost';
354
- Engine.prototype.DEFAULT_PORT = 6379;
355
- Engine.prototype.DEFAULT_DATABASE = 0;
356
- Engine.prototype.DEFAULT_GC = 60;
357
- Engine.prototype.LOCK_TIMEOUT = 120;
358
-
359
- module.exports = Engine;
395
+ export default Engine;
396
+ export { Engine };
397
+ export type { EngineOptions, FayeMessage, FayeServer };
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { RedisClientType } from 'redis';
2
+
3
+ export interface EngineOptions {
4
+ host?: string;
5
+ port?: number;
6
+ password?: string;
7
+ database?: number;
8
+ namespace?: string;
9
+ socket?: string;
10
+ gc?: number;
11
+ }
12
+
13
+ export interface FayeMessage {
14
+ clientId?: string;
15
+ channel: string;
16
+ data: any;
17
+ id?: string;
18
+ }
19
+
20
+ export interface FayeServer {
21
+ timeout: number;
22
+ generateId(): string;
23
+ debug(...args: any[]): void;
24
+ trigger(event: string, ...args: any[]): void;
25
+ hasConnection(clientId: string): boolean;
26
+ deliver(clientId: string, messages: FayeMessage[]): void;
27
+ }
28
+
29
+ export type RedisClient = RedisClientType;
30
+
31
+ export type CallbackContext = any;
32
+
33
+ export type EmptyCallback = () => void;
34
+ export type ClientCallback = (clientId: string) => void;
35
+ export type ExistsCallback = (exists: boolean) => void;
@@ -1,117 +0,0 @@
1
- # Quick Release Guide
2
-
3
- ## 🚀 How to Release a New Version
4
-
5
- ### For Bug Fixes (Patch: 1.0.1 → 1.0.2)
6
-
7
- ```bash
8
- # Update version and create tag
9
- npm version patch -m "Fix: description of bug fix"
10
-
11
- # Push to GitHub (triggers auto-publish)
12
- git push origin master --follow-tags
13
- ```
14
-
15
- ### For New Features (Minor: 1.0.1 → 1.1.0)
16
-
17
- ```bash
18
- # Update version and create tag
19
- npm version minor -m "Feature: description of new feature"
20
-
21
- # Push to GitHub (triggers auto-publish)
22
- git push origin master --follow-tags
23
- ```
24
-
25
- ### For Breaking Changes (Major: 1.0.1 → 2.0.0)
26
-
27
- ```bash
28
- # Update version and create tag
29
- npm version major -m "Breaking: description of breaking change"
30
-
31
- # Push to GitHub (triggers auto-publish)
32
- git push origin master --follow-tags
33
- ```
34
-
35
- ## 📋 Pre-Release Checklist
36
-
37
- Before running `npm version`:
38
-
39
- - [ ] All changes committed
40
- - [ ] Tests passing locally
41
- - [ ] CHANGELOG.md updated
42
- - [ ] README.md updated (if needed)
43
- - [ ] No uncommitted changes (`git status` clean)
44
-
45
- ## 🔍 What Happens Automatically
46
-
47
- When you push a tag, GitHub Actions will:
48
-
49
- 1. ✅ Verify package version matches tag
50
- 2. ✅ Run tests with Redis
51
- 3. ✅ Publish to npm with provenance
52
- 4. ✅ Extract changelog for this version
53
- 5. ✅ Create GitHub Release with notes
54
- 6. ✅ Upload package tarball
55
-
56
- **Check progress**: https://github.com/YOUR-USERNAME/faye-redis-ng/actions
57
-
58
- ## 📝 Manual Release (If Automation Fails)
59
-
60
- ```bash
61
- # 1. Update version in package.json manually
62
- # 2. Update CHANGELOG.md
63
- # 3. Commit changes
64
- git add .
65
- git commit -m "Release v1.0.2"
66
-
67
- # 4. Create tag
68
- git tag v1.0.2
69
- git push origin master
70
- git push origin v1.0.2
71
-
72
- # 5. If GitHub Actions fails, publish manually:
73
- npm login
74
- npm publish --access public
75
- ```
76
-
77
- ## 🎯 First Time Publishing
78
-
79
- If this is your first publish:
80
-
81
- 1. **One-time setup** (see `.github/SETUP.md`):
82
- - Create npm token
83
- - Add to GitHub Secrets as `NPM_TOKEN`
84
-
85
- 2. **Then just push a tag**:
86
- ```bash
87
- git tag v1.0.1
88
- git push origin master --follow-tags
89
- ```
90
-
91
- ## 🐛 Troubleshooting
92
-
93
- ### "Version already published"
94
-
95
- ```bash
96
- # Bump version again
97
- npm version patch
98
- git push origin master --follow-tags
99
- ```
100
-
101
- ### "npm token invalid"
102
-
103
- 1. Go to https://www.npmjs.com/settings/YOUR-USERNAME/tokens
104
- 2. Regenerate token
105
- 3. Update GitHub Secret `NPM_TOKEN`
106
- 4. Re-run failed workflow
107
-
108
- ### Tag pushed but workflow didn't run
109
-
110
- Check:
111
- 1. `.github/workflows/publish.yml` exists
112
- 2. GitHub Actions enabled in repository settings
113
- 3. Tag starts with `v` (e.g., `v1.0.1`)
114
-
115
- ---
116
-
117
- **Need help?** See full setup guide in `.github/SETUP.md`