mvc-kit 2.9.0 → 2.10.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 (102) hide show
  1. package/README.md +3 -1
  2. package/agent-config/claude-code/skills/guide/anti-patterns.md +64 -0
  3. package/agent-config/claude-code/skills/guide/api-reference.md +33 -2
  4. package/agent-config/claude-code/skills/guide/patterns.md +38 -0
  5. package/agent-config/copilot/copilot-instructions.md +5 -1
  6. package/agent-config/cursor/cursorrules +5 -1
  7. package/dist/Channel.cjs +5 -0
  8. package/dist/Channel.cjs.map +1 -1
  9. package/dist/Channel.d.ts +1 -0
  10. package/dist/Channel.d.ts.map +1 -1
  11. package/dist/Channel.js +5 -0
  12. package/dist/Channel.js.map +1 -1
  13. package/dist/Collection.cjs +20 -17
  14. package/dist/Collection.cjs.map +1 -1
  15. package/dist/Collection.d.ts +2 -2
  16. package/dist/Collection.d.ts.map +1 -1
  17. package/dist/Collection.js +20 -17
  18. package/dist/Collection.js.map +1 -1
  19. package/dist/Controller.cjs +5 -0
  20. package/dist/Controller.cjs.map +1 -1
  21. package/dist/Controller.d.ts +1 -0
  22. package/dist/Controller.d.ts.map +1 -1
  23. package/dist/Controller.js +5 -0
  24. package/dist/Controller.js.map +1 -1
  25. package/dist/EventBus.cjs +5 -0
  26. package/dist/EventBus.cjs.map +1 -1
  27. package/dist/EventBus.d.ts +1 -0
  28. package/dist/EventBus.d.ts.map +1 -1
  29. package/dist/EventBus.js +5 -0
  30. package/dist/EventBus.js.map +1 -1
  31. package/dist/Feed.cjs +4 -0
  32. package/dist/Feed.cjs.map +1 -1
  33. package/dist/Feed.d.ts +1 -0
  34. package/dist/Feed.d.ts.map +1 -1
  35. package/dist/Feed.js +4 -0
  36. package/dist/Feed.js.map +1 -1
  37. package/dist/Model.cjs +6 -3
  38. package/dist/Model.cjs.map +1 -1
  39. package/dist/Model.d.ts +1 -1
  40. package/dist/Model.d.ts.map +1 -1
  41. package/dist/Model.js +6 -3
  42. package/dist/Model.js.map +1 -1
  43. package/dist/Pagination.cjs +2 -0
  44. package/dist/Pagination.cjs.map +1 -1
  45. package/dist/Pagination.d.ts.map +1 -1
  46. package/dist/Pagination.js +2 -0
  47. package/dist/Pagination.js.map +1 -1
  48. package/dist/Pending.cjs +301 -0
  49. package/dist/Pending.cjs.map +1 -0
  50. package/dist/Pending.d.ts +91 -0
  51. package/dist/Pending.d.ts.map +1 -0
  52. package/dist/Pending.js +301 -0
  53. package/dist/Pending.js.map +1 -0
  54. package/dist/PersistentCollection.cjs +1 -1
  55. package/dist/PersistentCollection.cjs.map +1 -1
  56. package/dist/PersistentCollection.d.ts.map +1 -1
  57. package/dist/PersistentCollection.js +1 -1
  58. package/dist/PersistentCollection.js.map +1 -1
  59. package/dist/Selection.cjs +4 -0
  60. package/dist/Selection.cjs.map +1 -1
  61. package/dist/Selection.d.ts +1 -0
  62. package/dist/Selection.d.ts.map +1 -1
  63. package/dist/Selection.js +4 -0
  64. package/dist/Selection.js.map +1 -1
  65. package/dist/Service.cjs +5 -0
  66. package/dist/Service.cjs.map +1 -1
  67. package/dist/Service.d.ts +1 -0
  68. package/dist/Service.d.ts.map +1 -1
  69. package/dist/Service.js +5 -0
  70. package/dist/Service.js.map +1 -1
  71. package/dist/Sorting.cjs +2 -0
  72. package/dist/Sorting.cjs.map +1 -1
  73. package/dist/Sorting.d.ts.map +1 -1
  74. package/dist/Sorting.js +2 -0
  75. package/dist/Sorting.js.map +1 -1
  76. package/dist/ViewModel.cjs +45 -17
  77. package/dist/ViewModel.cjs.map +1 -1
  78. package/dist/ViewModel.d.ts +13 -4
  79. package/dist/ViewModel.d.ts.map +1 -1
  80. package/dist/ViewModel.js +45 -17
  81. package/dist/ViewModel.js.map +1 -1
  82. package/dist/bindPublicMethods.cjs +27 -0
  83. package/dist/bindPublicMethods.cjs.map +1 -0
  84. package/dist/bindPublicMethods.d.ts +18 -0
  85. package/dist/bindPublicMethods.d.ts.map +1 -0
  86. package/dist/bindPublicMethods.js +27 -0
  87. package/dist/bindPublicMethods.js.map +1 -0
  88. package/dist/index.d.ts +2 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/mvc-kit.cjs +2 -0
  91. package/dist/mvc-kit.cjs.map +1 -1
  92. package/dist/mvc-kit.js +2 -0
  93. package/dist/mvc-kit.js.map +1 -1
  94. package/dist/walkPrototypeChain.cjs.map +1 -1
  95. package/dist/walkPrototypeChain.d.ts +1 -1
  96. package/dist/walkPrototypeChain.js.map +1 -1
  97. package/dist/wrapAsyncMethods.cjs +15 -3
  98. package/dist/wrapAsyncMethods.cjs.map +1 -1
  99. package/dist/wrapAsyncMethods.d.ts.map +1 -1
  100. package/dist/wrapAsyncMethods.js +15 -3
  101. package/dist/wrapAsyncMethods.js.map +1 -1
  102. package/package.json +2 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mvc-kit
2
2
 
3
- <img src="/mvc-kit-logo.jpg" alt="mvc-kit logo" width="300" />
3
+ <img src="https://bitbucket.org/thermsio/mvc-kit/raw/master/mvc-kit-logo.jpg" alt="mvc-kit logo" width="300" />
4
4
 
5
5
  Zero-
6
6
 
@@ -790,6 +790,7 @@ function App() {
790
790
  | `Pagination` | Page/pageSize state with `apply()` slicing |
791
791
  | `Selection<K>` | Key-based selection set with toggle/select-all semantics |
792
792
  | `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination |
793
+ | `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata |
793
794
 
794
795
  ### Interfaces
795
796
 
@@ -894,6 +895,7 @@ Each core class and React hook has a dedicated reference doc with full API detai
894
895
  | [Pagination](src/Pagination.md) | Page/pageSize state with array slicing |
895
896
  | [Selection](src/Selection.md) | Key-based selection set with toggle/select-all |
896
897
  | [Feed](src/Feed.md) | Cursor + hasMore + item accumulation for server-side pagination |
898
+ | [Pending](src/Pending.md) | Per-item operation queue with retry + status tracking |
897
899
 
898
900
  **React Hooks**
899
901
 
@@ -486,3 +486,67 @@ class VM extends ViewModel {
486
486
  }
487
487
  }
488
488
  ```
489
+
490
+ ---
491
+
492
+ ## 22. Pending as a ViewModel Property
493
+
494
+ ```typescript
495
+ // BAD — Pending dies on component unmount, losing in-flight retries
496
+ class ItemsVM extends ViewModel {
497
+ readonly pending = new Pending<string>();
498
+ // ...
499
+ }
500
+
501
+ // GOOD — Pending lives on the singleton Resource
502
+ class ItemsResource extends Resource<Item> {
503
+ readonly pending = new Pending<string>();
504
+ }
505
+ class ItemsVM extends ViewModel {
506
+ private resource = singleton(ItemsResource);
507
+ get pending() { return this.resource.pending; }
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ ## 23. Passing disposeSignal to Pending's Execute Callback
514
+
515
+ ```typescript
516
+ // BAD — aborts operation when component unmounts, defeating resilience
517
+ this.pending.enqueue(id, 'delete', async () => {
518
+ await api.deleteItem(id, this.disposeSignal); // component unmount aborts!
519
+ });
520
+
521
+ // GOOD — use the signal from Pending (aborted only on cancel/supersede/dispose)
522
+ this.pending.enqueue(id, 'delete', async (signal) => {
523
+ await api.deleteItem(id, signal);
524
+ });
525
+ ```
526
+
527
+ ---
528
+
529
+ ## 24. Using collection.optimistic() Rollback with Pending
530
+
531
+ ```typescript
532
+ // BAD — snapshot goes stale during retries
533
+ async deleteItem(id: string) {
534
+ const rollback = this.collection.optimistic(() => this.remove(id));
535
+ this.pending.enqueue(id, 'delete', async (signal) => {
536
+ try {
537
+ await api.deleteItem(id, signal);
538
+ } catch (e) {
539
+ rollback(); // stale — other mutations may have happened during retries
540
+ throw e;
541
+ }
542
+ });
543
+ }
544
+
545
+ // GOOD — optimistic remove without rollback; let Pending manage failure UX
546
+ async deleteItem(id: string) {
547
+ this.optimistic(() => this.remove(id));
548
+ this.pending.enqueue(id, 'delete', async (signal) => {
549
+ await api.deleteItem(id, signal);
550
+ });
551
+ }
552
+ ```
@@ -5,7 +5,7 @@
5
5
  ```typescript
6
6
  // Core classes and utilities
7
7
  import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
8
- import { Sorting, Pagination, Selection, Feed } from 'mvc-kit';
8
+ import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
9
9
  import { singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
10
10
  import { HttpError, isAbortError, classifyError } from 'mvc-kit';
11
11
  import type { Subscribable, Disposable, Initializable, Listener, Updater, ValidationErrors, TaskState, AppError, AsyncMethodKeys, ResourceAsyncMethodKeys, ChannelStatus } from 'mvc-kit';
@@ -159,7 +159,7 @@ Static overrides for auto-eviction (zero-cost when not configured):
159
159
  Abstract base for Collections that persist to external storage. Extends Collection with delta tracking, debounced writes, and hydration.
160
160
 
161
161
  ### Static Config
162
- - `static WRITE_DELAY = 100` — Debounce ms for storage writes. `0` = immediate.
162
+ - `static WRITE_DELAY = 0` — Debounce ms for storage writes. `0` = immediate (default). Override to a positive value to coalesce rapid mutations.
163
163
 
164
164
  ### Public API
165
165
  - `hydrate(): Promise<T[]>` — Load from storage, idempotent. Returns items.
@@ -374,6 +374,37 @@ new Feed<Message>() // typed (with item accumulation)
374
374
  - `reset(): void` — Clear cursor, items, set hasMore to true.
375
375
  - `subscribe(listener): () => void` — Auto-tracked by ViewModel.
376
376
 
377
+ ### Pending\<K, Meta\>
378
+
379
+ Per-item operation queue with retry and status tracking. Lives on singleton Resource, not component-scoped ViewModel. Optional `Meta` generic for typed UI metadata on each operation.
380
+
381
+ ```typescript
382
+ new Pending<string>() // string keys, no metadata
383
+ new Pending<number>() // numeric keys
384
+ new Pending<string, { label: string }>() // with typed metadata
385
+ ```
386
+
387
+ - `getStatus(id: K): PendingOperation<Meta> | null` — Frozen snapshot of operation state (`active | retrying | failed`). Includes `meta: Meta | null`.
388
+ - `has(id: K): boolean` — Whether an operation exists for the given ID.
389
+ - `count: number` — Number of operations (all statuses).
390
+ - `hasPending: boolean` — Whether any operations are in-flight (active or retrying). Excludes failed.
391
+ - `hasFailed: boolean` — Whether any operations are in failed status.
392
+ - `failedCount: number` — Number of failed operations.
393
+ - `entries: readonly PendingEntry<K, Meta>[]` — All operations with ids. Cached, reference-stable between mutations. `PendingEntry` extends `PendingOperation<Meta>` with `readonly id: K`.
394
+ - `enqueue(id, operation, execute, meta?): void` — Fire-and-forget. Supersedes existing operation for same ID. Optional `meta` attached to snapshot.
395
+ - `retry(id: K): void` — Re-process a failed operation. Resets attempts to 0.
396
+ - `retryAll(): void` — Retry all failed operations.
397
+ - `cancel(id: K): void` — Abort signal, clear timer, remove operation.
398
+ - `cancelAll(): void` — Cancel all operations.
399
+ - `dismiss(id: K): void` — Remove a failed operation without retrying.
400
+ - `dismissAll(): void` — Remove all failed operations. Single notification.
401
+ - `subscribe(listener): () => void` — Auto-tracked by ViewModel.
402
+ - `dispose(): void` — Cancel all operations, clear listeners.
403
+
404
+ Static config (override via subclass): `MAX_RETRIES=5`, `RETRY_BASE=1000`, `RETRY_MAX=30000`, `RETRY_FACTOR=2`.
405
+
406
+ Overridable hooks: `isRetryable(error)`, `onConfirmed(id, operation)`, `onFailed(id, operation, error)`.
407
+
377
408
  ---
378
409
 
379
410
  ## Headless React Components
@@ -65,6 +65,8 @@ setTypeFilter(typeFilter: State['typeFilter']) { this.set({ typeFilter }); }
65
65
 
66
66
  The setter changes state → React re-renders → getters recompute. No manual refiltering needed.
67
67
 
68
+ All classes auto-bind methods, so they can be passed point-free as callbacks (`onFoo={vm.foo}`, `onClick={sorting.toggle}`, `onSubmit={model.commit}`).
69
+
68
70
  ---
69
71
 
70
72
  ## Encapsulating Collections
@@ -321,6 +323,42 @@ class MessageThreadVM extends ViewModel<{ draft: string }> {
321
323
  }
322
324
  ```
323
325
 
326
+ ### Pending Pattern (resilient optimistic updates)
327
+
328
+ Pending lives on the singleton Resource so operations survive component unmount:
329
+
330
+ ```typescript
331
+ class ItemsResource extends Resource<Item> {
332
+ readonly pending = new Pending<string>();
333
+
334
+ async deleteItem(id: string) {
335
+ this.optimistic(() => this.remove(id));
336
+ this.pending.enqueue(id, 'delete', async (signal) => {
337
+ await api.deleteItem(id, signal);
338
+ });
339
+ }
340
+
341
+ protected override onDispose() {
342
+ this.pending.dispose();
343
+ }
344
+ }
345
+
346
+ class ItemsVM extends ViewModel<{ filter: string }> {
347
+ private resource = singleton(ItemsResource);
348
+
349
+ get pending() { return this.resource.pending; }
350
+ get items() { return this.resource.items; }
351
+
352
+ deleteItem(id: string) { this.resource.deleteItem(id); }
353
+ retryFailed() { this.resource.pending.retryAll(); }
354
+ }
355
+ ```
356
+
357
+ **Key points:**
358
+ - Pending uses the signal from `enqueue`'s callback — do not pass `vm.disposeSignal`.
359
+ - Retries automatically on network, timeout, and server errors. Fails immediately on 4xx.
360
+ - Override `isRetryable()` in a subclass to customize retry logic.
361
+
324
362
  ### Headless Components
325
363
 
326
364
  Pass helpers directly — DataTable duck-types their interfaces:
@@ -19,6 +19,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
19
19
  | `Pagination` | Page/pageSize state with `apply()` slicing | ViewModel property |
20
20
  | `Selection<K>` | Key-based selection set with toggle/select-all semantics | ViewModel property |
21
21
  | `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination | ViewModel property |
22
+ | `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata | Resource property |
22
23
 
23
24
  ## Headless React Components (`mvc-kit/react`)
24
25
 
@@ -32,7 +33,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
32
33
 
33
34
  ```typescript
34
35
  import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
35
- import { Sorting, Pagination, Selection, Feed } from 'mvc-kit';
36
+ import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
36
37
  import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
37
38
  import { useLocal, useSingleton, useInstance, useModel, useModelRef, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
38
39
  import { DataTable, CardList, InfiniteScroll } from 'mvc-kit/react';
@@ -97,6 +98,8 @@ class ItemsViewModel extends ViewModel<ItemState> {
97
98
 
98
99
  **Section order:** Private fields → Computed getters → Lifecycle → Actions → Setters.
99
100
 
101
+ All classes auto-bind methods — safe to pass point-free as callbacks (`onFoo={vm.foo}`, `onClick={sorting.toggle}`).
102
+
100
103
  ## Component Pattern
101
104
 
102
105
  ```tsx
@@ -297,6 +300,7 @@ test('example', () => {
297
300
  - Coordinates multiple ViewModels → **Controller** (rare)
298
301
  - Sort/paginate/select on a list → **Sorting/Pagination/Selection** helpers
299
302
  - Cursor-based server pagination → **Feed** helper
303
+ - Per-item operation retry with status → **Pending** helper (on Resource)
300
304
  - Unstyled table/list/infinite scroll → **DataTable/CardList/InfiniteScroll** components
301
305
 
302
306
  ## Dev Mode
@@ -19,6 +19,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
19
19
  | `Pagination` | Page/pageSize state with `apply()` slicing | ViewModel property |
20
20
  | `Selection<K>` | Key-based selection set with toggle/select-all semantics | ViewModel property |
21
21
  | `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination | ViewModel property |
22
+ | `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata | Resource property |
22
23
 
23
24
  ## Headless React Components (`mvc-kit/react`)
24
25
 
@@ -32,7 +33,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
32
33
 
33
34
  ```typescript
34
35
  import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
35
- import { Sorting, Pagination, Selection, Feed } from 'mvc-kit';
36
+ import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
36
37
  import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
37
38
  import { useLocal, useSingleton, useInstance, useModel, useModelRef, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
38
39
  import { DataTable, CardList, InfiniteScroll } from 'mvc-kit/react';
@@ -97,6 +98,8 @@ class ItemsViewModel extends ViewModel<ItemState> {
97
98
 
98
99
  **Section order:** Private fields → Computed getters → Lifecycle → Actions → Setters.
99
100
 
101
+ All classes auto-bind methods — safe to pass point-free as callbacks (`onFoo={vm.foo}`, `onClick={sorting.toggle}`).
102
+
100
103
  ## Component Pattern
101
104
 
102
105
  ```tsx
@@ -297,6 +300,7 @@ test('example', () => {
297
300
  - Coordinates multiple ViewModels → **Controller** (rare)
298
301
  - Sort/paginate/select on a list → **Sorting/Pagination/Selection** helpers
299
302
  - Cursor-based server pagination → **Feed** helper
303
+ - Per-item operation retry with status → **Pending** helper (on Resource)
300
304
  - Unstyled table/list/infinite scroll → **DataTable/CardList/InfiniteScroll** components
301
305
 
302
306
  ## Dev Mode
package/dist/Channel.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const bindPublicMethods = require("./bindPublicMethods.cjs");
3
4
  const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
5
+ const PROTECTED_KEYS = /* @__PURE__ */ new Set(["receive", "disconnected", "addCleanup", "subscribeTo", "listenTo"]);
4
6
  const INITIAL_STATUS = Object.freeze({
5
7
  connected: false,
6
8
  reconnecting: false,
@@ -28,6 +30,9 @@ class Channel {
28
30
  _connectAbort = null;
29
31
  _reconnectTimer = null;
30
32
  _cleanups = null;
33
+ constructor() {
34
+ bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
35
+ }
31
36
  // ── Subscribable<ChannelStatus> ─────────────────────────────────
32
37
  /** Current connection status. */
33
38
  get state() {
@@ -1 +1 @@
1
- {"version":3,"file":"Channel.cjs","sources":["../src/Channel.ts"],"sourcesContent":["import type { Listener, Subscribable, Disposable, Initializable, EventPayload } 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 /** Phantom type brand — enables correct inference of M in generic helpers like listenTo(). */\n declare readonly _types: M;\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 /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\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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"}
1
+ {"version":3,"file":"Channel.cjs","sources":["../src/Channel.ts"],"sourcesContent":["import type { Listener, Subscribable, Disposable, Initializable, EventPayload } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\nconst PROTECTED_KEYS = new Set(['receive', 'disconnected', 'addCleanup', 'subscribeTo', 'listenTo']);\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 /** Phantom type brand — enables correct inference of M in generic helpers like listenTo(). */\n declare readonly _types: M;\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 constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\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 /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\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":["bindPublicMethods"],"mappings":"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"}
package/dist/Channel.d.ts CHANGED
@@ -32,6 +32,7 @@ export declare abstract class Channel<M extends Record<string, any>> implements
32
32
  private _connectAbort;
33
33
  private _reconnectTimer;
34
34
  private _cleanups;
35
+ constructor();
35
36
  /** Current connection status. */
36
37
  get state(): ChannelStatus;
37
38
  /** Subscribes to connection status changes. Returns an unsubscribe function. */
@@ -1 +1 @@
1
- {"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../src/Channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../src/Channel.ts"],"names":[],"mappings":"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"}
package/dist/Channel.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { bindPublicMethods } from "./bindPublicMethods.js";
1
2
  const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
3
+ const PROTECTED_KEYS = /* @__PURE__ */ new Set(["receive", "disconnected", "addCleanup", "subscribeTo", "listenTo"]);
2
4
  const INITIAL_STATUS = Object.freeze({
3
5
  connected: false,
4
6
  reconnecting: false,
@@ -26,6 +28,9 @@ class Channel {
26
28
  _connectAbort = null;
27
29
  _reconnectTimer = null;
28
30
  _cleanups = null;
31
+ constructor() {
32
+ bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
33
+ }
29
34
  // ── Subscribable<ChannelStatus> ─────────────────────────────────
30
35
  /** Current connection status. */
31
36
  get state() {
@@ -1 +1 @@
1
- {"version":3,"file":"Channel.js","sources":["../src/Channel.ts"],"sourcesContent":["import type { Listener, Subscribable, Disposable, Initializable, EventPayload } 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 /** Phantom type brand — enables correct inference of M in generic helpers like listenTo(). */\n declare readonly _types: M;\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 /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\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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"}
1
+ {"version":3,"file":"Channel.js","sources":["../src/Channel.ts"],"sourcesContent":["import type { Listener, Subscribable, Disposable, Initializable, EventPayload } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\nconst PROTECTED_KEYS = new Set(['receive', 'disconnected', 'addCleanup', 'subscribeTo', 'listenTo']);\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 /** Phantom type brand — enables correct inference of M in generic helpers like listenTo(). */\n declare readonly _types: M;\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 constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\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 /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\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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"}