@lemoncloud/chatic-sockets-lib 0.1.0 → 0.2.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/dist/client-socket-v2/connection-rotation-controller.d.ts +4 -0
- package/dist/client-socket-v2/connection-rotation-controller.js +9 -3
- package/dist/client-socket-v2/create-client-socket-v2.js +150 -27
- package/dist/client-socket-v2/index.d.ts +2 -0
- package/dist/client-socket-v2/index.js +2 -0
- package/dist/client-socket-v2/keep-alive-loop.d.ts +5 -7
- package/dist/client-socket-v2/keep-alive-loop.js +32 -9
- package/dist/client-socket-v2/message-router.d.ts +1 -0
- package/dist/client-socket-v2/message-router.js +4 -0
- package/dist/client-socket-v2/reconnect-controller.d.ts +23 -7
- package/dist/client-socket-v2/reconnect-controller.js +72 -9
- package/dist/client-socket-v2/socket-runtime.js +14 -4
- package/dist/client-socket-v2/socket-transport.d.ts +8 -1
- package/dist/client-socket-v2/socket-transport.js +30 -4
- package/dist/client-socket-v2/types.d.ts +29 -1
- package/dist/lib/auth/types.d.ts +36 -0
- package/dist/lib/auth/types.js +2 -0
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@ export interface ConnectionRotationControllerOptions {
|
|
|
4
4
|
reconnect?: ReconnectController;
|
|
5
5
|
maxLifetimeMs?: number;
|
|
6
6
|
refreshBeforeMs?: number;
|
|
7
|
+
/** rotation delay에 적용할 ±jitter 비율 (0~1). 동시 부트스트랩 클라이언트군의 일제 rotate burst 분산용. 기본 0.1 (±10%) */
|
|
8
|
+
jitterRatio?: number;
|
|
7
9
|
timerScheduler?: SharedTimerScheduler;
|
|
8
10
|
timerKey?: string;
|
|
9
11
|
}
|
|
@@ -11,6 +13,7 @@ export declare class ConnectionRotationController {
|
|
|
11
13
|
private readonly options;
|
|
12
14
|
private readonly maxLifetimeMs;
|
|
13
15
|
private readonly refreshBeforeMs;
|
|
16
|
+
private readonly jitterRatio;
|
|
14
17
|
private readonly timerScheduler?;
|
|
15
18
|
private readonly timerKey;
|
|
16
19
|
private readonly unsubs;
|
|
@@ -21,6 +24,7 @@ export declare class ConnectionRotationController {
|
|
|
21
24
|
stop: () => void;
|
|
22
25
|
destroy: () => void;
|
|
23
26
|
private scheduleRotation;
|
|
27
|
+
private computeRotationDelay;
|
|
24
28
|
private rotate;
|
|
25
29
|
private clearTimer;
|
|
26
30
|
}
|
|
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.ConnectionRotationController = void 0;
|
|
13
13
|
class ConnectionRotationController {
|
|
14
14
|
constructor(options) {
|
|
15
|
-
var _a, _b, _c;
|
|
15
|
+
var _a, _b, _c, _d;
|
|
16
16
|
this.options = options;
|
|
17
17
|
this.unsubs = [];
|
|
18
18
|
this.active = false;
|
|
@@ -33,13 +33,18 @@ class ConnectionRotationController {
|
|
|
33
33
|
this.clearTimer();
|
|
34
34
|
if (!this.active)
|
|
35
35
|
return;
|
|
36
|
-
const delayMs =
|
|
36
|
+
const delayMs = this.computeRotationDelay();
|
|
37
37
|
if (this.timerScheduler) {
|
|
38
38
|
this.timerScheduler.schedule(this.timerKey, delayMs, () => this.rotate());
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
this.timer = setTimeout(() => void this.rotate(), delayMs);
|
|
42
42
|
};
|
|
43
|
+
this.computeRotationDelay = () => {
|
|
44
|
+
const baseDelay = Math.max(0, this.maxLifetimeMs - this.refreshBeforeMs);
|
|
45
|
+
const jitter = this.jitterRatio > 0 ? baseDelay * (Math.random() * 2 - 1) * this.jitterRatio : 0;
|
|
46
|
+
return Math.max(0, baseDelay + jitter);
|
|
47
|
+
};
|
|
43
48
|
this.rotate = () => __awaiter(this, void 0, void 0, function* () {
|
|
44
49
|
if (!this.active)
|
|
45
50
|
return;
|
|
@@ -64,8 +69,9 @@ class ConnectionRotationController {
|
|
|
64
69
|
};
|
|
65
70
|
this.maxLifetimeMs = (_a = options.maxLifetimeMs) !== null && _a !== void 0 ? _a : 1000 * 60 * 110;
|
|
66
71
|
this.refreshBeforeMs = (_b = options.refreshBeforeMs) !== null && _b !== void 0 ? _b : 1000 * 60 * 10;
|
|
72
|
+
this.jitterRatio = Math.max(0, Math.min(1, (_c = options.jitterRatio) !== null && _c !== void 0 ? _c : 0.1));
|
|
67
73
|
this.timerScheduler = options.timerScheduler;
|
|
68
|
-
this.timerKey = (
|
|
74
|
+
this.timerKey = (_d = options.timerKey) !== null && _d !== void 0 ? _d : 'rotation';
|
|
69
75
|
this.unsubs.push(options.client.onState(event => {
|
|
70
76
|
if (!this.active)
|
|
71
77
|
return;
|
|
@@ -11,8 +11,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.createClientSocketV2 = void 0;
|
|
13
13
|
const common_1 = require("./common");
|
|
14
|
+
const keep_alive_loop_1 = require("./keep-alive-loop");
|
|
14
15
|
const message_router_1 = require("./message-router");
|
|
15
16
|
const pending_request_store_1 = require("./pending-request-store");
|
|
17
|
+
const reconnect_controller_1 = require("./reconnect-controller");
|
|
16
18
|
const shared_timer_scheduler_1 = require("./shared-timer-scheduler");
|
|
17
19
|
const socket_transport_1 = require("./socket-transport");
|
|
18
20
|
const buildMid = () => `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -28,16 +30,29 @@ const parseMessage = (raw) => {
|
|
|
28
30
|
}
|
|
29
31
|
return (0, common_1.onlyDefined)(Object.assign(Object.assign({}, message), { type: (0, common_1.cleanString)(message.type) }));
|
|
30
32
|
};
|
|
33
|
+
const normalizeRequestCap = (value, fallback) => {
|
|
34
|
+
const n = value !== null && value !== void 0 ? value : fallback;
|
|
35
|
+
return n > 0 ? n : Number.POSITIVE_INFINITY;
|
|
36
|
+
};
|
|
37
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
38
|
+
const DEFAULT_MAX_INFLIGHT_REQUESTS = 32;
|
|
39
|
+
const DEFAULT_MAX_PENDING_REQUESTS = 512;
|
|
31
40
|
class ClientSocketV2Impl {
|
|
32
41
|
constructor(options) {
|
|
33
|
-
var _a;
|
|
42
|
+
var _a, _b;
|
|
34
43
|
this.options = options;
|
|
35
44
|
this.router = new message_router_1.MessageRouter();
|
|
36
45
|
this.stateListeners = new Set();
|
|
37
46
|
this.errorListeners = new Set();
|
|
38
47
|
this.messageListeners = new Set();
|
|
48
|
+
this.requestQueue = [];
|
|
49
|
+
this.inflightRequests = 0;
|
|
50
|
+
this.transportUnsubs = [];
|
|
39
51
|
this._state = 'idle';
|
|
52
|
+
/** reconnect.stop이 client.disconnect를 호출하므로 재귀 진입 차단용 */
|
|
53
|
+
this.disconnecting = false;
|
|
40
54
|
this.connect = () => __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _c;
|
|
41
56
|
if (this._state === 'connected' || this._state === 'connecting')
|
|
42
57
|
return;
|
|
43
58
|
this.setState('connecting');
|
|
@@ -49,10 +64,27 @@ class ClientSocketV2Impl {
|
|
|
49
64
|
this.setState('closed');
|
|
50
65
|
throw error;
|
|
51
66
|
}
|
|
67
|
+
finally {
|
|
68
|
+
void ((_c = this.reconnect) === null || _c === void 0 ? void 0 : _c.start().catch(() => undefined));
|
|
69
|
+
}
|
|
52
70
|
});
|
|
53
71
|
this.disconnect = (code, reason) => __awaiter(this, void 0, void 0, function* () {
|
|
54
72
|
if (this._state === 'closed' || this._state === 'idle')
|
|
55
73
|
return;
|
|
74
|
+
if (this.reconnect && !this.disconnecting) {
|
|
75
|
+
this.disconnecting = true;
|
|
76
|
+
try {
|
|
77
|
+
yield this.reconnect.stop(code, reason);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.emitError({ error, phase: 'disconnect' });
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.disconnecting = false;
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
56
88
|
this.setState('closing');
|
|
57
89
|
try {
|
|
58
90
|
yield this.transport.disconnect(code, reason);
|
|
@@ -73,26 +105,22 @@ class ClientSocketV2Impl {
|
|
|
73
105
|
this.transport.send(JSON.stringify(message));
|
|
74
106
|
};
|
|
75
107
|
this.request = (type, data, options) => __awaiter(this, void 0, void 0, function* () {
|
|
76
|
-
var
|
|
77
|
-
const mid = (
|
|
108
|
+
var _d, _e, _f, _g;
|
|
109
|
+
const mid = (_f = (_e = (_d = this.options).createMid) === null || _e === void 0 ? void 0 : _e.call(_d)) !== null && _f !== void 0 ? _f : buildMid();
|
|
78
110
|
const canonicalType = this.normalizeType(type);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
this.transport.send(JSON.stringify((0, common_1.onlyDefined)({
|
|
85
|
-
type: canonicalType,
|
|
86
|
-
data: (_f = data) !== null && _f !== void 0 ? _f : null,
|
|
87
|
-
mid,
|
|
88
|
-
})));
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
this.pending.reject(mid, error);
|
|
92
|
-
this.emitError({ error, phase: 'request' });
|
|
93
|
-
throw error;
|
|
111
|
+
const timeoutMs = (_g = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _g !== void 0 ? _g : this.requestTimeoutMs;
|
|
112
|
+
const totalAccepted = this.inflightRequests + this.requestQueue.length;
|
|
113
|
+
if (totalAccepted >= this.maxPendingRequests) {
|
|
114
|
+
throw new Error(`429 TOO MANY REQUESTS - pending queue full (${totalAccepted}/${this.maxPendingRequests})`);
|
|
94
115
|
}
|
|
95
|
-
return
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const job = { mid, type: canonicalType, data, timeoutMs, resolve, reject };
|
|
118
|
+
if (this.inflightRequests < this.maxInflightRequests) {
|
|
119
|
+
this.dispatchRequest(job);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.requestQueue.push(job);
|
|
123
|
+
});
|
|
96
124
|
});
|
|
97
125
|
this.onState = (listener) => {
|
|
98
126
|
this.stateListeners.add(listener);
|
|
@@ -107,6 +135,80 @@ class ClientSocketV2Impl {
|
|
|
107
135
|
return () => this.messageListeners.delete(listener);
|
|
108
136
|
};
|
|
109
137
|
this.onType = (type, listener) => this.router.onType(type, listener);
|
|
138
|
+
this.destroy = () => {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
/** 순서 invariant: reconnect.destroy → keepAlive.destroy → transport.disconnect (그 close 이벤트를 reconnect가 못 듣게 하기 위함) → 나머지 정리 */
|
|
141
|
+
(_a = this.reconnect) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
142
|
+
(_b = this.keepAlive) === null || _b === void 0 ? void 0 : _b.destroy();
|
|
143
|
+
void this.transport.disconnect().catch(() => undefined);
|
|
144
|
+
this.transportUnsubs.splice(0).forEach(unsub => unsub());
|
|
145
|
+
this.rejectQueuedRequests(new Error(`499 CLIENT CLOSED REQUEST - client destroyed`));
|
|
146
|
+
this.pending.clear(new Error(`499 CLIENT CLOSED REQUEST - client destroyed`));
|
|
147
|
+
this.router.clear();
|
|
148
|
+
this.stateListeners.clear();
|
|
149
|
+
this.errorListeners.clear();
|
|
150
|
+
this.messageListeners.clear();
|
|
151
|
+
if (this.ownsTimerScheduler)
|
|
152
|
+
this.timerScheduler.cancelAll();
|
|
153
|
+
this._state = 'closed';
|
|
154
|
+
};
|
|
155
|
+
this.dispatchRequest = (job) => {
|
|
156
|
+
var _a;
|
|
157
|
+
this.inflightRequests += 1;
|
|
158
|
+
let released = false;
|
|
159
|
+
const release = () => {
|
|
160
|
+
if (released)
|
|
161
|
+
return;
|
|
162
|
+
released = true;
|
|
163
|
+
this.inflightRequests = Math.max(0, this.inflightRequests - 1);
|
|
164
|
+
this.dispatchNextQueuedRequest();
|
|
165
|
+
};
|
|
166
|
+
let pending;
|
|
167
|
+
try {
|
|
168
|
+
pending = this.pending.create(job.mid, {
|
|
169
|
+
timeoutMs: job.timeoutMs,
|
|
170
|
+
onTimeout: (requestMid) => new Error(`408 REQUEST TIMEOUT - ${job.type}[${requestMid}]`),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
release();
|
|
175
|
+
job.reject(error);
|
|
176
|
+
this.emitError({ error, phase: 'request' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
void pending.then(value => {
|
|
180
|
+
job.resolve(value);
|
|
181
|
+
release();
|
|
182
|
+
}, error => {
|
|
183
|
+
job.reject(error);
|
|
184
|
+
release();
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
this.transport.send(JSON.stringify((0, common_1.onlyDefined)({
|
|
188
|
+
type: job.type,
|
|
189
|
+
data: (_a = job.data) !== null && _a !== void 0 ? _a : null,
|
|
190
|
+
mid: job.mid,
|
|
191
|
+
})));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.pending.reject(job.mid, error);
|
|
195
|
+
release();
|
|
196
|
+
this.emitError({ error, phase: 'request' });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
this.dispatchNextQueuedRequest = () => {
|
|
200
|
+
while (this.inflightRequests < this.maxInflightRequests && this.requestQueue.length) {
|
|
201
|
+
const next = this.requestQueue.shift();
|
|
202
|
+
if (!next)
|
|
203
|
+
return;
|
|
204
|
+
this.dispatchRequest(next);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
this.rejectQueuedRequests = (reason) => {
|
|
208
|
+
const queued = this.requestQueue.splice(0);
|
|
209
|
+
queued.forEach(job => job.reject(reason));
|
|
210
|
+
return queued.length;
|
|
211
|
+
};
|
|
110
212
|
this.normalizeType = (type) => {
|
|
111
213
|
var _a;
|
|
112
214
|
const source = (0, common_1.cleanString)(type);
|
|
@@ -127,6 +229,12 @@ class ClientSocketV2Impl {
|
|
|
127
229
|
this.emitError({ error, phase: 'message', raw });
|
|
128
230
|
}
|
|
129
231
|
};
|
|
232
|
+
this.closeTransportInternal = (code, reason) => {
|
|
233
|
+
if (this._state === 'closed' || this._state === 'idle')
|
|
234
|
+
return;
|
|
235
|
+
this.setState('closing');
|
|
236
|
+
void this.transport.disconnect(code, reason).catch(() => undefined);
|
|
237
|
+
};
|
|
130
238
|
this.setState = (next) => {
|
|
131
239
|
const prev = this._state;
|
|
132
240
|
this._state = next;
|
|
@@ -139,22 +247,37 @@ class ClientSocketV2Impl {
|
|
|
139
247
|
this.errorListeners.forEach(listener => listener(event));
|
|
140
248
|
};
|
|
141
249
|
this.deviceDefaults = options.device ? (0, common_1.omitKeys)(options.device, ['tick']) : undefined;
|
|
142
|
-
this.
|
|
250
|
+
this.requestTimeoutMs = (_a = options.requestTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
251
|
+
this.maxInflightRequests = normalizeRequestCap(options.maxInflightRequests, DEFAULT_MAX_INFLIGHT_REQUESTS);
|
|
252
|
+
this.maxPendingRequests = normalizeRequestCap(options.maxPendingRequests, DEFAULT_MAX_PENDING_REQUESTS);
|
|
253
|
+
this.ownsTimerScheduler = !options.timerScheduler;
|
|
254
|
+
this.timerScheduler =
|
|
255
|
+
(_b = options.timerScheduler) !== null && _b !== void 0 ? _b : new shared_timer_scheduler_1.InMemorySharedTimerScheduler(options.now ? { now: options.now } : {});
|
|
143
256
|
this.pending = new pending_request_store_1.PendingRequestStore({
|
|
144
257
|
timerScheduler: this.timerScheduler,
|
|
145
258
|
timerPrefix: 'pending:',
|
|
146
259
|
});
|
|
147
260
|
this.aliases = Object.assign({ ping: 'system.ping' }, (options.aliases || {}));
|
|
148
|
-
this.transport = new socket_transport_1.WebSocketTransport(options.url, options.protocols, options.socketFactory
|
|
149
|
-
|
|
150
|
-
|
|
261
|
+
this.transport = new socket_transport_1.WebSocketTransport(options.url, options.protocols, options.socketFactory, {
|
|
262
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
263
|
+
});
|
|
264
|
+
this.transportUnsubs.push(this.transport.on('open', () => this.setState('connected')));
|
|
265
|
+
this.transportUnsubs.push(this.transport.on('close', () => {
|
|
266
|
+
this.rejectQueuedRequests(new Error(`499 CLIENT CLOSED REQUEST - socket closed`));
|
|
151
267
|
this.pending.clear(new Error(`499 CLIENT CLOSED REQUEST - socket closed`));
|
|
152
268
|
this.setState('closed');
|
|
153
|
-
});
|
|
154
|
-
this.transport.on('error', event => {
|
|
269
|
+
}));
|
|
270
|
+
this.transportUnsubs.push(this.transport.on('error', event => {
|
|
155
271
|
this.emitError({ error: event === null || event === void 0 ? void 0 : event.error, phase: 'transport' });
|
|
156
|
-
});
|
|
157
|
-
this.transport.on('message', event => this.handleRawMessage(event.data));
|
|
272
|
+
}));
|
|
273
|
+
this.transportUnsubs.push(this.transport.on('message', event => this.handleRawMessage(event.data)));
|
|
274
|
+
if (options.keepAlive !== false) {
|
|
275
|
+
this.keepAlive = new keep_alive_loop_1.KeepAliveLoop(Object.assign({ client: this, timerScheduler: this.timerScheduler, onPongTimeout: () => this.closeTransportInternal(1001, 'pong-timeout') }, (options.keepAlive || {})));
|
|
276
|
+
this.keepAlive.start();
|
|
277
|
+
}
|
|
278
|
+
if (options.reconnect !== false) {
|
|
279
|
+
this.reconnect = new reconnect_controller_1.AutoReconnectController(Object.assign({ client: this, timerScheduler: this.timerScheduler }, (options.reconnect || {})));
|
|
280
|
+
}
|
|
158
281
|
}
|
|
159
282
|
get state() {
|
|
160
283
|
return this._state;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../lib/sockets/packets';
|
|
2
2
|
import '../lib/device/types';
|
|
3
|
+
import '../lib/auth/types';
|
|
3
4
|
export * from './types';
|
|
4
5
|
export * from './common';
|
|
5
6
|
export { createClientSocketV2 as default } from './create-client-socket-v2';
|
|
@@ -21,3 +22,4 @@ export * from './gateways/device-gateway';
|
|
|
21
22
|
export * from '../lib/types';
|
|
22
23
|
export * from '../lib/device/types';
|
|
23
24
|
export * from '../lib/device/contracts';
|
|
25
|
+
export * from '../lib/auth/types';
|
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.default = void 0;
|
|
18
18
|
require("../lib/sockets/packets");
|
|
19
19
|
require("../lib/device/types");
|
|
20
|
+
require("../lib/auth/types");
|
|
20
21
|
__exportStar(require("./types"), exports);
|
|
21
22
|
__exportStar(require("./common"), exports);
|
|
22
23
|
var create_client_socket_v2_1 = require("./create-client-socket-v2");
|
|
@@ -39,3 +40,4 @@ __exportStar(require("./gateways/device-gateway"), exports);
|
|
|
39
40
|
__exportStar(require("../lib/types"), exports);
|
|
40
41
|
__exportStar(require("../lib/device/types"), exports);
|
|
41
42
|
__exportStar(require("../lib/device/contracts"), exports);
|
|
43
|
+
__exportStar(require("../lib/auth/types"), exports);
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import type { KeepAliveLoopControl, ClientSocketV2, SharedTimerScheduler } from './types';
|
|
2
|
-
export interface KeepAliveLoopOptions {
|
|
1
|
+
import type { KeepAliveLoopControl, ClientSocketV2, SharedTimerScheduler, KeepAliveLoopOptionsPartial } from './types';
|
|
2
|
+
export interface KeepAliveLoopOptions extends KeepAliveLoopOptionsPartial {
|
|
3
3
|
client: ClientSocketV2;
|
|
4
|
-
intervalMs?: number;
|
|
5
|
-
timeoutMs?: number;
|
|
6
|
-
buildPayload?: () => unknown;
|
|
7
|
-
mode?: 'send' | 'request';
|
|
8
4
|
timerScheduler?: SharedTimerScheduler;
|
|
9
|
-
timerKey?: string;
|
|
10
5
|
}
|
|
11
6
|
export declare class KeepAliveLoop implements KeepAliveLoopControl {
|
|
12
7
|
private readonly options;
|
|
13
8
|
private readonly intervalMs;
|
|
9
|
+
private readonly pingTimeoutMs;
|
|
10
|
+
private readonly maxMissedPongs;
|
|
14
11
|
private readonly mode;
|
|
15
12
|
private readonly timerScheduler?;
|
|
16
13
|
private readonly timerKey;
|
|
@@ -18,6 +15,7 @@ export declare class KeepAliveLoop implements KeepAliveLoopControl {
|
|
|
18
15
|
private timer?;
|
|
19
16
|
private running;
|
|
20
17
|
private inFlight?;
|
|
18
|
+
private missedPongs;
|
|
21
19
|
constructor(options: KeepAliveLoopOptions);
|
|
22
20
|
start: () => void;
|
|
23
21
|
stop: () => void;
|
|
@@ -10,12 +10,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.KeepAliveLoop = void 0;
|
|
13
|
+
const DEFAULT_PING_TIMEOUT_MS = 10000;
|
|
14
|
+
const DEFAULT_MAX_MISSED_PONGS = 2;
|
|
13
15
|
class KeepAliveLoop {
|
|
14
16
|
constructor(options) {
|
|
15
|
-
var _a, _b, _c;
|
|
17
|
+
var _a, _b, _c, _d, _e;
|
|
16
18
|
this.options = options;
|
|
17
19
|
this.unsubs = [];
|
|
18
20
|
this.running = false;
|
|
21
|
+
this.missedPongs = 0;
|
|
19
22
|
this.start = () => {
|
|
20
23
|
this.running = true;
|
|
21
24
|
if (this.options.client.state === 'connected')
|
|
@@ -55,12 +58,12 @@ class KeepAliveLoop {
|
|
|
55
58
|
this.timer = undefined;
|
|
56
59
|
};
|
|
57
60
|
this.tick = () => __awaiter(this, void 0, void 0, function* () {
|
|
58
|
-
var
|
|
61
|
+
var _f, _g, _h;
|
|
59
62
|
if (!this.running || this.options.client.state !== 'connected')
|
|
60
63
|
return;
|
|
61
64
|
if (this.inFlight)
|
|
62
65
|
return;
|
|
63
|
-
const payload = (
|
|
66
|
+
const payload = (_h = (_g = (_f = this.options).buildPayload) === null || _g === void 0 ? void 0 : _g.call(_f)) !== null && _h !== void 0 ? _h : null;
|
|
64
67
|
this.inFlight = Promise.resolve()
|
|
65
68
|
.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
66
69
|
if (this.mode === 'send') {
|
|
@@ -68,10 +71,24 @@ class KeepAliveLoop {
|
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
yield this.options.client.request('system.ping', payload, {
|
|
71
|
-
timeoutMs: this.
|
|
74
|
+
timeoutMs: this.pingTimeoutMs,
|
|
72
75
|
});
|
|
76
|
+
this.missedPongs = 0;
|
|
77
|
+
}))
|
|
78
|
+
.catch(() => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
if (this.mode !== 'request')
|
|
80
|
+
return;
|
|
81
|
+
this.missedPongs += 1;
|
|
82
|
+
if (this.missedPongs < this.maxMissedPongs)
|
|
83
|
+
return;
|
|
84
|
+
this.missedPongs = 0;
|
|
85
|
+
if (this.options.onPongTimeout) {
|
|
86
|
+
yield Promise.resolve(this.options.onPongTimeout()).catch(() => undefined);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
yield this.options.client.disconnect(1001, 'pong-timeout').catch(() => undefined);
|
|
90
|
+
}
|
|
73
91
|
}))
|
|
74
|
-
.catch(() => undefined)
|
|
75
92
|
.then(() => {
|
|
76
93
|
this.inFlight = undefined;
|
|
77
94
|
this.scheduleNext();
|
|
@@ -79,14 +96,20 @@ class KeepAliveLoop {
|
|
|
79
96
|
yield this.inFlight;
|
|
80
97
|
});
|
|
81
98
|
this.intervalMs = (_a = options.intervalMs) !== null && _a !== void 0 ? _a : 30000;
|
|
82
|
-
this.
|
|
99
|
+
this.pingTimeoutMs = (_b = options.timeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_PING_TIMEOUT_MS;
|
|
100
|
+
this.maxMissedPongs = Math.max(1, (_c = options.maxMissedPongs) !== null && _c !== void 0 ? _c : DEFAULT_MAX_MISSED_PONGS);
|
|
101
|
+
this.mode = (_d = options.mode) !== null && _d !== void 0 ? _d : 'request';
|
|
83
102
|
this.timerScheduler = options.timerScheduler;
|
|
84
|
-
this.timerKey = (
|
|
103
|
+
this.timerKey = (_e = options.timerKey) !== null && _e !== void 0 ? _e : 'keepalive';
|
|
85
104
|
this.unsubs.push(options.client.onState(event => {
|
|
86
|
-
if (event.next === 'connected' && this.running)
|
|
105
|
+
if (event.next === 'connected' && this.running) {
|
|
106
|
+
this.missedPongs = 0;
|
|
87
107
|
this.scheduleNow();
|
|
88
|
-
|
|
108
|
+
}
|
|
109
|
+
if (event.next === 'closing' || event.next === 'closed') {
|
|
110
|
+
this.missedPongs = 0;
|
|
89
111
|
this.clearTimer();
|
|
112
|
+
}
|
|
90
113
|
}));
|
|
91
114
|
}
|
|
92
115
|
}
|
|
@@ -5,5 +5,6 @@ export declare class MessageRouter {
|
|
|
5
5
|
private readonly typeListeners;
|
|
6
6
|
onAny: (listener: MessageListener) => (() => void);
|
|
7
7
|
onType: <T = any>(type: string, listener: MessageListener<T>) => (() => void);
|
|
8
|
+
clear: () => void;
|
|
8
9
|
route: (message: SocketMessage<any>) => number;
|
|
9
10
|
}
|
|
@@ -23,6 +23,10 @@ class MessageRouter {
|
|
|
23
23
|
this.typeListeners.delete(key);
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
|
+
this.clear = () => {
|
|
27
|
+
this.anyListeners.clear();
|
|
28
|
+
this.typeListeners.clear();
|
|
29
|
+
};
|
|
26
30
|
this.route = (message) => {
|
|
27
31
|
var _a;
|
|
28
32
|
const key = `${(_a = message === null || message === void 0 ? void 0 : message.type) !== null && _a !== void 0 ? _a : ''}`.trim();
|
|
@@ -1,32 +1,48 @@
|
|
|
1
|
-
import type { ClientSocketV2, ReconnectController, SharedTimerScheduler } from './types';
|
|
2
|
-
export interface
|
|
1
|
+
import type { AutoReconnectOptionsPartial, ClientSocketV2, ReconnectController, SharedTimerScheduler } from './types';
|
|
2
|
+
export interface AutoReconnectGiveUpEvent {
|
|
3
|
+
attempts: number;
|
|
4
|
+
}
|
|
5
|
+
export interface AutoReconnectConnectFailedEvent {
|
|
6
|
+
attempt: number;
|
|
7
|
+
error: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface AutoReconnectControllerOptions extends AutoReconnectOptionsPartial {
|
|
3
10
|
client: ClientSocketV2;
|
|
4
|
-
minDelayMs?: number;
|
|
5
|
-
maxDelayMs?: number;
|
|
6
|
-
factor?: number;
|
|
7
11
|
timerScheduler?: SharedTimerScheduler;
|
|
8
|
-
timerKey?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare class AutoReconnectController implements ReconnectController {
|
|
11
14
|
private readonly options;
|
|
12
15
|
private readonly minDelayMs;
|
|
13
16
|
private readonly maxDelayMs;
|
|
14
17
|
private readonly factor;
|
|
18
|
+
private readonly jitterRatio;
|
|
19
|
+
private readonly jitterMode;
|
|
20
|
+
private readonly maxAttempts;
|
|
21
|
+
private readonly minStableMs;
|
|
15
22
|
private readonly timerScheduler?;
|
|
16
23
|
private readonly timerKey;
|
|
24
|
+
private readonly now;
|
|
17
25
|
private readonly unsubs;
|
|
26
|
+
private readonly giveUpListeners;
|
|
27
|
+
private readonly connectFailedListeners;
|
|
18
28
|
private timer?;
|
|
19
29
|
private active;
|
|
20
30
|
private manualStop;
|
|
21
31
|
private restarting;
|
|
22
32
|
private attempt;
|
|
23
33
|
private connecting?;
|
|
34
|
+
private stableSince?;
|
|
24
35
|
constructor(options: AutoReconnectControllerOptions);
|
|
25
36
|
start: () => Promise<void>;
|
|
26
|
-
stop: () => Promise<void>;
|
|
37
|
+
stop: (code?: number, reason?: string) => Promise<void>;
|
|
27
38
|
restart: () => Promise<void>;
|
|
39
|
+
onGiveUp: (listener: (event: AutoReconnectGiveUpEvent) => void) => (() => void);
|
|
40
|
+
onConnectFailed: (listener: (event: AutoReconnectConnectFailedEvent) => void) => (() => void);
|
|
28
41
|
destroy: () => void;
|
|
42
|
+
private applyStableReset;
|
|
43
|
+
private giveUp;
|
|
29
44
|
private scheduleReconnect;
|
|
30
45
|
private tryConnect;
|
|
31
46
|
private clearTimer;
|
|
47
|
+
private computeBackoffDelay;
|
|
32
48
|
}
|
|
@@ -10,11 +10,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.AutoReconnectController = void 0;
|
|
13
|
+
const DEFAULT_MIN_STABLE_MS = 5000;
|
|
13
14
|
class AutoReconnectController {
|
|
14
15
|
constructor(options) {
|
|
15
|
-
var _a, _b, _c, _d;
|
|
16
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
16
17
|
this.options = options;
|
|
17
18
|
this.unsubs = [];
|
|
19
|
+
this.giveUpListeners = new Set();
|
|
20
|
+
this.connectFailedListeners = new Set();
|
|
18
21
|
this.active = false;
|
|
19
22
|
this.manualStop = false;
|
|
20
23
|
this.restarting = false;
|
|
@@ -23,17 +26,20 @@ class AutoReconnectController {
|
|
|
23
26
|
this.active = true;
|
|
24
27
|
this.manualStop = false;
|
|
25
28
|
this.clearTimer();
|
|
29
|
+
this.attempt = 0;
|
|
30
|
+
this.stableSince = this.options.client.state === 'connected' ? this.now() : undefined;
|
|
26
31
|
if (this.options.client.state !== 'connected') {
|
|
27
32
|
yield this.tryConnect();
|
|
28
33
|
}
|
|
29
34
|
});
|
|
30
|
-
this.stop = () => __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
this.stop = (code, reason) => __awaiter(this, void 0, void 0, function* () {
|
|
31
36
|
this.active = false;
|
|
32
37
|
this.manualStop = true;
|
|
33
38
|
this.restarting = false;
|
|
34
39
|
this.clearTimer();
|
|
35
40
|
this.attempt = 0;
|
|
36
|
-
|
|
41
|
+
this.stableSince = undefined;
|
|
42
|
+
yield this.options.client.disconnect(code, reason);
|
|
37
43
|
});
|
|
38
44
|
this.restart = () => __awaiter(this, void 0, void 0, function* () {
|
|
39
45
|
if (!this.active) {
|
|
@@ -42,6 +48,7 @@ class AutoReconnectController {
|
|
|
42
48
|
}
|
|
43
49
|
this.clearTimer();
|
|
44
50
|
this.attempt = 0;
|
|
51
|
+
this.stableSince = undefined;
|
|
45
52
|
this.restarting = true;
|
|
46
53
|
try {
|
|
47
54
|
if (this.options.client.state === 'connected' || this.options.client.state === 'connecting') {
|
|
@@ -53,16 +60,48 @@ class AutoReconnectController {
|
|
|
53
60
|
this.restarting = false;
|
|
54
61
|
}
|
|
55
62
|
});
|
|
63
|
+
this.onGiveUp = (listener) => {
|
|
64
|
+
this.giveUpListeners.add(listener);
|
|
65
|
+
return () => {
|
|
66
|
+
this.giveUpListeners.delete(listener);
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
this.onConnectFailed = (listener) => {
|
|
70
|
+
this.connectFailedListeners.add(listener);
|
|
71
|
+
return () => {
|
|
72
|
+
this.connectFailedListeners.delete(listener);
|
|
73
|
+
};
|
|
74
|
+
};
|
|
56
75
|
this.destroy = () => {
|
|
57
76
|
this.clearTimer();
|
|
77
|
+
this.giveUpListeners.clear();
|
|
78
|
+
this.connectFailedListeners.clear();
|
|
58
79
|
this.unsubs.splice(0).forEach(unsub => unsub());
|
|
59
80
|
};
|
|
81
|
+
this.applyStableReset = () => {
|
|
82
|
+
if (this.stableSince === undefined)
|
|
83
|
+
return;
|
|
84
|
+
const stableMs = this.now() - this.stableSince;
|
|
85
|
+
this.stableSince = undefined;
|
|
86
|
+
if (stableMs >= this.minStableMs)
|
|
87
|
+
this.attempt = 0;
|
|
88
|
+
};
|
|
89
|
+
this.giveUp = () => {
|
|
90
|
+
const attempts = this.attempt;
|
|
91
|
+
this.active = false;
|
|
92
|
+
this.clearTimer();
|
|
93
|
+
this.giveUpListeners.forEach(listener => listener({ attempts }));
|
|
94
|
+
};
|
|
60
95
|
this.scheduleReconnect = () => {
|
|
61
96
|
if (!this.active || this.manualStop)
|
|
62
97
|
return;
|
|
63
98
|
if (this.timer || this.connecting)
|
|
64
99
|
return;
|
|
65
|
-
|
|
100
|
+
if (this.maxAttempts > 0 && this.attempt >= this.maxAttempts) {
|
|
101
|
+
this.giveUp();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const delayMs = this.computeBackoffDelay(this.attempt);
|
|
66
105
|
if (this.timerScheduler) {
|
|
67
106
|
this.timerScheduler.schedule(this.timerKey, delayMs, () => {
|
|
68
107
|
void this.tryConnect();
|
|
@@ -81,13 +120,21 @@ class AutoReconnectController {
|
|
|
81
120
|
return;
|
|
82
121
|
if (this.connecting)
|
|
83
122
|
return this.connecting;
|
|
123
|
+
const attempt = this.attempt;
|
|
124
|
+
let failed = false;
|
|
125
|
+
let failedError;
|
|
84
126
|
this.connecting = this.options.client
|
|
85
127
|
.connect()
|
|
86
|
-
.catch(
|
|
87
|
-
|
|
128
|
+
.catch(error => {
|
|
129
|
+
failed = true;
|
|
130
|
+
failedError = error;
|
|
88
131
|
})
|
|
89
132
|
.then(() => {
|
|
90
133
|
this.connecting = undefined;
|
|
134
|
+
if (failed) {
|
|
135
|
+
this.connectFailedListeners.forEach(listener => listener({ attempt, error: failedError }));
|
|
136
|
+
this.scheduleReconnect();
|
|
137
|
+
}
|
|
91
138
|
});
|
|
92
139
|
return this.connecting;
|
|
93
140
|
});
|
|
@@ -98,20 +145,36 @@ class AutoReconnectController {
|
|
|
98
145
|
clearTimeout(this.timer);
|
|
99
146
|
this.timer = undefined;
|
|
100
147
|
};
|
|
148
|
+
this.computeBackoffDelay = (attempt) => {
|
|
149
|
+
const expDelay = Math.min(this.minDelayMs * Math.pow(this.factor, attempt), this.maxDelayMs);
|
|
150
|
+
if (this.jitterMode === 'full') {
|
|
151
|
+
/** AWS full jitter: random in [0, expDelay). minDelay floor 적용. */
|
|
152
|
+
const delay = Math.random() * expDelay;
|
|
153
|
+
return Math.max(this.minDelayMs, Math.min(this.maxDelayMs, delay));
|
|
154
|
+
}
|
|
155
|
+
const jitter = this.jitterRatio > 0 ? expDelay * (Math.random() * 2 - 1) * this.jitterRatio : 0;
|
|
156
|
+
return Math.max(this.minDelayMs, Math.min(this.maxDelayMs, expDelay + jitter));
|
|
157
|
+
};
|
|
101
158
|
this.minDelayMs = (_a = options.minDelayMs) !== null && _a !== void 0 ? _a : 500;
|
|
102
|
-
this.maxDelayMs = (_b = options.maxDelayMs) !== null && _b !== void 0 ? _b :
|
|
159
|
+
this.maxDelayMs = (_b = options.maxDelayMs) !== null && _b !== void 0 ? _b : 30000;
|
|
103
160
|
this.factor = (_c = options.factor) !== null && _c !== void 0 ? _c : 2;
|
|
161
|
+
this.jitterRatio = Math.max(0, Math.min(1, (_d = options.jitterRatio) !== null && _d !== void 0 ? _d : 0.3));
|
|
162
|
+
this.jitterMode = (_e = options.jitterMode) !== null && _e !== void 0 ? _e : 'equal';
|
|
163
|
+
this.maxAttempts = Math.max(0, (_f = options.maxAttempts) !== null && _f !== void 0 ? _f : 0);
|
|
164
|
+
this.minStableMs = Math.max(0, (_g = options.minStableMs) !== null && _g !== void 0 ? _g : DEFAULT_MIN_STABLE_MS);
|
|
104
165
|
this.timerScheduler = options.timerScheduler;
|
|
105
|
-
this.timerKey = (
|
|
166
|
+
this.timerKey = (_h = options.timerKey) !== null && _h !== void 0 ? _h : 'reconnect';
|
|
167
|
+
this.now = (_j = options.now) !== null && _j !== void 0 ? _j : (() => Date.now());
|
|
106
168
|
this.unsubs.push(options.client.onState(event => {
|
|
107
169
|
if (!this.active)
|
|
108
170
|
return;
|
|
109
171
|
if (event.next === 'connected') {
|
|
110
|
-
this.
|
|
172
|
+
this.stableSince = this.now();
|
|
111
173
|
this.clearTimer();
|
|
112
174
|
this.connecting = undefined;
|
|
113
175
|
}
|
|
114
176
|
if (event.next === 'closed' && !this.manualStop && !this.restarting) {
|
|
177
|
+
this.applyStableReset();
|
|
115
178
|
this.scheduleReconnect();
|
|
116
179
|
}
|
|
117
180
|
}));
|
|
@@ -17,7 +17,7 @@ const shared_timer_scheduler_1 = require("./shared-timer-scheduler");
|
|
|
17
17
|
const sync_scheduler_1 = require("./sync-scheduler");
|
|
18
18
|
class SocketRuntime {
|
|
19
19
|
constructor(options) {
|
|
20
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
20
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
21
21
|
this.options = options;
|
|
22
22
|
this.start = () => __awaiter(this, void 0, void 0, function* () {
|
|
23
23
|
this.keepAlive.start();
|
|
@@ -42,11 +42,21 @@ class SocketRuntime {
|
|
|
42
42
|
timerScheduler: this.timerScheduler,
|
|
43
43
|
});
|
|
44
44
|
this.keepAlive =
|
|
45
|
-
(_e = options.keepAlive) !== null && _e !== void 0 ? _e :
|
|
45
|
+
(_e = options.keepAlive) !== null && _e !== void 0 ? _e : (options.keepAliveOptions
|
|
46
|
+
? new keep_alive_loop_1.KeepAliveLoop(Object.assign({ client: options.client, timerScheduler: this.timerScheduler }, options.keepAliveOptions))
|
|
47
|
+
: (_f = options.client.keepAlive) !== null && _f !== void 0 ? _f : new keep_alive_loop_1.KeepAliveLoop({
|
|
48
|
+
client: options.client,
|
|
49
|
+
timerScheduler: this.timerScheduler,
|
|
50
|
+
}));
|
|
46
51
|
this.reconnect =
|
|
47
|
-
(
|
|
52
|
+
(_g = options.reconnect) !== null && _g !== void 0 ? _g : (options.reconnectOptions
|
|
53
|
+
? new reconnect_controller_1.AutoReconnectController(Object.assign({ client: options.client, timerScheduler: this.timerScheduler }, options.reconnectOptions))
|
|
54
|
+
: (_h = options.client.reconnect) !== null && _h !== void 0 ? _h : new reconnect_controller_1.AutoReconnectController({
|
|
55
|
+
client: options.client,
|
|
56
|
+
timerScheduler: this.timerScheduler,
|
|
57
|
+
}));
|
|
48
58
|
this.rotation =
|
|
49
|
-
(
|
|
59
|
+
(_j = options.rotation) !== null && _j !== void 0 ? _j : new connection_rotation_controller_1.ConnectionRotationController(Object.assign({ client: options.client, reconnect: this.reconnect, timerScheduler: this.timerScheduler }, (options.rotationOptions || {})));
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
exports.SocketRuntime = SocketRuntime;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ClientSocketState, SocketFactoryContext, SocketLike, SocketTransport, SocketTransportEventMap } from './types';
|
|
2
|
+
export interface WebSocketTransportOptions {
|
|
3
|
+
/** connect() open 이벤트 대기 timeout. 만료 시 socket close + Promise reject. 0이면 무한 대기. 기본 10000 */
|
|
4
|
+
connectTimeoutMs?: number;
|
|
5
|
+
}
|
|
2
6
|
export declare class WebSocketTransport implements SocketTransport {
|
|
3
7
|
readonly url: string;
|
|
4
8
|
readonly protocols?: string | string[];
|
|
@@ -7,7 +11,10 @@ export declare class WebSocketTransport implements SocketTransport {
|
|
|
7
11
|
private socket?;
|
|
8
12
|
private unbinders;
|
|
9
13
|
private readonly listeners;
|
|
10
|
-
|
|
14
|
+
private readonly connectTimeoutMs;
|
|
15
|
+
private connecting?;
|
|
16
|
+
private cancelConnecting?;
|
|
17
|
+
constructor(url: string, protocols?: string | string[], socketFactory?: (context: SocketFactoryContext) => SocketLike, options?: WebSocketTransportOptions);
|
|
11
18
|
get state(): ClientSocketState;
|
|
12
19
|
on: <TType extends keyof SocketTransportEventMap>(type: TType, listener: (event: SocketTransportEventMap[TType]) => void) => (() => void);
|
|
13
20
|
connect: () => Promise<void>;
|
|
@@ -25,8 +25,10 @@ const bindSocketListener = (socket, type, listener) => {
|
|
|
25
25
|
socket[key] = prev !== null && prev !== void 0 ? prev : null;
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
|
|
28
29
|
class WebSocketTransport {
|
|
29
|
-
constructor(url, protocols, socketFactory = context => new globalThis.WebSocket(context.url, context.protocols)) {
|
|
30
|
+
constructor(url, protocols, socketFactory = context => new globalThis.WebSocket(context.url, context.protocols), options) {
|
|
31
|
+
var _a;
|
|
30
32
|
this.url = url;
|
|
31
33
|
this.protocols = protocols;
|
|
32
34
|
this.socketFactory = socketFactory;
|
|
@@ -47,20 +49,40 @@ class WebSocketTransport {
|
|
|
47
49
|
this.connect = () => __awaiter(this, void 0, void 0, function* () {
|
|
48
50
|
if (this._state === 'connected')
|
|
49
51
|
return;
|
|
50
|
-
if (this.
|
|
51
|
-
return;
|
|
52
|
+
if (this.connecting)
|
|
53
|
+
return this.connecting;
|
|
52
54
|
this.cleanupSocket();
|
|
53
55
|
this._state = 'connecting';
|
|
54
56
|
const socket = this.socketFactory({ url: this.url, protocols: this.protocols });
|
|
55
57
|
this.socket = socket;
|
|
56
|
-
|
|
58
|
+
this.connecting = new Promise((resolve, reject) => {
|
|
57
59
|
let connectSettled = false;
|
|
60
|
+
let timeoutTimer;
|
|
58
61
|
const settleConnect = (handler) => {
|
|
59
62
|
if (connectSettled)
|
|
60
63
|
return;
|
|
61
64
|
connectSettled = true;
|
|
65
|
+
if (timeoutTimer)
|
|
66
|
+
clearTimeout(timeoutTimer);
|
|
67
|
+
this.connecting = undefined;
|
|
68
|
+
this.cancelConnecting = undefined;
|
|
62
69
|
handler();
|
|
63
70
|
};
|
|
71
|
+
this.cancelConnecting = error => settleConnect(() => reject(error));
|
|
72
|
+
if (this.connectTimeoutMs > 0) {
|
|
73
|
+
timeoutTimer = setTimeout(() => {
|
|
74
|
+
if (connectSettled)
|
|
75
|
+
return;
|
|
76
|
+
const error = new Error(`408 CONNECT TIMEOUT - WebSocketTransport.connect() after ${this.connectTimeoutMs}ms`);
|
|
77
|
+
settleConnect(() => reject(error));
|
|
78
|
+
try {
|
|
79
|
+
socket.close(1000, 'connect-timeout');
|
|
80
|
+
}
|
|
81
|
+
catch (_a) {
|
|
82
|
+
/** ignore — best-effort cleanup */
|
|
83
|
+
}
|
|
84
|
+
}, this.connectTimeoutMs);
|
|
85
|
+
}
|
|
64
86
|
this.unbinders = [
|
|
65
87
|
bindSocketListener(socket, 'open', () => {
|
|
66
88
|
this._state = 'connected';
|
|
@@ -89,8 +111,10 @@ class WebSocketTransport {
|
|
|
89
111
|
}),
|
|
90
112
|
];
|
|
91
113
|
});
|
|
114
|
+
return this.connecting;
|
|
92
115
|
});
|
|
93
116
|
this.disconnect = (code, reason) => __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
var _b;
|
|
94
118
|
if (!this.socket) {
|
|
95
119
|
this._state = 'closed';
|
|
96
120
|
return;
|
|
@@ -98,6 +122,7 @@ class WebSocketTransport {
|
|
|
98
122
|
this._state = 'closing';
|
|
99
123
|
const socket = this.socket;
|
|
100
124
|
socket.close(code, reason);
|
|
125
|
+
(_b = this.cancelConnecting) === null || _b === void 0 ? void 0 : _b.call(this, new Error(`499 CLIENT CLOSED REQUEST - disconnect during connecting`));
|
|
101
126
|
this.cleanupSocket();
|
|
102
127
|
this._state = 'closed';
|
|
103
128
|
this.emit('close', { code, reason, wasClean: true });
|
|
@@ -116,6 +141,7 @@ class WebSocketTransport {
|
|
|
116
141
|
this.unbinders.splice(0).forEach(unbind => unbind());
|
|
117
142
|
this.socket = undefined;
|
|
118
143
|
};
|
|
144
|
+
this.connectTimeoutMs = (_a = options === null || options === void 0 ? void 0 : options.connectTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
119
145
|
}
|
|
120
146
|
get state() {
|
|
121
147
|
return this._state;
|
|
@@ -39,6 +39,26 @@ export interface SocketTransport {
|
|
|
39
39
|
send(raw: string): void;
|
|
40
40
|
on<TType extends keyof SocketTransportEventMap>(type: TType, listener: (event: SocketTransportEventMap[TType]) => void): () => void;
|
|
41
41
|
}
|
|
42
|
+
export interface KeepAliveLoopOptionsPartial {
|
|
43
|
+
intervalMs?: number;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
maxMissedPongs?: number;
|
|
46
|
+
buildPayload?: () => unknown;
|
|
47
|
+
mode?: 'send' | 'request';
|
|
48
|
+
timerKey?: string;
|
|
49
|
+
onPongTimeout?: () => void | Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export interface AutoReconnectOptionsPartial {
|
|
52
|
+
minDelayMs?: number;
|
|
53
|
+
maxDelayMs?: number;
|
|
54
|
+
factor?: number;
|
|
55
|
+
jitterRatio?: number;
|
|
56
|
+
jitterMode?: 'equal' | 'full';
|
|
57
|
+
maxAttempts?: number;
|
|
58
|
+
minStableMs?: number;
|
|
59
|
+
timerKey?: string;
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}
|
|
42
62
|
export interface ClientSocketOptions {
|
|
43
63
|
url: string;
|
|
44
64
|
protocols?: string | string[];
|
|
@@ -47,6 +67,11 @@ export interface ClientSocketOptions {
|
|
|
47
67
|
aliases?: Partial<Record<SocketPacketInputType | string, SocketPacketType>>;
|
|
48
68
|
timerScheduler?: SharedTimerScheduler;
|
|
49
69
|
requestTimeoutMs?: number;
|
|
70
|
+
connectTimeoutMs?: number;
|
|
71
|
+
maxInflightRequests?: number;
|
|
72
|
+
maxPendingRequests?: number;
|
|
73
|
+
keepAlive?: false | KeepAliveLoopOptionsPartial;
|
|
74
|
+
reconnect?: false | AutoReconnectOptionsPartial;
|
|
50
75
|
createMid?: () => string;
|
|
51
76
|
now?: () => number;
|
|
52
77
|
}
|
|
@@ -67,6 +92,8 @@ export interface ClientSocketV2 {
|
|
|
67
92
|
readonly state: ClientSocketState;
|
|
68
93
|
readonly deviceDefaults?: DeviceBootstrapInput;
|
|
69
94
|
readonly timerScheduler?: SharedTimerScheduler;
|
|
95
|
+
readonly keepAlive?: KeepAliveLoopControl;
|
|
96
|
+
readonly reconnect?: ReconnectController;
|
|
70
97
|
connect(): Promise<void>;
|
|
71
98
|
disconnect(code?: number, reason?: string): Promise<void>;
|
|
72
99
|
send<TType extends SocketPacketInputType>(type: TType, data?: SocketPacketRequestData<ResolveSocketPacketType<TType>>): void;
|
|
@@ -78,6 +105,7 @@ export interface ClientSocketV2 {
|
|
|
78
105
|
onError(listener: (event: ClientSocketErrorEvent) => void): () => void;
|
|
79
106
|
onMessage(listener: (event: ClientSocketMessageEvent) => void): () => void;
|
|
80
107
|
onType<T = any>(type: string, listener: (message: SocketMessage<T>) => void): () => void;
|
|
108
|
+
destroy(): void;
|
|
81
109
|
}
|
|
82
110
|
export declare type SyncTargetType = string;
|
|
83
111
|
export interface SyncTargetDescriptor {
|
|
@@ -116,7 +144,7 @@ export interface KeepAliveLoopControl {
|
|
|
116
144
|
}
|
|
117
145
|
export interface ReconnectController {
|
|
118
146
|
start(): Promise<void>;
|
|
119
|
-
stop(): Promise<void>;
|
|
147
|
+
stop(code?: number, reason?: string): Promise<void>;
|
|
120
148
|
restart(): Promise<void>;
|
|
121
149
|
}
|
|
122
150
|
export interface SharedTimerScheduler {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InferSocketError, InferSocketRequest, InferSocketResponse, SocketErrorMessage, SocketRequestMessage, SocketResponseMessage } from '../types';
|
|
2
|
+
export declare type AuthUpdateState = '' | 'pending' | 'validating' | 'authenticated' | 'failed' | 'disconnected';
|
|
3
|
+
export interface AuthUpdateRequestData {
|
|
4
|
+
token?: string;
|
|
5
|
+
dryRun?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface AuthUpdateMemberHead {
|
|
8
|
+
id?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AuthUpdateResponseData {
|
|
12
|
+
connId?: string;
|
|
13
|
+
deviceId?: string;
|
|
14
|
+
authId?: string;
|
|
15
|
+
memberId?: string;
|
|
16
|
+
state?: AuthUpdateState;
|
|
17
|
+
stateAt?: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
member$?: AuthUpdateMemberHead;
|
|
20
|
+
}
|
|
21
|
+
declare module '../types' {
|
|
22
|
+
interface SocketPacketRegistry {
|
|
23
|
+
'auth.update': {
|
|
24
|
+
request: AuthUpdateRequestData;
|
|
25
|
+
response: AuthUpdateResponseData;
|
|
26
|
+
error: null;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export declare type AuthUpdateType = 'auth.update';
|
|
31
|
+
export declare type AuthUpdateInput = InferSocketRequest<AuthUpdateType>;
|
|
32
|
+
export declare type AuthUpdateResponse = InferSocketResponse<AuthUpdateType>;
|
|
33
|
+
export declare type AuthUpdateErrorData = InferSocketError<AuthUpdateType>;
|
|
34
|
+
export declare type AuthUpdateRequestMessage = SocketRequestMessage<AuthUpdateType>;
|
|
35
|
+
export declare type AuthUpdateResponseMessage = SocketResponseMessage<AuthUpdateType>;
|
|
36
|
+
export declare type AuthUpdateErrorMessage = SocketErrorMessage<AuthUpdateType>;
|
package/package.json
CHANGED