faye-redis-ng 1.0.2 → 1.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 +4 -1
- package/dist/faye-redis.d.ts +38 -0
- package/dist/faye-redis.d.ts.map +1 -0
- package/dist/faye-redis.js +336 -0
- package/dist/faye-redis.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +24 -7
- package/{faye-redis.js → src/faye-redis.ts} +97 -67
- package/src/index.ts +13 -0
- package/src/types.ts +35 -0
- package/.github/RELEASE.md +0 -117
- package/.github/SETUP.md +0 -251
- package/.github/TRUSTED_PUBLISHING.md +0 -219
- package/.github/workflows/ci.yml +0 -70
- package/.github/workflows/publish.yml +0 -77
- package/AUTOMATION.md +0 -256
- package/CHANGELOG.md +0 -98
- package/CLAUDE.md +0 -134
- package/CODE_OF_CONDUCT.md +0 -4
- package/NPM_PUBLISH.md +0 -358
- package/REFACTORING.md +0 -215
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 && {
|
|
37
|
-
|
|
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 =
|
|
41
|
-
this._subscriber =
|
|
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
|
|
109
|
+
await this._subscriber!.subscribe(this._messageChannel, (message) => {
|
|
80
110
|
this.emptyQueue(message);
|
|
81
111
|
});
|
|
82
|
-
await this._subscriber
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
|
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
|
|
205
|
+
const channels = await this._redis!.sMembers(this._ns + '/clients/' + clientId + '/channels');
|
|
172
206
|
|
|
173
|
-
const multi = this._redis
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
263
|
-
await this._redis
|
|
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
|
|
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
|
|
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
|
|
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
|
|
370
|
+
await this._redis!.del(lockKey);
|
|
337
371
|
}
|
|
338
372
|
};
|
|
339
373
|
|
|
340
|
-
const set = await this._redis
|
|
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
|
|
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
|
|
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
|
-
|
|
362
|
-
Engine
|
|
363
|
-
|
|
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/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Backward compatibility entry point
|
|
2
|
+
// This file provides CommonJS compatibility while maintaining TypeScript benefits
|
|
3
|
+
//
|
|
4
|
+
// Usage patterns supported:
|
|
5
|
+
// - const Engine = require('faye-redis-ng'); (CommonJS direct)
|
|
6
|
+
// - const { Engine } = require('faye-redis-ng'); (CommonJS destructured)
|
|
7
|
+
// - import Engine from 'faye-redis-ng'; (ES6 default)
|
|
8
|
+
// - import { Engine } from 'faye-redis-ng'; (ES6 named)
|
|
9
|
+
|
|
10
|
+
import Engine from './faye-redis';
|
|
11
|
+
|
|
12
|
+
// Export as CommonJS default (for backward compatibility)
|
|
13
|
+
export = Engine;
|
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;
|
package/.github/RELEASE.md
DELETED
|
@@ -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`
|