mvc-kit 2.5.3 → 2.5.5

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 (125) hide show
  1. package/dist/Channel.cjs +291 -0
  2. package/dist/Channel.cjs.map +1 -0
  3. package/dist/Channel.js +291 -0
  4. package/dist/Channel.js.map +1 -0
  5. package/dist/Collection.cjs +452 -0
  6. package/dist/Collection.cjs.map +1 -0
  7. package/dist/Collection.js +452 -0
  8. package/dist/Collection.js.map +1 -0
  9. package/dist/Controller.cjs +57 -0
  10. package/dist/Controller.cjs.map +1 -0
  11. package/dist/Controller.js +57 -0
  12. package/dist/Controller.js.map +1 -0
  13. package/dist/EventBus.cjs +84 -0
  14. package/dist/EventBus.cjs.map +1 -0
  15. package/dist/EventBus.js +84 -0
  16. package/dist/EventBus.js.map +1 -0
  17. package/dist/Model.cjs +175 -0
  18. package/dist/Model.cjs.map +1 -0
  19. package/dist/Model.js +175 -0
  20. package/dist/Model.js.map +1 -0
  21. package/dist/PersistentCollection.cjs +285 -0
  22. package/dist/PersistentCollection.cjs.map +1 -0
  23. package/dist/PersistentCollection.js +285 -0
  24. package/dist/PersistentCollection.js.map +1 -0
  25. package/dist/Resource.cjs +308 -0
  26. package/dist/Resource.cjs.map +1 -0
  27. package/dist/Resource.js +308 -0
  28. package/dist/Resource.js.map +1 -0
  29. package/dist/Service.cjs +51 -0
  30. package/dist/Service.cjs.map +1 -0
  31. package/dist/Service.js +51 -0
  32. package/dist/Service.js.map +1 -0
  33. package/dist/ViewModel.cjs +583 -0
  34. package/dist/ViewModel.cjs.map +1 -0
  35. package/dist/ViewModel.d.ts +6 -9
  36. package/dist/ViewModel.d.ts.map +1 -1
  37. package/dist/ViewModel.js +583 -0
  38. package/dist/ViewModel.js.map +1 -0
  39. package/dist/errors.cjs +79 -0
  40. package/dist/errors.cjs.map +1 -0
  41. package/dist/errors.js +79 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/mvc-kit.cjs +29 -1
  44. package/dist/mvc-kit.cjs.map +1 -1
  45. package/dist/mvc-kit.js +27 -1132
  46. package/dist/mvc-kit.js.map +1 -1
  47. package/dist/react/guards.cjs +7 -0
  48. package/dist/react/guards.cjs.map +1 -0
  49. package/dist/react/guards.js +7 -0
  50. package/dist/react/guards.js.map +1 -0
  51. package/dist/react/provider.cjs +26 -0
  52. package/dist/react/provider.cjs.map +1 -0
  53. package/dist/react/provider.js +26 -0
  54. package/dist/react/provider.js.map +1 -0
  55. package/dist/react/use-event-bus.cjs +26 -0
  56. package/dist/react/use-event-bus.cjs.map +1 -0
  57. package/dist/react/use-event-bus.js +26 -0
  58. package/dist/react/use-event-bus.js.map +1 -0
  59. package/dist/react/use-instance.cjs +31 -0
  60. package/dist/react/use-instance.cjs.map +1 -0
  61. package/dist/react/use-instance.js +31 -0
  62. package/dist/react/use-instance.js.map +1 -0
  63. package/dist/react/use-local.cjs +64 -0
  64. package/dist/react/use-local.cjs.map +1 -0
  65. package/dist/react/use-local.js +64 -0
  66. package/dist/react/use-local.js.map +1 -0
  67. package/dist/react/use-model.cjs +80 -0
  68. package/dist/react/use-model.cjs.map +1 -0
  69. package/dist/react/use-model.js +80 -0
  70. package/dist/react/use-model.js.map +1 -0
  71. package/dist/react/use-singleton.cjs +21 -0
  72. package/dist/react/use-singleton.cjs.map +1 -0
  73. package/dist/react/use-singleton.js +21 -0
  74. package/dist/react/use-singleton.js.map +1 -0
  75. package/dist/react/use-teardown.cjs +22 -0
  76. package/dist/react/use-teardown.cjs.map +1 -0
  77. package/dist/react/use-teardown.js +22 -0
  78. package/dist/react/use-teardown.js.map +1 -0
  79. package/dist/react-native/NativeCollection.cjs +76 -0
  80. package/dist/react-native/NativeCollection.cjs.map +1 -0
  81. package/dist/react-native/NativeCollection.js +76 -0
  82. package/dist/react-native/NativeCollection.js.map +1 -0
  83. package/dist/react-native.cjs +4 -1
  84. package/dist/react-native.cjs.map +1 -1
  85. package/dist/react-native.js +2 -60
  86. package/dist/react-native.js.map +1 -1
  87. package/dist/react.cjs +19 -1
  88. package/dist/react.cjs.map +1 -1
  89. package/dist/react.js +17 -145
  90. package/dist/react.js.map +1 -1
  91. package/dist/singleton.cjs +34 -0
  92. package/dist/singleton.cjs.map +1 -0
  93. package/dist/singleton.js +34 -0
  94. package/dist/singleton.js.map +1 -0
  95. package/dist/walkPrototypeChain.cjs +15 -0
  96. package/dist/walkPrototypeChain.cjs.map +1 -0
  97. package/dist/walkPrototypeChain.d.ts +9 -0
  98. package/dist/walkPrototypeChain.d.ts.map +1 -0
  99. package/dist/walkPrototypeChain.js +15 -0
  100. package/dist/walkPrototypeChain.js.map +1 -0
  101. package/dist/web/IndexedDBCollection.cjs +37 -0
  102. package/dist/web/IndexedDBCollection.cjs.map +1 -0
  103. package/dist/web/IndexedDBCollection.js +37 -0
  104. package/dist/web/IndexedDBCollection.js.map +1 -0
  105. package/dist/web/WebStorageCollection.cjs +85 -0
  106. package/dist/web/WebStorageCollection.cjs.map +1 -0
  107. package/dist/web/WebStorageCollection.js +85 -0
  108. package/dist/web/WebStorageCollection.js.map +1 -0
  109. package/dist/web/idb.cjs +121 -0
  110. package/dist/web/idb.cjs.map +1 -0
  111. package/dist/web/idb.js +121 -0
  112. package/dist/web/idb.js.map +1 -0
  113. package/dist/web.cjs +6 -1
  114. package/dist/web.cjs.map +1 -1
  115. package/dist/web.js +4 -178
  116. package/dist/web.js.map +1 -1
  117. package/package.json +4 -2
  118. package/dist/PersistentCollection-B8kNECDj.cjs +0 -2
  119. package/dist/PersistentCollection-B8kNECDj.cjs.map +0 -1
  120. package/dist/PersistentCollection-CbYqzFHc.js +0 -542
  121. package/dist/PersistentCollection-CbYqzFHc.js.map +0 -1
  122. package/dist/singleton-CaEXSbYg.js +0 -89
  123. package/dist/singleton-CaEXSbYg.js.map +0 -1
  124. package/dist/singleton-L-u2W_lX.cjs +0 -2
  125. package/dist/singleton-L-u2W_lX.cjs.map +0 -1
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
4
+ const INITIAL_STATUS = Object.freeze({
5
+ connected: false,
6
+ reconnecting: false,
7
+ attempt: 0,
8
+ error: null
9
+ });
10
+ class Channel {
11
+ // Static config (subclass overrides)
12
+ /** Base delay (ms) for reconnection backoff. */
13
+ static RECONNECT_BASE = 1e3;
14
+ /** Maximum delay cap (ms) for reconnection backoff. */
15
+ static RECONNECT_MAX = 3e4;
16
+ /** Exponential backoff multiplier for reconnection delay. */
17
+ static RECONNECT_FACTOR = 2;
18
+ /** Maximum number of reconnection attempts before giving up. */
19
+ static MAX_ATTEMPTS = Infinity;
20
+ // ── Internal state ──────────────────────────────────────────────
21
+ _status = INITIAL_STATUS;
22
+ _connState = 0;
23
+ _disposed = false;
24
+ _initialized = false;
25
+ _listeners = /* @__PURE__ */ new Set();
26
+ _handlers = /* @__PURE__ */ new Map();
27
+ _abortController = null;
28
+ _connectAbort = null;
29
+ _reconnectTimer = null;
30
+ _cleanups = null;
31
+ // ── Subscribable<ChannelStatus> ─────────────────────────────────
32
+ /** Current connection status. */
33
+ get state() {
34
+ return this._status;
35
+ }
36
+ /** Subscribes to connection status changes. Returns an unsubscribe function. */
37
+ subscribe(listener) {
38
+ if (this._disposed) return () => {
39
+ };
40
+ this._listeners.add(listener);
41
+ return () => {
42
+ this._listeners.delete(listener);
43
+ };
44
+ }
45
+ // ── Disposable / Initializable ──────────────────────────────────
46
+ /** Whether this instance has been disposed. */
47
+ get disposed() {
48
+ return this._disposed;
49
+ }
50
+ /** Whether init() has been called. */
51
+ get initialized() {
52
+ return this._initialized;
53
+ }
54
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
55
+ get disposeSignal() {
56
+ if (!this._abortController) {
57
+ this._abortController = new AbortController();
58
+ }
59
+ return this._abortController.signal;
60
+ }
61
+ /** Initializes the instance. Called automatically by React hooks after mount. */
62
+ init() {
63
+ if (this._initialized || this._disposed) return;
64
+ this._initialized = true;
65
+ return this.onInit?.();
66
+ }
67
+ /** Tears down the instance, releasing all subscriptions and resources. */
68
+ dispose() {
69
+ if (this._disposed) return;
70
+ this._disposed = true;
71
+ this._connState = 4;
72
+ if (this._reconnectTimer !== null) {
73
+ clearTimeout(this._reconnectTimer);
74
+ this._reconnectTimer = null;
75
+ }
76
+ this._connectAbort?.abort();
77
+ this._connectAbort = null;
78
+ this._abortController?.abort();
79
+ try {
80
+ this.close();
81
+ } catch {
82
+ }
83
+ if (this._cleanups) {
84
+ for (const fn of this._cleanups) fn();
85
+ this._cleanups = null;
86
+ }
87
+ this.onDispose?.();
88
+ this._listeners.clear();
89
+ this._handlers.clear();
90
+ }
91
+ // ── Connection control ──────────────────────────────────────────
92
+ /** Initiates a connection with automatic reconnection on failure. */
93
+ connect() {
94
+ if (this._disposed) {
95
+ if (__DEV__) {
96
+ console.warn("[mvc-kit] connect() called after dispose — ignored.");
97
+ }
98
+ return;
99
+ }
100
+ if (__DEV__ && !this._initialized) {
101
+ console.warn("[mvc-kit] connect() called before init().");
102
+ }
103
+ if (this._connState === 1 || this._connState === 2) {
104
+ return;
105
+ }
106
+ if (this._reconnectTimer !== null) {
107
+ clearTimeout(this._reconnectTimer);
108
+ this._reconnectTimer = null;
109
+ }
110
+ this._attemptConnect(0);
111
+ }
112
+ /** Closes the connection and cancels any pending reconnection. */
113
+ disconnect() {
114
+ if (this._disposed) return;
115
+ if (this._reconnectTimer !== null) {
116
+ clearTimeout(this._reconnectTimer);
117
+ this._reconnectTimer = null;
118
+ }
119
+ this._connectAbort?.abort();
120
+ this._connectAbort = null;
121
+ if (this._connState === 2 || this._connState === 1) {
122
+ this._connState = 0;
123
+ try {
124
+ this.close();
125
+ } catch {
126
+ }
127
+ } else {
128
+ this._connState = 0;
129
+ }
130
+ this._setStatus({ connected: false, reconnecting: false, attempt: 0, error: null });
131
+ }
132
+ // ── Subclass signals ────────────────────────────────────────────
133
+ /** Call from subclass when a message arrives from the transport. @protected */
134
+ receive(type, payload) {
135
+ if (this._disposed) {
136
+ if (__DEV__) {
137
+ console.warn(`[mvc-kit] receive("${String(type)}") called after dispose — ignored.`);
138
+ }
139
+ return;
140
+ }
141
+ const handlers = this._handlers.get(type);
142
+ if (handlers) {
143
+ for (const handler of handlers) {
144
+ handler(payload);
145
+ }
146
+ }
147
+ }
148
+ /** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */
149
+ disconnected() {
150
+ if (this._disposed) return;
151
+ if (this._connState !== 2 && this._connState !== 1) {
152
+ return;
153
+ }
154
+ this._connectAbort?.abort();
155
+ this._connectAbort = null;
156
+ this._connState = 3;
157
+ this._scheduleReconnect(1);
158
+ }
159
+ // ── Consumer API ────────────────────────────────────────────────
160
+ /** Subscribes to a specific message type. Returns an unsubscribe function. */
161
+ on(type, handler) {
162
+ if (this._disposed) return () => {
163
+ };
164
+ let handlers = this._handlers.get(type);
165
+ if (!handlers) {
166
+ handlers = /* @__PURE__ */ new Set();
167
+ this._handlers.set(type, handlers);
168
+ }
169
+ handlers.add(handler);
170
+ return () => {
171
+ handlers.delete(handler);
172
+ };
173
+ }
174
+ /** Subscribes to a message type, auto-removing the handler after the first invocation. */
175
+ once(type, handler) {
176
+ const unsubscribe = this.on(type, (payload) => {
177
+ unsubscribe();
178
+ handler(payload);
179
+ });
180
+ return unsubscribe;
181
+ }
182
+ // ── Infrastructure ──────────────────────────────────────────────
183
+ /** Registers a cleanup function to be called on dispose. @protected */
184
+ addCleanup(fn) {
185
+ if (!this._cleanups) {
186
+ this._cleanups = [];
187
+ }
188
+ this._cleanups.push(fn);
189
+ }
190
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
191
+ subscribeTo(source, listener) {
192
+ const unsubscribe = source.subscribe(listener);
193
+ this.addCleanup(unsubscribe);
194
+ return unsubscribe;
195
+ }
196
+ // ── Backoff ─────────────────────────────────────────────────────
197
+ /** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */
198
+ _calculateDelay(attempt) {
199
+ const ctor = this.constructor;
200
+ const capped = Math.min(
201
+ ctor.RECONNECT_BASE * Math.pow(ctor.RECONNECT_FACTOR, attempt),
202
+ ctor.RECONNECT_MAX
203
+ );
204
+ return Math.random() * capped;
205
+ }
206
+ // ── Internals ───────────────────────────────────────────────────
207
+ _setStatus(next) {
208
+ const prev = this._status;
209
+ if (prev.connected === next.connected && prev.reconnecting === next.reconnecting && prev.attempt === next.attempt && prev.error === next.error) {
210
+ return;
211
+ }
212
+ this._status = Object.freeze(next);
213
+ for (const listener of this._listeners) {
214
+ listener(this._status, prev);
215
+ }
216
+ }
217
+ _attemptConnect(attempt) {
218
+ if (this._disposed) return;
219
+ this._connState = 1;
220
+ this._connectAbort?.abort();
221
+ this._connectAbort = new AbortController();
222
+ const signal = this._abortController ? AbortSignal.any([this._abortController.signal, this._connectAbort.signal]) : this._connectAbort.signal;
223
+ this._setStatus({
224
+ connected: false,
225
+ reconnecting: attempt > 0,
226
+ attempt,
227
+ error: null
228
+ });
229
+ let result;
230
+ try {
231
+ result = this.open(signal);
232
+ } catch (e) {
233
+ this._onOpenFailed(attempt, e);
234
+ return;
235
+ }
236
+ if (result && typeof result.then === "function") {
237
+ result.then(
238
+ () => this._onOpenSucceeded(),
239
+ (e) => this._onOpenFailed(attempt, e)
240
+ );
241
+ } else {
242
+ this._onOpenSucceeded();
243
+ }
244
+ }
245
+ _onOpenSucceeded() {
246
+ if (this._disposed) return;
247
+ if (this._connState !== 1) return;
248
+ this._connState = 2;
249
+ this._setStatus({
250
+ connected: true,
251
+ reconnecting: false,
252
+ attempt: 0,
253
+ error: null
254
+ });
255
+ }
256
+ _onOpenFailed(attempt, error) {
257
+ if (this._disposed) return;
258
+ if (this._connState === 0) return;
259
+ this._connectAbort?.abort();
260
+ this._connectAbort = null;
261
+ this._connState = 3;
262
+ this._scheduleReconnect(attempt + 1, error);
263
+ }
264
+ _scheduleReconnect(attempt, error) {
265
+ const ctor = this.constructor;
266
+ if (attempt > ctor.MAX_ATTEMPTS) {
267
+ this._connState = 0;
268
+ this._setStatus({
269
+ connected: false,
270
+ reconnecting: false,
271
+ attempt,
272
+ error: error instanceof Error ? error.message : "Max reconnection attempts reached"
273
+ });
274
+ return;
275
+ }
276
+ const errorMsg = error instanceof Error ? error.message : error ? String(error) : null;
277
+ this._setStatus({
278
+ connected: false,
279
+ reconnecting: true,
280
+ attempt,
281
+ error: errorMsg
282
+ });
283
+ const delay = this._calculateDelay(attempt - 1);
284
+ this._reconnectTimer = setTimeout(() => {
285
+ this._reconnectTimer = null;
286
+ this._attemptConnect(attempt);
287
+ }, delay);
288
+ }
289
+ }
290
+ exports.Channel = Channel;
291
+ //# sourceMappingURL=Channel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Channel.cjs","sources":["../src/Channel.ts"],"sourcesContent":["import type { Listener, Subscribable, Disposable, Initializable } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Types ─────────────────────────────────────────────────────────\n\n/** Describes the current connection state of a Channel. */\nexport interface ChannelStatus {\n readonly connected: boolean;\n readonly reconnecting: boolean;\n readonly attempt: number;\n readonly error: string | null;\n}\n\ntype Handler<T> = (payload: T) => void;\n\nconst enum ConnectionState {\n Idle,\n Connecting,\n Connected,\n Reconnecting,\n Disposed,\n}\n\nconst INITIAL_STATUS: ChannelStatus = Object.freeze({\n connected: false,\n reconnecting: false,\n attempt: 0,\n error: null,\n});\n\n// ── Channel ───────────────────────────────────────────────────────\n\n/**\n * Abstract persistent connection with automatic reconnection and exponential backoff.\n * Subclass to implement WebSocket, SSE, or other transport protocols.\n */\nexport abstract class Channel<M extends Record<string, any>>\n implements Subscribable<ChannelStatus>, Initializable, Disposable\n{\n // Static config (subclass overrides)\n /** Base delay (ms) for reconnection backoff. */\n static RECONNECT_BASE = 1000;\n /** Maximum delay cap (ms) for reconnection backoff. */\n static RECONNECT_MAX = 30000;\n /** Exponential backoff multiplier for reconnection delay. */\n static RECONNECT_FACTOR = 2;\n /** Maximum number of reconnection attempts before giving up. */\n static MAX_ATTEMPTS = Infinity;\n\n // ── Internal state ──────────────────────────────────────────────\n private _status: ChannelStatus = INITIAL_STATUS;\n private _connState: ConnectionState = ConnectionState.Idle;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<ChannelStatus>>();\n private _handlers = new Map<keyof M, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _connectAbort: AbortController | null = null;\n private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n // ── Subscribable<ChannelStatus> ─────────────────────────────────\n\n /** Current connection status. */\n get state(): ChannelStatus {\n return this._status;\n }\n\n /** Subscribes to connection status changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<ChannelStatus>): () => void {\n if (this._disposed) return () => {};\n this._listeners.add(listener);\n return () => { this._listeners.delete(listener); };\n }\n\n // ── Disposable / Initializable ──────────────────────────────────\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this._connState = ConnectionState.Disposed;\n\n // Cancel pending reconnect\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n // Abort per-connection signal\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n // Abort dispose signal\n this._abortController?.abort();\n\n // Close transport\n try { this.close(); } catch { /* swallow close errors during dispose */ }\n\n // Run cleanups\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n\n this.onDispose?.();\n this._listeners.clear();\n this._handlers.clear();\n }\n\n // ── Subclass contract ───────────────────────────────────────────\n\n /** Establishes the underlying connection. Called internally by connect(). @protected */\n protected abstract open(signal: AbortSignal): void | Promise<void>;\n /** Tears down the underlying connection. Called internally by disconnect() and dispose(). @protected */\n protected abstract close(): void;\n\n // ── Connection control ──────────────────────────────────────────\n\n /** Initiates a connection with automatic reconnection on failure. */\n connect(): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] connect() called after dispose — ignored.');\n }\n return;\n }\n if (__DEV__ && !this._initialized) {\n console.warn('[mvc-kit] connect() called before init().');\n }\n if (\n this._connState === ConnectionState.Connecting ||\n this._connState === ConnectionState.Connected\n ) {\n return;\n }\n\n // Cancel any pending reconnect\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n this._attemptConnect(0);\n }\n\n /** Closes the connection and cancels any pending reconnection. */\n disconnect(): void {\n if (this._disposed) return;\n\n // Cancel pending reconnect\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n // Abort current connection attempt\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n // Close transport\n if (\n this._connState === ConnectionState.Connected ||\n this._connState === ConnectionState.Connecting\n ) {\n this._connState = ConnectionState.Idle;\n try { this.close(); } catch { /* swallow */ }\n } else {\n this._connState = ConnectionState.Idle;\n }\n\n this._setStatus({ connected: false, reconnecting: false, attempt: 0, error: null });\n }\n\n // ── Subclass signals ────────────────────────────────────────────\n\n /** Call from subclass when a message arrives from the transport. @protected */\n protected receive<K extends keyof M>(type: K, payload: M[K]): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn(`[mvc-kit] receive(\"${String(type)}\") called after dispose — ignored.`);\n }\n return;\n }\n\n const handlers = this._handlers.get(type);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */\n protected disconnected(): void {\n if (this._disposed) return;\n // Only trigger reconnect from connected or connecting states\n if (\n this._connState !== ConnectionState.Connected &&\n this._connState !== ConnectionState.Connecting\n ) {\n return;\n }\n\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n this._connState = ConnectionState.Reconnecting;\n this._scheduleReconnect(1);\n }\n\n // ── Consumer API ────────────────────────────────────────────────\n\n /** Subscribes to a specific message type. Returns an unsubscribe function. */\n on<K extends keyof M>(type: K, handler: Handler<M[K]>): () => void {\n if (this._disposed) return () => {};\n\n let handlers = this._handlers.get(type);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(type, handlers);\n }\n handlers.add(handler as Handler<unknown>);\n\n return () => { handlers!.delete(handler as Handler<unknown>); };\n }\n\n /** Subscribes to a message type, auto-removing the handler after the first invocation. */\n once<K extends keyof M>(type: K, handler: Handler<M[K]>): () => void {\n const unsubscribe = this.on(type, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\n // ── Infrastructure ──────────────────────────────────────────────\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n // ── Backoff ─────────────────────────────────────────────────────\n\n /** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */\n protected _calculateDelay(attempt: number): number {\n const ctor = this.constructor as typeof Channel;\n const capped = Math.min(\n ctor.RECONNECT_BASE * Math.pow(ctor.RECONNECT_FACTOR, attempt),\n ctor.RECONNECT_MAX,\n );\n return Math.random() * capped;\n }\n\n // ── Internals ───────────────────────────────────────────────────\n\n private _setStatus(next: ChannelStatus): void {\n const prev = this._status;\n if (\n prev.connected === next.connected &&\n prev.reconnecting === next.reconnecting &&\n prev.attempt === next.attempt &&\n prev.error === next.error\n ) {\n return;\n }\n\n this._status = Object.freeze(next);\n for (const listener of this._listeners) {\n listener(this._status, prev);\n }\n }\n\n private _attemptConnect(attempt: number): void {\n if (this._disposed) return;\n\n this._connState = ConnectionState.Connecting;\n\n // Create per-connection abort controller\n this._connectAbort?.abort();\n this._connectAbort = new AbortController();\n\n const signal = this._abortController\n ? AbortSignal.any([this._abortController.signal, this._connectAbort.signal])\n : this._connectAbort.signal;\n\n this._setStatus({\n connected: false,\n reconnecting: attempt > 0,\n attempt,\n error: null,\n });\n\n let result: void | Promise<void>;\n try {\n result = this.open(signal);\n } catch (e) {\n this._onOpenFailed(attempt, e);\n return;\n }\n\n if (result && typeof (result as Promise<void>).then === 'function') {\n (result as Promise<void>).then(\n () => this._onOpenSucceeded(),\n (e) => this._onOpenFailed(attempt, e),\n );\n } else {\n this._onOpenSucceeded();\n }\n }\n\n private _onOpenSucceeded(): void {\n if (this._disposed) return;\n // Only transition if we're still connecting (disconnect may have been called)\n if (this._connState !== ConnectionState.Connecting) return;\n\n this._connState = ConnectionState.Connected;\n this._setStatus({\n connected: true,\n reconnecting: false,\n attempt: 0,\n error: null,\n });\n }\n\n private _onOpenFailed(attempt: number, error: unknown): void {\n if (this._disposed) return;\n // If disconnect was called during open, don't reconnect\n if (this._connState === ConnectionState.Idle) return;\n\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n this._connState = ConnectionState.Reconnecting;\n this._scheduleReconnect(attempt + 1, error);\n }\n\n private _scheduleReconnect(attempt: number, error?: unknown): void {\n const ctor = this.constructor as typeof Channel;\n\n if (attempt > ctor.MAX_ATTEMPTS) {\n this._connState = ConnectionState.Idle;\n this._setStatus({\n connected: false,\n reconnecting: false,\n attempt,\n error: error instanceof Error ? error.message : 'Max reconnection attempts reached',\n });\n return;\n }\n\n const errorMsg = error instanceof Error ? error.message : (error ? String(error) : null);\n\n this._setStatus({\n connected: false,\n reconnecting: true,\n attempt,\n error: errorMsg,\n });\n\n const delay = this._calculateDelay(attempt - 1);\n this._reconnectTimer = setTimeout(() => {\n this._reconnectTimer = null;\n this._attemptConnect(attempt);\n }, delay);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,291 @@
1
+ const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
2
+ const INITIAL_STATUS = Object.freeze({
3
+ connected: false,
4
+ reconnecting: false,
5
+ attempt: 0,
6
+ error: null
7
+ });
8
+ class Channel {
9
+ // Static config (subclass overrides)
10
+ /** Base delay (ms) for reconnection backoff. */
11
+ static RECONNECT_BASE = 1e3;
12
+ /** Maximum delay cap (ms) for reconnection backoff. */
13
+ static RECONNECT_MAX = 3e4;
14
+ /** Exponential backoff multiplier for reconnection delay. */
15
+ static RECONNECT_FACTOR = 2;
16
+ /** Maximum number of reconnection attempts before giving up. */
17
+ static MAX_ATTEMPTS = Infinity;
18
+ // ── Internal state ──────────────────────────────────────────────
19
+ _status = INITIAL_STATUS;
20
+ _connState = 0;
21
+ _disposed = false;
22
+ _initialized = false;
23
+ _listeners = /* @__PURE__ */ new Set();
24
+ _handlers = /* @__PURE__ */ new Map();
25
+ _abortController = null;
26
+ _connectAbort = null;
27
+ _reconnectTimer = null;
28
+ _cleanups = null;
29
+ // ── Subscribable<ChannelStatus> ─────────────────────────────────
30
+ /** Current connection status. */
31
+ get state() {
32
+ return this._status;
33
+ }
34
+ /** Subscribes to connection status changes. Returns an unsubscribe function. */
35
+ subscribe(listener) {
36
+ if (this._disposed) return () => {
37
+ };
38
+ this._listeners.add(listener);
39
+ return () => {
40
+ this._listeners.delete(listener);
41
+ };
42
+ }
43
+ // ── Disposable / Initializable ──────────────────────────────────
44
+ /** Whether this instance has been disposed. */
45
+ get disposed() {
46
+ return this._disposed;
47
+ }
48
+ /** Whether init() has been called. */
49
+ get initialized() {
50
+ return this._initialized;
51
+ }
52
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
53
+ get disposeSignal() {
54
+ if (!this._abortController) {
55
+ this._abortController = new AbortController();
56
+ }
57
+ return this._abortController.signal;
58
+ }
59
+ /** Initializes the instance. Called automatically by React hooks after mount. */
60
+ init() {
61
+ if (this._initialized || this._disposed) return;
62
+ this._initialized = true;
63
+ return this.onInit?.();
64
+ }
65
+ /** Tears down the instance, releasing all subscriptions and resources. */
66
+ dispose() {
67
+ if (this._disposed) return;
68
+ this._disposed = true;
69
+ this._connState = 4;
70
+ if (this._reconnectTimer !== null) {
71
+ clearTimeout(this._reconnectTimer);
72
+ this._reconnectTimer = null;
73
+ }
74
+ this._connectAbort?.abort();
75
+ this._connectAbort = null;
76
+ this._abortController?.abort();
77
+ try {
78
+ this.close();
79
+ } catch {
80
+ }
81
+ if (this._cleanups) {
82
+ for (const fn of this._cleanups) fn();
83
+ this._cleanups = null;
84
+ }
85
+ this.onDispose?.();
86
+ this._listeners.clear();
87
+ this._handlers.clear();
88
+ }
89
+ // ── Connection control ──────────────────────────────────────────
90
+ /** Initiates a connection with automatic reconnection on failure. */
91
+ connect() {
92
+ if (this._disposed) {
93
+ if (__DEV__) {
94
+ console.warn("[mvc-kit] connect() called after dispose — ignored.");
95
+ }
96
+ return;
97
+ }
98
+ if (__DEV__ && !this._initialized) {
99
+ console.warn("[mvc-kit] connect() called before init().");
100
+ }
101
+ if (this._connState === 1 || this._connState === 2) {
102
+ return;
103
+ }
104
+ if (this._reconnectTimer !== null) {
105
+ clearTimeout(this._reconnectTimer);
106
+ this._reconnectTimer = null;
107
+ }
108
+ this._attemptConnect(0);
109
+ }
110
+ /** Closes the connection and cancels any pending reconnection. */
111
+ disconnect() {
112
+ if (this._disposed) return;
113
+ if (this._reconnectTimer !== null) {
114
+ clearTimeout(this._reconnectTimer);
115
+ this._reconnectTimer = null;
116
+ }
117
+ this._connectAbort?.abort();
118
+ this._connectAbort = null;
119
+ if (this._connState === 2 || this._connState === 1) {
120
+ this._connState = 0;
121
+ try {
122
+ this.close();
123
+ } catch {
124
+ }
125
+ } else {
126
+ this._connState = 0;
127
+ }
128
+ this._setStatus({ connected: false, reconnecting: false, attempt: 0, error: null });
129
+ }
130
+ // ── Subclass signals ────────────────────────────────────────────
131
+ /** Call from subclass when a message arrives from the transport. @protected */
132
+ receive(type, payload) {
133
+ if (this._disposed) {
134
+ if (__DEV__) {
135
+ console.warn(`[mvc-kit] receive("${String(type)}") called after dispose — ignored.`);
136
+ }
137
+ return;
138
+ }
139
+ const handlers = this._handlers.get(type);
140
+ if (handlers) {
141
+ for (const handler of handlers) {
142
+ handler(payload);
143
+ }
144
+ }
145
+ }
146
+ /** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */
147
+ disconnected() {
148
+ if (this._disposed) return;
149
+ if (this._connState !== 2 && this._connState !== 1) {
150
+ return;
151
+ }
152
+ this._connectAbort?.abort();
153
+ this._connectAbort = null;
154
+ this._connState = 3;
155
+ this._scheduleReconnect(1);
156
+ }
157
+ // ── Consumer API ────────────────────────────────────────────────
158
+ /** Subscribes to a specific message type. Returns an unsubscribe function. */
159
+ on(type, handler) {
160
+ if (this._disposed) return () => {
161
+ };
162
+ let handlers = this._handlers.get(type);
163
+ if (!handlers) {
164
+ handlers = /* @__PURE__ */ new Set();
165
+ this._handlers.set(type, handlers);
166
+ }
167
+ handlers.add(handler);
168
+ return () => {
169
+ handlers.delete(handler);
170
+ };
171
+ }
172
+ /** Subscribes to a message type, auto-removing the handler after the first invocation. */
173
+ once(type, handler) {
174
+ const unsubscribe = this.on(type, (payload) => {
175
+ unsubscribe();
176
+ handler(payload);
177
+ });
178
+ return unsubscribe;
179
+ }
180
+ // ── Infrastructure ──────────────────────────────────────────────
181
+ /** Registers a cleanup function to be called on dispose. @protected */
182
+ addCleanup(fn) {
183
+ if (!this._cleanups) {
184
+ this._cleanups = [];
185
+ }
186
+ this._cleanups.push(fn);
187
+ }
188
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
189
+ subscribeTo(source, listener) {
190
+ const unsubscribe = source.subscribe(listener);
191
+ this.addCleanup(unsubscribe);
192
+ return unsubscribe;
193
+ }
194
+ // ── Backoff ─────────────────────────────────────────────────────
195
+ /** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */
196
+ _calculateDelay(attempt) {
197
+ const ctor = this.constructor;
198
+ const capped = Math.min(
199
+ ctor.RECONNECT_BASE * Math.pow(ctor.RECONNECT_FACTOR, attempt),
200
+ ctor.RECONNECT_MAX
201
+ );
202
+ return Math.random() * capped;
203
+ }
204
+ // ── Internals ───────────────────────────────────────────────────
205
+ _setStatus(next) {
206
+ const prev = this._status;
207
+ if (prev.connected === next.connected && prev.reconnecting === next.reconnecting && prev.attempt === next.attempt && prev.error === next.error) {
208
+ return;
209
+ }
210
+ this._status = Object.freeze(next);
211
+ for (const listener of this._listeners) {
212
+ listener(this._status, prev);
213
+ }
214
+ }
215
+ _attemptConnect(attempt) {
216
+ if (this._disposed) return;
217
+ this._connState = 1;
218
+ this._connectAbort?.abort();
219
+ this._connectAbort = new AbortController();
220
+ const signal = this._abortController ? AbortSignal.any([this._abortController.signal, this._connectAbort.signal]) : this._connectAbort.signal;
221
+ this._setStatus({
222
+ connected: false,
223
+ reconnecting: attempt > 0,
224
+ attempt,
225
+ error: null
226
+ });
227
+ let result;
228
+ try {
229
+ result = this.open(signal);
230
+ } catch (e) {
231
+ this._onOpenFailed(attempt, e);
232
+ return;
233
+ }
234
+ if (result && typeof result.then === "function") {
235
+ result.then(
236
+ () => this._onOpenSucceeded(),
237
+ (e) => this._onOpenFailed(attempt, e)
238
+ );
239
+ } else {
240
+ this._onOpenSucceeded();
241
+ }
242
+ }
243
+ _onOpenSucceeded() {
244
+ if (this._disposed) return;
245
+ if (this._connState !== 1) return;
246
+ this._connState = 2;
247
+ this._setStatus({
248
+ connected: true,
249
+ reconnecting: false,
250
+ attempt: 0,
251
+ error: null
252
+ });
253
+ }
254
+ _onOpenFailed(attempt, error) {
255
+ if (this._disposed) return;
256
+ if (this._connState === 0) return;
257
+ this._connectAbort?.abort();
258
+ this._connectAbort = null;
259
+ this._connState = 3;
260
+ this._scheduleReconnect(attempt + 1, error);
261
+ }
262
+ _scheduleReconnect(attempt, error) {
263
+ const ctor = this.constructor;
264
+ if (attempt > ctor.MAX_ATTEMPTS) {
265
+ this._connState = 0;
266
+ this._setStatus({
267
+ connected: false,
268
+ reconnecting: false,
269
+ attempt,
270
+ error: error instanceof Error ? error.message : "Max reconnection attempts reached"
271
+ });
272
+ return;
273
+ }
274
+ const errorMsg = error instanceof Error ? error.message : error ? String(error) : null;
275
+ this._setStatus({
276
+ connected: false,
277
+ reconnecting: true,
278
+ attempt,
279
+ error: errorMsg
280
+ });
281
+ const delay = this._calculateDelay(attempt - 1);
282
+ this._reconnectTimer = setTimeout(() => {
283
+ this._reconnectTimer = null;
284
+ this._attemptConnect(attempt);
285
+ }, delay);
286
+ }
287
+ }
288
+ export {
289
+ Channel
290
+ };
291
+ //# sourceMappingURL=Channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Channel.js","sources":["../src/Channel.ts"],"sourcesContent":["import type { Listener, Subscribable, Disposable, Initializable } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Types ─────────────────────────────────────────────────────────\n\n/** Describes the current connection state of a Channel. */\nexport interface ChannelStatus {\n readonly connected: boolean;\n readonly reconnecting: boolean;\n readonly attempt: number;\n readonly error: string | null;\n}\n\ntype Handler<T> = (payload: T) => void;\n\nconst enum ConnectionState {\n Idle,\n Connecting,\n Connected,\n Reconnecting,\n Disposed,\n}\n\nconst INITIAL_STATUS: ChannelStatus = Object.freeze({\n connected: false,\n reconnecting: false,\n attempt: 0,\n error: null,\n});\n\n// ── Channel ───────────────────────────────────────────────────────\n\n/**\n * Abstract persistent connection with automatic reconnection and exponential backoff.\n * Subclass to implement WebSocket, SSE, or other transport protocols.\n */\nexport abstract class Channel<M extends Record<string, any>>\n implements Subscribable<ChannelStatus>, Initializable, Disposable\n{\n // Static config (subclass overrides)\n /** Base delay (ms) for reconnection backoff. */\n static RECONNECT_BASE = 1000;\n /** Maximum delay cap (ms) for reconnection backoff. */\n static RECONNECT_MAX = 30000;\n /** Exponential backoff multiplier for reconnection delay. */\n static RECONNECT_FACTOR = 2;\n /** Maximum number of reconnection attempts before giving up. */\n static MAX_ATTEMPTS = Infinity;\n\n // ── Internal state ──────────────────────────────────────────────\n private _status: ChannelStatus = INITIAL_STATUS;\n private _connState: ConnectionState = ConnectionState.Idle;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<ChannelStatus>>();\n private _handlers = new Map<keyof M, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _connectAbort: AbortController | null = null;\n private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n // ── Subscribable<ChannelStatus> ─────────────────────────────────\n\n /** Current connection status. */\n get state(): ChannelStatus {\n return this._status;\n }\n\n /** Subscribes to connection status changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<ChannelStatus>): () => void {\n if (this._disposed) return () => {};\n this._listeners.add(listener);\n return () => { this._listeners.delete(listener); };\n }\n\n // ── Disposable / Initializable ──────────────────────────────────\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this._connState = ConnectionState.Disposed;\n\n // Cancel pending reconnect\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n // Abort per-connection signal\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n // Abort dispose signal\n this._abortController?.abort();\n\n // Close transport\n try { this.close(); } catch { /* swallow close errors during dispose */ }\n\n // Run cleanups\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n\n this.onDispose?.();\n this._listeners.clear();\n this._handlers.clear();\n }\n\n // ── Subclass contract ───────────────────────────────────────────\n\n /** Establishes the underlying connection. Called internally by connect(). @protected */\n protected abstract open(signal: AbortSignal): void | Promise<void>;\n /** Tears down the underlying connection. Called internally by disconnect() and dispose(). @protected */\n protected abstract close(): void;\n\n // ── Connection control ──────────────────────────────────────────\n\n /** Initiates a connection with automatic reconnection on failure. */\n connect(): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] connect() called after dispose — ignored.');\n }\n return;\n }\n if (__DEV__ && !this._initialized) {\n console.warn('[mvc-kit] connect() called before init().');\n }\n if (\n this._connState === ConnectionState.Connecting ||\n this._connState === ConnectionState.Connected\n ) {\n return;\n }\n\n // Cancel any pending reconnect\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n this._attemptConnect(0);\n }\n\n /** Closes the connection and cancels any pending reconnection. */\n disconnect(): void {\n if (this._disposed) return;\n\n // Cancel pending reconnect\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n // Abort current connection attempt\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n // Close transport\n if (\n this._connState === ConnectionState.Connected ||\n this._connState === ConnectionState.Connecting\n ) {\n this._connState = ConnectionState.Idle;\n try { this.close(); } catch { /* swallow */ }\n } else {\n this._connState = ConnectionState.Idle;\n }\n\n this._setStatus({ connected: false, reconnecting: false, attempt: 0, error: null });\n }\n\n // ── Subclass signals ────────────────────────────────────────────\n\n /** Call from subclass when a message arrives from the transport. @protected */\n protected receive<K extends keyof M>(type: K, payload: M[K]): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn(`[mvc-kit] receive(\"${String(type)}\") called after dispose — ignored.`);\n }\n return;\n }\n\n const handlers = this._handlers.get(type);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */\n protected disconnected(): void {\n if (this._disposed) return;\n // Only trigger reconnect from connected or connecting states\n if (\n this._connState !== ConnectionState.Connected &&\n this._connState !== ConnectionState.Connecting\n ) {\n return;\n }\n\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n this._connState = ConnectionState.Reconnecting;\n this._scheduleReconnect(1);\n }\n\n // ── Consumer API ────────────────────────────────────────────────\n\n /** Subscribes to a specific message type. Returns an unsubscribe function. */\n on<K extends keyof M>(type: K, handler: Handler<M[K]>): () => void {\n if (this._disposed) return () => {};\n\n let handlers = this._handlers.get(type);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(type, handlers);\n }\n handlers.add(handler as Handler<unknown>);\n\n return () => { handlers!.delete(handler as Handler<unknown>); };\n }\n\n /** Subscribes to a message type, auto-removing the handler after the first invocation. */\n once<K extends keyof M>(type: K, handler: Handler<M[K]>): () => void {\n const unsubscribe = this.on(type, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\n // ── Infrastructure ──────────────────────────────────────────────\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n // ── Backoff ─────────────────────────────────────────────────────\n\n /** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */\n protected _calculateDelay(attempt: number): number {\n const ctor = this.constructor as typeof Channel;\n const capped = Math.min(\n ctor.RECONNECT_BASE * Math.pow(ctor.RECONNECT_FACTOR, attempt),\n ctor.RECONNECT_MAX,\n );\n return Math.random() * capped;\n }\n\n // ── Internals ───────────────────────────────────────────────────\n\n private _setStatus(next: ChannelStatus): void {\n const prev = this._status;\n if (\n prev.connected === next.connected &&\n prev.reconnecting === next.reconnecting &&\n prev.attempt === next.attempt &&\n prev.error === next.error\n ) {\n return;\n }\n\n this._status = Object.freeze(next);\n for (const listener of this._listeners) {\n listener(this._status, prev);\n }\n }\n\n private _attemptConnect(attempt: number): void {\n if (this._disposed) return;\n\n this._connState = ConnectionState.Connecting;\n\n // Create per-connection abort controller\n this._connectAbort?.abort();\n this._connectAbort = new AbortController();\n\n const signal = this._abortController\n ? AbortSignal.any([this._abortController.signal, this._connectAbort.signal])\n : this._connectAbort.signal;\n\n this._setStatus({\n connected: false,\n reconnecting: attempt > 0,\n attempt,\n error: null,\n });\n\n let result: void | Promise<void>;\n try {\n result = this.open(signal);\n } catch (e) {\n this._onOpenFailed(attempt, e);\n return;\n }\n\n if (result && typeof (result as Promise<void>).then === 'function') {\n (result as Promise<void>).then(\n () => this._onOpenSucceeded(),\n (e) => this._onOpenFailed(attempt, e),\n );\n } else {\n this._onOpenSucceeded();\n }\n }\n\n private _onOpenSucceeded(): void {\n if (this._disposed) return;\n // Only transition if we're still connecting (disconnect may have been called)\n if (this._connState !== ConnectionState.Connecting) return;\n\n this._connState = ConnectionState.Connected;\n this._setStatus({\n connected: true,\n reconnecting: false,\n attempt: 0,\n error: null,\n });\n }\n\n private _onOpenFailed(attempt: number, error: unknown): void {\n if (this._disposed) return;\n // If disconnect was called during open, don't reconnect\n if (this._connState === ConnectionState.Idle) return;\n\n this._connectAbort?.abort();\n this._connectAbort = null;\n\n this._connState = ConnectionState.Reconnecting;\n this._scheduleReconnect(attempt + 1, error);\n }\n\n private _scheduleReconnect(attempt: number, error?: unknown): void {\n const ctor = this.constructor as typeof Channel;\n\n if (attempt > ctor.MAX_ATTEMPTS) {\n this._connState = ConnectionState.Idle;\n this._setStatus({\n connected: false,\n reconnecting: false,\n attempt,\n error: error instanceof Error ? error.message : 'Max reconnection attempts reached',\n });\n return;\n }\n\n const errorMsg = error instanceof Error ? error.message : (error ? String(error) : null);\n\n this._setStatus({\n connected: false,\n reconnecting: true,\n attempt,\n error: errorMsg,\n });\n\n const delay = this._calculateDelay(attempt - 1);\n this._reconnectTimer = setTimeout(() => {\n this._reconnectTimer = null;\n this._attemptConnect(attempt);\n }, delay);\n }\n}\n"],"names":[],"mappings":"AAEA,MAAM,UAAU,OAAO,oBAAoB,eAAe;AAsB1D,MAAM,iBAAgC,OAAO,OAAO;AAAA,EAClD,WAAW;AAAA,EACX,cAAc;AAAA,EACd,SAAS;AAAA,EACT,OAAO;AACT,CAAC;AAQM,MAAe,QAEtB;AAAA;AAAA;AAAA,EAGE,OAAO,iBAAiB;AAAA;AAAA,EAExB,OAAO,gBAAgB;AAAA;AAAA,EAEvB,OAAO,mBAAmB;AAAA;AAAA,EAE1B,OAAO,eAAe;AAAA;AAAA,EAGd,UAAyB;AAAA,EACzB,aAA8B;AAAA,EAC9B,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,iCAAiB,IAAA;AAAA,EACjB,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,gBAAwC;AAAA,EACxC,kBAAwD;AAAA,EACxD,YAAmC;AAAA;AAAA;AAAA,EAK3C,IAAI,QAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,UAAU,UAA+C;AACvD,QAAI,KAAK,UAAW,QAAO,MAAM;AAAA,IAAC;AAClC,SAAK,WAAW,IAAI,QAAQ;AAC5B,WAAO,MAAM;AAAE,WAAK,WAAW,OAAO,QAAQ;AAAA,IAAG;AAAA,EACnD;AAAA;AAAA;AAAA,EAKA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA,EAGA,OAA6B;AAC3B,QAAI,KAAK,gBAAgB,KAAK,UAAW;AACzC,SAAK,eAAe;AACpB,WAAO,KAAK,SAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,aAAa;AAGlB,QAAI,KAAK,oBAAoB,MAAM;AACjC,mBAAa,KAAK,eAAe;AACjC,WAAK,kBAAkB;AAAA,IACzB;AAGA,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB;AAGrB,SAAK,kBAAkB,MAAA;AAGvB,QAAI;AAAE,WAAK,MAAA;AAAA,IAAS,QAAQ;AAAA,IAA4C;AAGxE,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AAEA,SAAK,YAAA;AACL,SAAK,WAAW,MAAA;AAChB,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA;AAAA;AAAA,EAYA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB,UAAI,SAAS;AACX,gBAAQ,KAAK,qDAAqD;AAAA,MACpE;AACA;AAAA,IACF;AACA,QAAI,WAAW,CAAC,KAAK,cAAc;AACjC,cAAQ,KAAK,2CAA2C;AAAA,IAC1D;AACA,QACE,KAAK,eAAe,KACpB,KAAK,eAAe,GACpB;AACA;AAAA,IACF;AAGA,QAAI,KAAK,oBAAoB,MAAM;AACjC,mBAAa,KAAK,eAAe;AACjC,WAAK,kBAAkB;AAAA,IACzB;AAEA,SAAK,gBAAgB,CAAC;AAAA,EACxB;AAAA;AAAA,EAGA,aAAmB;AACjB,QAAI,KAAK,UAAW;AAGpB,QAAI,KAAK,oBAAoB,MAAM;AACjC,mBAAa,KAAK,eAAe;AACjC,WAAK,kBAAkB;AAAA,IACzB;AAGA,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB;AAGrB,QACE,KAAK,eAAe,KACpB,KAAK,eAAe,GACpB;AACA,WAAK,aAAa;AAClB,UAAI;AAAE,aAAK,MAAA;AAAA,MAAS,QAAQ;AAAA,MAAgB;AAAA,IAC9C,OAAO;AACL,WAAK,aAAa;AAAA,IACpB;AAEA,SAAK,WAAW,EAAE,WAAW,OAAO,cAAc,OAAO,SAAS,GAAG,OAAO,KAAA,CAAM;AAAA,EACpF;AAAA;AAAA;AAAA,EAKU,QAA2B,MAAS,SAAqB;AACjE,QAAI,KAAK,WAAW;AAClB,UAAI,SAAS;AACX,gBAAQ,KAAK,sBAAsB,OAAO,IAAI,CAAC,oCAAoC;AAAA,MACrF;AACA;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,UAAU,IAAI,IAAI;AACxC,QAAI,UAAU;AACZ,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGU,eAAqB;AAC7B,QAAI,KAAK,UAAW;AAEpB,QACE,KAAK,eAAe,KACpB,KAAK,eAAe,GACpB;AACA;AAAA,IACF;AAEA,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB;AAErB,SAAK,aAAa;AAClB,SAAK,mBAAmB,CAAC;AAAA,EAC3B;AAAA;AAAA;AAAA,EAKA,GAAsB,MAAS,SAAoC;AACjE,QAAI,KAAK,UAAW,QAAO,MAAM;AAAA,IAAC;AAElC,QAAI,WAAW,KAAK,UAAU,IAAI,IAAI;AACtC,QAAI,CAAC,UAAU;AACb,qCAAe,IAAA;AACf,WAAK,UAAU,IAAI,MAAM,QAAQ;AAAA,IACnC;AACA,aAAS,IAAI,OAA2B;AAExC,WAAO,MAAM;AAAE,eAAU,OAAO,OAA2B;AAAA,IAAG;AAAA,EAChE;AAAA;AAAA,EAGA,KAAwB,MAAS,SAAoC;AACnE,UAAM,cAAc,KAAK,GAAG,MAAM,CAAC,YAAY;AAC7C,kBAAA;AACA,cAAQ,OAAO;AAAA,IACjB,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA,EAKU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAAA;AAAA,EAGU,YAAe,QAAyB,UAAmC;AACnF,UAAM,cAAc,OAAO,UAAU,QAAQ;AAC7C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA,EAUU,gBAAgB,SAAyB;AACjD,UAAM,OAAO,KAAK;AAClB,UAAM,SAAS,KAAK;AAAA,MAClB,KAAK,iBAAiB,KAAK,IAAI,KAAK,kBAAkB,OAAO;AAAA,MAC7D,KAAK;AAAA,IAAA;AAEP,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA;AAAA,EAIQ,WAAW,MAA2B;AAC5C,UAAM,OAAO,KAAK;AAClB,QACE,KAAK,cAAc,KAAK,aACxB,KAAK,iBAAiB,KAAK,gBAC3B,KAAK,YAAY,KAAK,WACtB,KAAK,UAAU,KAAK,OACpB;AACA;AAAA,IACF;AAEA,SAAK,UAAU,OAAO,OAAO,IAAI;AACjC,eAAW,YAAY,KAAK,YAAY;AACtC,eAAS,KAAK,SAAS,IAAI;AAAA,IAC7B;AAAA,EACF;AAAA,EAEQ,gBAAgB,SAAuB;AAC7C,QAAI,KAAK,UAAW;AAEpB,SAAK,aAAa;AAGlB,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB,IAAI,gBAAA;AAEzB,UAAM,SAAS,KAAK,mBAChB,YAAY,IAAI,CAAC,KAAK,iBAAiB,QAAQ,KAAK,cAAc,MAAM,CAAC,IACzE,KAAK,cAAc;AAEvB,SAAK,WAAW;AAAA,MACd,WAAW;AAAA,MACX,cAAc,UAAU;AAAA,MACxB;AAAA,MACA,OAAO;AAAA,IAAA,CACR;AAED,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,KAAK,MAAM;AAAA,IAC3B,SAAS,GAAG;AACV,WAAK,cAAc,SAAS,CAAC;AAC7B;AAAA,IACF;AAEA,QAAI,UAAU,OAAQ,OAAyB,SAAS,YAAY;AACjE,aAAyB;AAAA,QACxB,MAAM,KAAK,iBAAA;AAAA,QACX,CAAC,MAAM,KAAK,cAAc,SAAS,CAAC;AAAA,MAAA;AAAA,IAExC,OAAO;AACL,WAAK,iBAAA;AAAA,IACP;AAAA,EACF;AAAA,EAEQ,mBAAyB;AAC/B,QAAI,KAAK,UAAW;AAEpB,QAAI,KAAK,eAAe,EAA4B;AAEpD,SAAK,aAAa;AAClB,SAAK,WAAW;AAAA,MACd,WAAW;AAAA,MACX,cAAc;AAAA,MACd,SAAS;AAAA,MACT,OAAO;AAAA,IAAA,CACR;AAAA,EACH;AAAA,EAEQ,cAAc,SAAiB,OAAsB;AAC3D,QAAI,KAAK,UAAW;AAEpB,QAAI,KAAK,eAAe,EAAsB;AAE9C,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB;AAErB,SAAK,aAAa;AAClB,SAAK,mBAAmB,UAAU,GAAG,KAAK;AAAA,EAC5C;AAAA,EAEQ,mBAAmB,SAAiB,OAAuB;AACjE,UAAM,OAAO,KAAK;AAElB,QAAI,UAAU,KAAK,cAAc;AAC/B,WAAK,aAAa;AAClB,WAAK,WAAW;AAAA,QACd,WAAW;AAAA,QACX,cAAc;AAAA,QACd;AAAA,QACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAAA,CACjD;AACD;AAAA,IACF;AAEA,UAAM,WAAW,iBAAiB,QAAQ,MAAM,UAAW,QAAQ,OAAO,KAAK,IAAI;AAEnF,SAAK,WAAW;AAAA,MACd,WAAW;AAAA,MACX,cAAc;AAAA,MACd;AAAA,MACA,OAAO;AAAA,IAAA,CACR;AAED,UAAM,QAAQ,KAAK,gBAAgB,UAAU,CAAC;AAC9C,SAAK,kBAAkB,WAAW,MAAM;AACtC,WAAK,kBAAkB;AACvB,WAAK,gBAAgB,OAAO;AAAA,IAC9B,GAAG,KAAK;AAAA,EACV;AACF;"}