faye-redis-ng 1.0.2 → 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
  });
@@ -67,19 +100,16 @@ class Engine {
67
100
  this._initialized = true;
68
101
  });
69
102
 
70
- // Track if we've already set up subscriptions to prevent duplicates
71
- this._subscriptionsSetUp = false;
72
-
73
103
  this._subscriber.on('ready', async () => {
74
104
  console.log('Redis subscriber ready');
75
105
  // Only re-subscribe after reconnection (not on initial connection)
76
106
  if (this._subscriptionsSetUp) {
77
107
  console.log('Redis subscriber reconnected, re-subscribing...');
78
108
  try {
79
- await this._subscriber.subscribe(this._messageChannel, (message) => {
109
+ await this._subscriber!.subscribe(this._messageChannel, (message) => {
80
110
  this.emptyQueue(message);
81
111
  });
82
- await this._subscriber.subscribe(this._closeChannel, (message) => {
112
+ await this._subscriber!.subscribe(this._closeChannel, (message) => {
83
113
  this._server.trigger('close', message);
84
114
  });
85
115
  this._initialized = true;
@@ -105,7 +135,7 @@ class Engine {
105
135
  this._initialized = true;
106
136
  }
107
137
 
108
- _reconnectStrategy(retries) {
138
+ private _reconnectStrategy(retries: number): number | Error {
109
139
  // Exponential backoff with max delay of 10 seconds
110
140
  if (retries > 20) {
111
141
  // After 20 retries, give up (roughly 2 minutes)
@@ -117,27 +147,31 @@ class Engine {
117
147
  return delay;
118
148
  }
119
149
 
120
- async _waitForInit() {
150
+ private async _waitForInit(): Promise<void> {
121
151
  while (!this._initialized) {
122
152
  await new Promise(resolve => setTimeout(resolve, 10));
123
153
  }
124
154
  }
125
155
 
126
- static create(server, options) {
156
+ static create(server: FayeServer, options?: EngineOptions): Engine {
127
157
  return new this(server, options);
128
158
  }
129
159
 
130
- async disconnect() {
131
- await this._subscriber.unsubscribe();
132
- await this._redis.quit();
133
- 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
+ }
134
168
  clearInterval(this._gc);
135
169
  }
136
170
 
137
- createClient(callback, context) {
171
+ createClient(callback: ClientCallback, context: CallbackContext): void {
138
172
  this._waitForInit().then(async () => {
139
173
  const clientId = this._server.generateId();
140
- const added = await this._redis.zAdd(this._ns + '/clients', {
174
+ const added = await this._redis!.zAdd(this._ns + '/clients', {
141
175
  score: 0,
142
176
  value: clientId
143
177
  }, { NX: true });
@@ -155,22 +189,22 @@ class Engine {
155
189
  });
156
190
  }
157
191
 
158
- clientExists(clientId, callback, context) {
192
+ clientExists(clientId: string, callback: ExistsCallback, context: CallbackContext): void {
159
193
  this._waitForInit().then(async () => {
160
194
  const cutoff = new Date().getTime() - (1000 * 1.6 * this._server.timeout);
161
- const score = await this._redis.zScore(this._ns + '/clients', clientId);
162
- 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);
163
197
  }).catch(err => {
164
198
  console.error('Error checking client existence:', err);
165
199
  callback.call(context, false);
166
200
  });
167
201
  }
168
202
 
169
- destroyClient(clientId, callback, context) {
203
+ destroyClient(clientId: string, callback?: EmptyCallback, context?: CallbackContext): void {
170
204
  this._waitForInit().then(async () => {
171
- const channels = await this._redis.sMembers(this._ns + '/clients/' + clientId + '/channels');
205
+ const channels = await this._redis!.sMembers(this._ns + '/clients/' + clientId + '/channels');
172
206
 
173
- const multi = this._redis.multi();
207
+ const multi = this._redis!.multi();
174
208
  multi.zAdd(this._ns + '/clients', { score: 0, value: clientId });
175
209
 
176
210
  for (const channel of channels) {
@@ -200,7 +234,7 @@ class Engine {
200
234
  });
201
235
  }
202
236
 
203
- ping(clientId) {
237
+ ping(clientId: string): void {
204
238
  const timeout = this._server.timeout;
205
239
  if (typeof timeout !== 'number') return;
206
240
 
@@ -208,20 +242,20 @@ class Engine {
208
242
 
209
243
  this._server.debug('Ping ?, ?', clientId, time);
210
244
  this._waitForInit().then(async () => {
211
- await this._redis.zAdd(this._ns + '/clients', { score: time, value: clientId });
245
+ await this._redis!.zAdd(this._ns + '/clients', { score: time, value: clientId });
212
246
  }).catch(err => {
213
247
  console.error('Error pinging client:', err);
214
248
  });
215
249
  }
216
250
 
217
- subscribe(clientId, channel, callback, context) {
251
+ subscribe(clientId: string, channel: string, callback?: EmptyCallback, context?: CallbackContext): void {
218
252
  this._waitForInit().then(async () => {
219
- 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);
220
254
  if (added === 1) {
221
255
  this._server.trigger('subscribe', clientId, channel);
222
256
  }
223
257
 
224
- await this._redis.sAdd(this._ns + '/channels' + channel, clientId);
258
+ await this._redis!.sAdd(this._ns + '/channels' + channel, clientId);
225
259
  this._server.debug('Subscribed client ? to channel ?', clientId, channel);
226
260
  if (callback) callback.call(context);
227
261
  }).catch(err => {
@@ -230,14 +264,14 @@ class Engine {
230
264
  });
231
265
  }
232
266
 
233
- unsubscribe(clientId, channel, callback, context) {
267
+ unsubscribe(clientId: string, channel: string, callback?: EmptyCallback, context?: CallbackContext): void {
234
268
  this._waitForInit().then(async () => {
235
- 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);
236
270
  if (removed === 1) {
237
271
  this._server.trigger('unsubscribe', clientId, channel);
238
272
  }
239
273
 
240
- await this._redis.sRem(this._ns + '/channels' + channel, clientId);
274
+ await this._redis!.sRem(this._ns + '/channels' + channel, clientId);
241
275
  this._server.debug('Unsubscribed client ? from channel ?', clientId, channel);
242
276
  if (callback) callback.call(context);
243
277
  }).catch(err => {
@@ -246,28 +280,28 @@ class Engine {
246
280
  });
247
281
  }
248
282
 
249
- publish(message, channels) {
283
+ publish(message: FayeMessage, channels: string[]): void {
250
284
  this._server.debug('Publishing message ?', message);
251
285
 
252
286
  this._waitForInit().then(async () => {
253
287
  const jsonMessage = JSON.stringify(message);
254
288
  const keys = channels.map(c => this._ns + '/channels' + c);
255
289
 
256
- const clients = await this._redis.sUnion(keys);
290
+ const clients = await this._redis!.sUnion(keys);
257
291
 
258
292
  for (const clientId of clients) {
259
293
  const queue = this._ns + '/clients/' + clientId + '/messages';
260
294
 
261
295
  this._server.debug('Queueing for client ?: ?', clientId, message);
262
- await this._redis.rPush(queue, jsonMessage);
263
- await this._redis.publish(this._messageChannel, clientId);
296
+ await this._redis!.rPush(queue, jsonMessage);
297
+ await this._redis!.publish(this._messageChannel, clientId);
264
298
 
265
- const exists = await new Promise((resolve) => {
299
+ const exists = await new Promise<boolean>((resolve) => {
266
300
  this.clientExists(clientId, resolve, null);
267
301
  });
268
302
 
269
303
  if (!exists) {
270
- await this._redis.del(queue);
304
+ await this._redis!.del(queue);
271
305
  }
272
306
  }
273
307
 
@@ -277,21 +311,21 @@ class Engine {
277
311
  });
278
312
  }
279
313
 
280
- emptyQueue(clientId) {
314
+ emptyQueue(clientId: string): void {
281
315
  if (!this._server.hasConnection(clientId)) return;
282
316
 
283
317
  this._waitForInit().then(async () => {
284
318
  const key = this._ns + '/clients/' + clientId + '/messages';
285
- const multi = this._redis.multi();
319
+ const multi = this._redis!.multi();
286
320
 
287
321
  multi.lRange(key, 0, -1);
288
322
  multi.del(key);
289
323
 
290
324
  const results = await multi.exec();
291
- const jsonMessages = results[0];
325
+ const jsonMessages = results[0] as string[];
292
326
 
293
327
  if (jsonMessages && jsonMessages.length > 0) {
294
- const messages = jsonMessages.map(json => JSON.parse(json));
328
+ const messages = jsonMessages.map(json => JSON.parse(json) as FayeMessage);
295
329
  this._server.deliver(clientId, messages);
296
330
  }
297
331
  }).catch(err => {
@@ -299,14 +333,14 @@ class Engine {
299
333
  });
300
334
  }
301
335
 
302
- gc() {
336
+ gc(): void {
303
337
  const timeout = this._server.timeout;
304
338
  if (typeof timeout !== 'number') return;
305
339
 
306
340
  this._withLock('gc', async (releaseLock) => {
307
341
  const cutoff = new Date().getTime() - 1000 * 2 * timeout;
308
342
 
309
- const clients = await this._redis.zRangeByScore(this._ns + '/clients', 0, cutoff);
343
+ const clients = await this._redis!.zRangeByScore(this._ns + '/clients', 0, cutoff);
310
344
 
311
345
  if (clients.length === 0) {
312
346
  releaseLock();
@@ -325,30 +359,30 @@ class Engine {
325
359
  });
326
360
  }
327
361
 
328
- _withLock(lockName, callback) {
362
+ private _withLock(lockName: string, callback: (releaseLock: () => Promise<void>) => void): void {
329
363
  this._waitForInit().then(async () => {
330
364
  const lockKey = this._ns + '/locks/' + lockName;
331
365
  const currentTime = new Date().getTime();
332
366
  const expiry = currentTime + this.LOCK_TIMEOUT * 1000 + 1;
333
367
 
334
- const releaseLock = async () => {
368
+ const releaseLock = async (): Promise<void> => {
335
369
  if (new Date().getTime() < expiry) {
336
- await this._redis.del(lockKey);
370
+ await this._redis!.del(lockKey);
337
371
  }
338
372
  };
339
373
 
340
- const set = await this._redis.setNX(lockKey, expiry.toString());
374
+ const set = await this._redis!.setNX(lockKey, expiry.toString());
341
375
  if (set) {
342
376
  return callback.call(this, releaseLock);
343
377
  }
344
378
 
345
- const timeout = await this._redis.get(lockKey);
379
+ const timeout = await this._redis!.get(lockKey);
346
380
  if (!timeout) return;
347
381
 
348
382
  const lockTimeout = parseInt(timeout, 10);
349
383
  if (currentTime < lockTimeout) return;
350
384
 
351
- const oldValue = await this._redis.set(lockKey, expiry.toString(), { GET: true });
385
+ const oldValue = await this._redis!.set(lockKey, expiry.toString(), { GET: true });
352
386
  if (oldValue === timeout) {
353
387
  callback.call(this, releaseLock);
354
388
  }
@@ -358,10 +392,6 @@ class Engine {
358
392
  }
359
393
  }
360
394
 
361
- Engine.prototype.DEFAULT_HOST = 'localhost';
362
- Engine.prototype.DEFAULT_PORT = 6379;
363
- Engine.prototype.DEFAULT_DATABASE = 0;
364
- Engine.prototype.DEFAULT_GC = 60;
365
- Engine.prototype.LOCK_TIMEOUT = 120;
366
-
367
- 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`