@noverachat/sdk-web 0.5.1 → 0.5.2
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/dist/index.cjs +22 -4
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -4
- package/package.json +1 -1
- package/src/transport/ws.ts +25 -4
package/dist/index.cjs
CHANGED
|
@@ -403,14 +403,26 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
403
403
|
// heartbeats and a stale typing event is worse than no event.
|
|
404
404
|
outboundQueue = [];
|
|
405
405
|
static OUTBOUND_QUEUE_CAP = 200;
|
|
406
|
+
/**
|
|
407
|
+
* 연결 시도의 세대 번호 — `connect()`/`close()` 마다 올린다.
|
|
408
|
+
*
|
|
409
|
+
* `openOnce()` 는 `urlBuilder()`(토큰 발급 왕복)를 await 하는 동안 아직
|
|
410
|
+
* 소켓이 없어서, 그 틈에 들어온 `close()` 는 닫을 대상을 찾지 못한다.
|
|
411
|
+
* 이어서 새 `connect()` 가 `explicitClose` 를 되돌리면, 뒤늦게 깨어난
|
|
412
|
+
* 앞선 시도가 **아무도 닫지 않는 소켓**을 하나 더 만든다(React StrictMode 의
|
|
413
|
+
* mount → cleanup → mount 에서 재현). 불리언 하나로는 "이미 취소됐다"를
|
|
414
|
+
* 알릴 수 없으므로 세대 번호로 판별한다.
|
|
415
|
+
*/
|
|
416
|
+
gen = 0;
|
|
406
417
|
get currentState() {
|
|
407
418
|
return this.state;
|
|
408
419
|
}
|
|
409
420
|
async connect() {
|
|
410
421
|
this.explicitClose = false;
|
|
411
|
-
await this.openOnce();
|
|
422
|
+
await this.openOnce(++this.gen);
|
|
412
423
|
}
|
|
413
424
|
close() {
|
|
425
|
+
this.gen++;
|
|
414
426
|
this.explicitClose = true;
|
|
415
427
|
this.setState("closed");
|
|
416
428
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
@@ -443,7 +455,8 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
443
455
|
);
|
|
444
456
|
return true;
|
|
445
457
|
}
|
|
446
|
-
async openOnce() {
|
|
458
|
+
async openOnce(gen) {
|
|
459
|
+
if (gen !== this.gen) return;
|
|
447
460
|
this.setState(this.attempt === 0 ? "connecting" : "reconnecting");
|
|
448
461
|
const WSImpl = this.opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
449
462
|
if (!WSImpl) throw new Error("No WebSocket implementation available");
|
|
@@ -452,7 +465,11 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
452
465
|
url = await this.opts.urlBuilder();
|
|
453
466
|
} catch (err) {
|
|
454
467
|
this.opts.logger?.("error", "url_builder_failed", err);
|
|
455
|
-
this.scheduleReconnect();
|
|
468
|
+
if (gen === this.gen) this.scheduleReconnect();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (gen !== this.gen) {
|
|
472
|
+
this.opts.logger?.("info", "ws_open_aborted_stale", { gen, current: this.gen });
|
|
456
473
|
return;
|
|
457
474
|
}
|
|
458
475
|
const ws = new WSImpl(url);
|
|
@@ -517,8 +534,9 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
517
534
|
const delay = exp + jitter;
|
|
518
535
|
this.opts.logger?.("info", "ws_reconnect_scheduled", { attempt: this.attempt, delay });
|
|
519
536
|
this.setState("reconnecting");
|
|
537
|
+
const gen = this.gen;
|
|
520
538
|
setTimeout(() => {
|
|
521
|
-
void this.openOnce();
|
|
539
|
+
void this.openOnce(gen);
|
|
522
540
|
}, delay);
|
|
523
541
|
}
|
|
524
542
|
startPings() {
|
package/dist/index.d.cts
CHANGED
|
@@ -666,6 +666,17 @@ declare class ReconnectingWs {
|
|
|
666
666
|
private explicitClose;
|
|
667
667
|
private outboundQueue;
|
|
668
668
|
private static readonly OUTBOUND_QUEUE_CAP;
|
|
669
|
+
/**
|
|
670
|
+
* 연결 시도의 세대 번호 — `connect()`/`close()` 마다 올린다.
|
|
671
|
+
*
|
|
672
|
+
* `openOnce()` 는 `urlBuilder()`(토큰 발급 왕복)를 await 하는 동안 아직
|
|
673
|
+
* 소켓이 없어서, 그 틈에 들어온 `close()` 는 닫을 대상을 찾지 못한다.
|
|
674
|
+
* 이어서 새 `connect()` 가 `explicitClose` 를 되돌리면, 뒤늦게 깨어난
|
|
675
|
+
* 앞선 시도가 **아무도 닫지 않는 소켓**을 하나 더 만든다(React StrictMode 의
|
|
676
|
+
* mount → cleanup → mount 에서 재현). 불리언 하나로는 "이미 취소됐다"를
|
|
677
|
+
* 알릴 수 없으므로 세대 번호로 판별한다.
|
|
678
|
+
*/
|
|
679
|
+
private gen;
|
|
669
680
|
constructor(opts: ReconnectingWsOptions);
|
|
670
681
|
get currentState(): WsState;
|
|
671
682
|
connect(): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -666,6 +666,17 @@ declare class ReconnectingWs {
|
|
|
666
666
|
private explicitClose;
|
|
667
667
|
private outboundQueue;
|
|
668
668
|
private static readonly OUTBOUND_QUEUE_CAP;
|
|
669
|
+
/**
|
|
670
|
+
* 연결 시도의 세대 번호 — `connect()`/`close()` 마다 올린다.
|
|
671
|
+
*
|
|
672
|
+
* `openOnce()` 는 `urlBuilder()`(토큰 발급 왕복)를 await 하는 동안 아직
|
|
673
|
+
* 소켓이 없어서, 그 틈에 들어온 `close()` 는 닫을 대상을 찾지 못한다.
|
|
674
|
+
* 이어서 새 `connect()` 가 `explicitClose` 를 되돌리면, 뒤늦게 깨어난
|
|
675
|
+
* 앞선 시도가 **아무도 닫지 않는 소켓**을 하나 더 만든다(React StrictMode 의
|
|
676
|
+
* mount → cleanup → mount 에서 재현). 불리언 하나로는 "이미 취소됐다"를
|
|
677
|
+
* 알릴 수 없으므로 세대 번호로 판별한다.
|
|
678
|
+
*/
|
|
679
|
+
private gen;
|
|
669
680
|
constructor(opts: ReconnectingWsOptions);
|
|
670
681
|
get currentState(): WsState;
|
|
671
682
|
connect(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -368,14 +368,26 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
368
368
|
// heartbeats and a stale typing event is worse than no event.
|
|
369
369
|
outboundQueue = [];
|
|
370
370
|
static OUTBOUND_QUEUE_CAP = 200;
|
|
371
|
+
/**
|
|
372
|
+
* 연결 시도의 세대 번호 — `connect()`/`close()` 마다 올린다.
|
|
373
|
+
*
|
|
374
|
+
* `openOnce()` 는 `urlBuilder()`(토큰 발급 왕복)를 await 하는 동안 아직
|
|
375
|
+
* 소켓이 없어서, 그 틈에 들어온 `close()` 는 닫을 대상을 찾지 못한다.
|
|
376
|
+
* 이어서 새 `connect()` 가 `explicitClose` 를 되돌리면, 뒤늦게 깨어난
|
|
377
|
+
* 앞선 시도가 **아무도 닫지 않는 소켓**을 하나 더 만든다(React StrictMode 의
|
|
378
|
+
* mount → cleanup → mount 에서 재현). 불리언 하나로는 "이미 취소됐다"를
|
|
379
|
+
* 알릴 수 없으므로 세대 번호로 판별한다.
|
|
380
|
+
*/
|
|
381
|
+
gen = 0;
|
|
371
382
|
get currentState() {
|
|
372
383
|
return this.state;
|
|
373
384
|
}
|
|
374
385
|
async connect() {
|
|
375
386
|
this.explicitClose = false;
|
|
376
|
-
await this.openOnce();
|
|
387
|
+
await this.openOnce(++this.gen);
|
|
377
388
|
}
|
|
378
389
|
close() {
|
|
390
|
+
this.gen++;
|
|
379
391
|
this.explicitClose = true;
|
|
380
392
|
this.setState("closed");
|
|
381
393
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
@@ -408,7 +420,8 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
408
420
|
);
|
|
409
421
|
return true;
|
|
410
422
|
}
|
|
411
|
-
async openOnce() {
|
|
423
|
+
async openOnce(gen) {
|
|
424
|
+
if (gen !== this.gen) return;
|
|
412
425
|
this.setState(this.attempt === 0 ? "connecting" : "reconnecting");
|
|
413
426
|
const WSImpl = this.opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
414
427
|
if (!WSImpl) throw new Error("No WebSocket implementation available");
|
|
@@ -417,7 +430,11 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
417
430
|
url = await this.opts.urlBuilder();
|
|
418
431
|
} catch (err) {
|
|
419
432
|
this.opts.logger?.("error", "url_builder_failed", err);
|
|
420
|
-
this.scheduleReconnect();
|
|
433
|
+
if (gen === this.gen) this.scheduleReconnect();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (gen !== this.gen) {
|
|
437
|
+
this.opts.logger?.("info", "ws_open_aborted_stale", { gen, current: this.gen });
|
|
421
438
|
return;
|
|
422
439
|
}
|
|
423
440
|
const ws = new WSImpl(url);
|
|
@@ -482,8 +499,9 @@ var ReconnectingWs = class _ReconnectingWs {
|
|
|
482
499
|
const delay = exp + jitter;
|
|
483
500
|
this.opts.logger?.("info", "ws_reconnect_scheduled", { attempt: this.attempt, delay });
|
|
484
501
|
this.setState("reconnecting");
|
|
502
|
+
const gen = this.gen;
|
|
485
503
|
setTimeout(() => {
|
|
486
|
-
void this.openOnce();
|
|
504
|
+
void this.openOnce(gen);
|
|
487
505
|
}, delay);
|
|
488
506
|
}
|
|
489
507
|
startPings() {
|
package/package.json
CHANGED
package/src/transport/ws.ts
CHANGED
|
@@ -41,6 +41,17 @@ export class ReconnectingWs {
|
|
|
41
41
|
// heartbeats and a stale typing event is worse than no event.
|
|
42
42
|
private outboundQueue: WsOutbound[] = [];
|
|
43
43
|
private static readonly OUTBOUND_QUEUE_CAP = 200;
|
|
44
|
+
/**
|
|
45
|
+
* 연결 시도의 세대 번호 — `connect()`/`close()` 마다 올린다.
|
|
46
|
+
*
|
|
47
|
+
* `openOnce()` 는 `urlBuilder()`(토큰 발급 왕복)를 await 하는 동안 아직
|
|
48
|
+
* 소켓이 없어서, 그 틈에 들어온 `close()` 는 닫을 대상을 찾지 못한다.
|
|
49
|
+
* 이어서 새 `connect()` 가 `explicitClose` 를 되돌리면, 뒤늦게 깨어난
|
|
50
|
+
* 앞선 시도가 **아무도 닫지 않는 소켓**을 하나 더 만든다(React StrictMode 의
|
|
51
|
+
* mount → cleanup → mount 에서 재현). 불리언 하나로는 "이미 취소됐다"를
|
|
52
|
+
* 알릴 수 없으므로 세대 번호로 판별한다.
|
|
53
|
+
*/
|
|
54
|
+
private gen = 0;
|
|
44
55
|
|
|
45
56
|
constructor(private opts: ReconnectingWsOptions) {}
|
|
46
57
|
|
|
@@ -50,10 +61,11 @@ export class ReconnectingWs {
|
|
|
50
61
|
|
|
51
62
|
async connect(): Promise<void> {
|
|
52
63
|
this.explicitClose = false;
|
|
53
|
-
await this.openOnce();
|
|
64
|
+
await this.openOnce(++this.gen);
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
close(): void {
|
|
68
|
+
this.gen++; // 진행 중인 연결 시도를 모두 무효화한다
|
|
57
69
|
this.explicitClose = true;
|
|
58
70
|
this.setState("closed");
|
|
59
71
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
@@ -91,7 +103,8 @@ export class ReconnectingWs {
|
|
|
91
103
|
return true;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
private async openOnce(): Promise<void> {
|
|
106
|
+
private async openOnce(gen: number): Promise<void> {
|
|
107
|
+
if (gen !== this.gen) return;
|
|
95
108
|
this.setState(this.attempt === 0 ? "connecting" : "reconnecting");
|
|
96
109
|
const WSImpl = this.opts.WebSocketImpl ?? (globalThis.WebSocket as typeof WebSocket);
|
|
97
110
|
if (!WSImpl) throw new Error("No WebSocket implementation available");
|
|
@@ -101,7 +114,14 @@ export class ReconnectingWs {
|
|
|
101
114
|
url = await this.opts.urlBuilder();
|
|
102
115
|
} catch (err) {
|
|
103
116
|
this.opts.logger?.("error", "url_builder_failed", err);
|
|
104
|
-
this.scheduleReconnect();
|
|
117
|
+
if (gen === this.gen) this.scheduleReconnect();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// await 사이에 close()/connect() 가 지나갔으면 이 시도는 이미 취소된
|
|
122
|
+
// 것이다 — 소켓을 만들지 않고 물러난다(누수 소켓 방지).
|
|
123
|
+
if (gen !== this.gen) {
|
|
124
|
+
this.opts.logger?.("info", "ws_open_aborted_stale", { gen, current: this.gen });
|
|
105
125
|
return;
|
|
106
126
|
}
|
|
107
127
|
|
|
@@ -175,8 +195,9 @@ export class ReconnectingWs {
|
|
|
175
195
|
const delay = exp + jitter;
|
|
176
196
|
this.opts.logger?.("info", "ws_reconnect_scheduled", { attempt: this.attempt, delay });
|
|
177
197
|
this.setState("reconnecting");
|
|
198
|
+
const gen = this.gen; // 예약 시점의 세대 — 그사이 close/connect 가 오면 취소
|
|
178
199
|
setTimeout(() => {
|
|
179
|
-
void this.openOnce();
|
|
200
|
+
void this.openOnce(gen);
|
|
180
201
|
}, delay);
|
|
181
202
|
}
|
|
182
203
|
|