@lemoncloud/chatic-sockets-lib 0.1.0 → 0.2.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.
Files changed (46) hide show
  1. package/dist/client-socket-v2/connection-rotation-controller.d.ts +4 -0
  2. package/dist/client-socket-v2/connection-rotation-controller.js +9 -3
  3. package/dist/client-socket-v2/create-client-socket-v2.js +155 -27
  4. package/dist/client-socket-v2/gateways/auth-gateway.d.ts +6 -0
  5. package/dist/client-socket-v2/gateways/auth-gateway.js +11 -0
  6. package/dist/client-socket-v2/gateways/channel-gateway.d.ts +38 -0
  7. package/dist/client-socket-v2/gateways/channel-gateway.js +24 -0
  8. package/dist/client-socket-v2/gateways/chat-gateway.d.ts +22 -0
  9. package/dist/client-socket-v2/gateways/chat-gateway.js +16 -0
  10. package/dist/client-socket-v2/gateways/cloud-gateway.d.ts +12 -0
  11. package/dist/client-socket-v2/gateways/cloud-gateway.js +11 -0
  12. package/dist/client-socket-v2/gateways/user-gateway.d.ts +26 -0
  13. package/dist/client-socket-v2/gateways/user-gateway.js +18 -0
  14. package/dist/client-socket-v2/index.d.ts +10 -0
  15. package/dist/client-socket-v2/index.js +9 -0
  16. package/dist/client-socket-v2/keep-alive-loop.d.ts +5 -7
  17. package/dist/client-socket-v2/keep-alive-loop.js +32 -9
  18. package/dist/client-socket-v2/message-router.d.ts +1 -0
  19. package/dist/client-socket-v2/message-router.js +4 -0
  20. package/dist/client-socket-v2/reconnect-controller.d.ts +23 -7
  21. package/dist/client-socket-v2/reconnect-controller.js +72 -9
  22. package/dist/client-socket-v2/socket-runtime.js +14 -4
  23. package/dist/client-socket-v2/socket-transport.d.ts +22 -5
  24. package/dist/client-socket-v2/socket-transport.js +79 -55
  25. package/dist/client-socket-v2/types.d.ts +45 -8
  26. package/dist/lib/auth/types.d.ts +36 -0
  27. package/dist/lib/auth/types.js +2 -0
  28. package/dist/lib/channel/types.d.ts +175 -0
  29. package/dist/lib/channel/types.js +2 -0
  30. package/dist/lib/chat/types.d.ts +79 -0
  31. package/dist/lib/chat/types.js +2 -0
  32. package/dist/lib/cloud/types.d.ts +18 -0
  33. package/dist/lib/cloud/types.js +2 -0
  34. package/dist/lib/device/types.d.ts +1 -0
  35. package/dist/lib/socket-actions.d.ts +239 -0
  36. package/dist/lib/socket-actions.js +167 -0
  37. package/dist/lib/socket-inputs.d.ts +13 -0
  38. package/dist/lib/socket-inputs.js +8 -0
  39. package/dist/lib/sockets/types.d.ts +27 -0
  40. package/dist/lib/sockets/types.js +17 -0
  41. package/dist/lib/types.d.ts +2 -0
  42. package/dist/lib/user/types.d.ts +102 -0
  43. package/dist/lib/user/types.js +2 -0
  44. package/dist/modules/chat/types.d.ts +65 -0
  45. package/dist/modules/chat/types.js +55 -0
  46. package/package.json +1 -1
@@ -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 _d, _e, _f;
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 = (_f = (_e = (_d = this.options).buildPayload) === null || _e === void 0 ? void 0 : _e.call(_d)) !== null && _f !== void 0 ? _f : null;
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.options.timeoutMs,
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.mode = (_b = options.mode) !== null && _b !== void 0 ? _b : 'request';
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 = (_c = options.timerKey) !== null && _c !== void 0 ? _c : 'keepalive';
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
- if (event.next === 'closing' || event.next === 'closed')
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 AutoReconnectControllerOptions {
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
- yield this.options.client.disconnect();
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
- const delayMs = Math.min(this.minDelayMs * Math.pow(this.factor, this.attempt), this.maxDelayMs);
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
- this.scheduleReconnect();
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 : 10000;
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 = (_d = options.timerKey) !== null && _d !== void 0 ? _d : 'reconnect';
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.attempt = 0;
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 : new keep_alive_loop_1.KeepAliveLoop(Object.assign({ client: options.client, timerScheduler: this.timerScheduler }, (options.keepAliveOptions || {})));
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
- (_f = options.reconnect) !== null && _f !== void 0 ? _f : new reconnect_controller_1.AutoReconnectController(Object.assign({ client: options.client, timerScheduler: this.timerScheduler }, (options.reconnectOptions || {})));
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
- (_g = options.rotation) !== null && _g !== void 0 ? _g : new connection_rotation_controller_1.ConnectionRotationController(Object.assign({ client: options.client, reconnect: this.reconnect, timerScheduler: this.timerScheduler }, (options.rotationOptions || {})));
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,18 +1,35 @@
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
+ }
6
+ /**
7
+ * `SocketTransport` adapter over lemon-model's owned WebSocket network.
8
+ *
9
+ * Raw network concerns (socket creation, OPEN send guard, actual close, raw event mapping) are owned by
10
+ * `OwnedWebSocketNetwork`. This adapter only translates that single-shot network into chatic's reconnectable,
11
+ * code-carrying, 4-event / 5-state lifecycle. The connect timeout stays here because it belongs to the
12
+ * reconnect/backoff policy, not the raw transport.
13
+ */
2
14
  export declare class WebSocketTransport implements SocketTransport {
3
15
  readonly url: string;
4
16
  readonly protocols?: string | string[];
5
- private readonly socketFactory;
17
+ private readonly socketFactory?;
6
18
  private _state;
7
- private socket?;
8
- private unbinders;
19
+ private network?;
20
+ private networkUnsubs;
9
21
  private readonly listeners;
10
- constructor(url: string, protocols?: string | string[], socketFactory?: (context: SocketFactoryContext) => SocketLike);
22
+ private readonly connectTimeoutMs;
23
+ private connecting?;
24
+ private cancelConnecting?;
25
+ constructor(url: string, protocols?: string | string[], socketFactory?: (context: SocketFactoryContext) => SocketLike, options?: WebSocketTransportOptions);
11
26
  get state(): ClientSocketState;
12
27
  on: <TType extends keyof SocketTransportEventMap>(type: TType, listener: (event: SocketTransportEventMap[TType]) => void) => (() => void);
13
28
  connect: () => Promise<void>;
14
29
  disconnect: (code?: number, reason?: string) => Promise<void>;
15
30
  send: (raw: string) => void;
16
31
  private emit;
17
- private cleanupSocket;
32
+ /** bridge chatic's `SocketLike` factory to the owned network's `WebSocketClosable` factory */
33
+ private buildSocketFactory;
34
+ private cleanupNetwork;
18
35
  }
@@ -10,28 +10,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.WebSocketTransport = void 0;
13
- const common_1 = require("./common");
14
- const OPEN_STATE = 1;
15
- const bindSocketListener = (socket, type, listener) => {
16
- if (socket.addEventListener) {
17
- socket.addEventListener(type, listener);
18
- return () => { var _a; return (_a = socket.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(socket, type, listener); };
19
- }
20
- const key = `on${type}`;
21
- const prev = socket[key];
22
- socket[key] = listener;
23
- return () => {
24
- if (socket[key] === listener)
25
- socket[key] = prev !== null && prev !== void 0 ? prev : null;
26
- };
27
- };
13
+ const lemon_model_1 = require("lemon-model");
14
+ const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
15
+ /**
16
+ * `SocketTransport` adapter over lemon-model's owned WebSocket network.
17
+ *
18
+ * Raw network concerns (socket creation, OPEN send guard, actual close, raw event mapping) are owned by
19
+ * `OwnedWebSocketNetwork`. This adapter only translates that single-shot network into chatic's reconnectable,
20
+ * code-carrying, 4-event / 5-state lifecycle. The connect timeout stays here because it belongs to the
21
+ * reconnect/backoff policy, not the raw transport.
22
+ */
28
23
  class WebSocketTransport {
29
- constructor(url, protocols, socketFactory = context => new globalThis.WebSocket(context.url, context.protocols)) {
24
+ constructor(url, protocols, socketFactory, options) {
25
+ var _a;
30
26
  this.url = url;
31
27
  this.protocols = protocols;
32
28
  this.socketFactory = socketFactory;
33
29
  this._state = 'idle';
34
- this.unbinders = [];
30
+ this.networkUnsubs = [];
35
31
  this.listeners = new Map();
36
32
  this.on = (type, listener) => {
37
33
  var _a;
@@ -47,75 +43,103 @@ class WebSocketTransport {
47
43
  this.connect = () => __awaiter(this, void 0, void 0, function* () {
48
44
  if (this._state === 'connected')
49
45
  return;
50
- if (this._state === 'connecting')
51
- return;
52
- this.cleanupSocket();
46
+ if (this.connecting)
47
+ return this.connecting;
48
+ this.cleanupNetwork();
53
49
  this._state = 'connecting';
54
- const socket = this.socketFactory({ url: this.url, protocols: this.protocols });
55
- this.socket = socket;
56
- yield new Promise((resolve, reject) => {
57
- let connectSettled = false;
50
+ const network = (0, lemon_model_1.createOwnedWebSocketNetwork)({
51
+ url: this.url,
52
+ protocols: this.protocols,
53
+ socketFactory: this.buildSocketFactory(),
54
+ connectTimeoutMs: 0, // connect timeout is owned here (reconnect policy), not by the network
55
+ });
56
+ this.network = network;
57
+ this.connecting = new Promise((resolve, reject) => {
58
+ var _a;
59
+ let settled = false;
60
+ let timeoutTimer;
58
61
  const settleConnect = (handler) => {
59
- if (connectSettled)
62
+ if (settled)
60
63
  return;
61
- connectSettled = true;
64
+ settled = true;
65
+ if (timeoutTimer)
66
+ clearTimeout(timeoutTimer);
67
+ this.connecting = undefined;
68
+ this.cancelConnecting = undefined;
62
69
  handler();
63
70
  };
64
- this.unbinders = [
65
- bindSocketListener(socket, 'open', () => {
66
- this._state = 'connected';
67
- this.emit('open', undefined);
68
- settleConnect(() => resolve());
69
- }),
70
- bindSocketListener(socket, 'close', event => {
71
- this._state = 'closed';
72
- this.emit('close', {
73
- code: event === null || event === void 0 ? void 0 : event.code,
74
- reason: event === null || event === void 0 ? void 0 : event.reason,
75
- wasClean: event === null || event === void 0 ? void 0 : event.wasClean,
76
- });
77
- settleConnect(() => resolve());
78
- }),
79
- bindSocketListener(socket, 'error', event => {
71
+ this.cancelConnecting = error => settleConnect(() => reject(error));
72
+ if (this.connectTimeoutMs > 0) {
73
+ timeoutTimer = setTimeout(() => {
74
+ const error = new Error(`408 CONNECT TIMEOUT - WebSocketTransport.connect() after ${this.connectTimeoutMs}ms`);
75
+ settleConnect(() => reject(error));
76
+ network.close(1000, 'connect-timeout');
77
+ }, this.connectTimeoutMs);
78
+ }
79
+ const unsubOpen = (_a = network.onOpen) === null || _a === void 0 ? void 0 : _a.call(network, () => {
80
+ this._state = 'connected';
81
+ this.emit('open', undefined);
82
+ settleConnect(() => resolve());
83
+ });
84
+ this.networkUnsubs = [
85
+ network.onMessage(raw => this.emit('message', { data: raw })),
86
+ network.onError((event, context) => {
80
87
  var _a;
81
- const error = (_a = event === null || event === void 0 ? void 0 : event.error) !== null && _a !== void 0 ? _a : new Error(`socket transport error`);
88
+ if (context.scope === 'ownedWebSocket.close') {
89
+ const close = event;
90
+ this._state = 'closed';
91
+ this.emit('close', { code: close === null || close === void 0 ? void 0 : close.code, reason: close === null || close === void 0 ? void 0 : close.reason, wasClean: close === null || close === void 0 ? void 0 : close.wasClean });
92
+ settleConnect(() => resolve()); // close-before-open resolves connect (reconnect drives off state)
93
+ return;
94
+ }
95
+ const error = (_a = event === null || event === void 0 ? void 0 : event.error) !== null && _a !== void 0 ? _a : (event instanceof Error ? event : new Error(`socket transport error`));
82
96
  this.emit('error', { error });
83
97
  if (this._state !== 'connected')
84
98
  this._state = 'closed';
85
99
  settleConnect(() => reject(error));
86
100
  }),
87
- bindSocketListener(socket, 'message', event => {
88
- this.emit('message', { data: (0, common_1.asString)(event === null || event === void 0 ? void 0 : event.data, '') });
89
- }),
90
101
  ];
102
+ if (unsubOpen)
103
+ this.networkUnsubs.push(unsubOpen);
91
104
  });
105
+ return this.connecting;
92
106
  });
93
107
  this.disconnect = (code, reason) => __awaiter(this, void 0, void 0, function* () {
94
- if (!this.socket) {
108
+ var _b;
109
+ if (!this.network) {
95
110
  this._state = 'closed';
96
111
  return;
97
112
  }
98
113
  this._state = 'closing';
99
- const socket = this.socket;
100
- socket.close(code, reason);
101
- this.cleanupSocket();
114
+ const network = this.network;
115
+ network.close(code, reason);
116
+ (_b = this.cancelConnecting) === null || _b === void 0 ? void 0 : _b.call(this, new Error(`499 CLIENT CLOSED REQUEST - disconnect during connecting`));
117
+ this.cleanupNetwork();
102
118
  this._state = 'closed';
103
119
  this.emit('close', { code, reason, wasClean: true });
104
120
  });
105
121
  this.send = (raw) => {
106
- if (!this.socket || this.socket.readyState !== undefined && this.socket.readyState !== OPEN_STATE) {
122
+ if (!this.network || this.network.readyState !== 'open') {
107
123
  throw new Error(`503 SOCKET NOT CONNECTED - WebSocketTransport.send()`);
108
124
  }
109
- this.socket.send(raw);
125
+ this.network.send(raw);
110
126
  };
111
127
  this.emit = (type, event) => {
112
128
  var _a;
113
129
  (_a = this.listeners.get(type)) === null || _a === void 0 ? void 0 : _a.forEach(listener => listener(event));
114
130
  };
115
- this.cleanupSocket = () => {
116
- this.unbinders.splice(0).forEach(unbind => unbind());
117
- this.socket = undefined;
131
+ /** bridge chatic's `SocketLike` factory to the owned network's `WebSocketClosable` factory */
132
+ this.buildSocketFactory = () => {
133
+ const factory = this.socketFactory;
134
+ if (!factory)
135
+ return undefined;
136
+ return context => factory({ url: context.url, protocols: context.protocols });
137
+ };
138
+ this.cleanupNetwork = () => {
139
+ this.networkUnsubs.splice(0).forEach(unsub => unsub());
140
+ this.network = undefined;
118
141
  };
142
+ this.connectTimeoutMs = (_a = options === null || options === void 0 ? void 0 : options.connectTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
119
143
  }
120
144
  get state() {
121
145
  return this._state;