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.
- package/README.md +7 -4
- 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/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 +21 -4
- package/{faye-redis.js → src/faye-redis.ts} +111 -73
- 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
|
});
|
|
@@ -69,23 +102,27 @@ class Engine {
|
|
|
69
102
|
|
|
70
103
|
this._subscriber.on('ready', async () => {
|
|
71
104
|
console.log('Redis subscriber ready');
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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
|
|
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
|
|
205
|
+
const channels = await this._redis!.sMembers(this._ns + '/clients/' + clientId + '/channels');
|
|
164
206
|
|
|
165
|
-
const multi = this._redis
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
255
|
-
await this._redis
|
|
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
|
|
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
|
|
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
|
|
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
|
|
370
|
+
await this._redis!.del(lockKey);
|
|
329
371
|
}
|
|
330
372
|
};
|
|
331
373
|
|
|
332
|
-
const set = await this._redis
|
|
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
|
|
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
|
|
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
|
-
|
|
354
|
-
Engine
|
|
355
|
-
|
|
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;
|
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`
|