mvc-kit 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +67 -3
- package/agent-config/claude-code/skills/guide/api-reference.md +170 -2
- package/agent-config/claude-code/skills/guide/patterns.md +158 -0
- package/agent-config/copilot/copilot-instructions.md +56 -0
- package/agent-config/cursor/cursorrules +56 -0
- package/dist/Channel.cjs +5 -0
- package/dist/Channel.cjs.map +1 -1
- package/dist/Channel.d.ts +1 -0
- package/dist/Channel.d.ts.map +1 -1
- package/dist/Channel.js +5 -0
- package/dist/Channel.js.map +1 -1
- package/dist/Collection.cjs +55 -14
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.d.ts +2 -2
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +55 -14
- package/dist/Collection.js.map +1 -1
- package/dist/Controller.cjs +5 -0
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.ts +1 -0
- package/dist/Controller.d.ts.map +1 -1
- package/dist/Controller.js +5 -0
- package/dist/Controller.js.map +1 -1
- package/dist/EventBus.cjs +5 -0
- package/dist/EventBus.cjs.map +1 -1
- package/dist/EventBus.d.ts +1 -0
- package/dist/EventBus.d.ts.map +1 -1
- package/dist/EventBus.js +5 -0
- package/dist/EventBus.js.map +1 -1
- package/dist/Feed.cjs +90 -0
- package/dist/Feed.cjs.map +1 -0
- package/dist/Feed.d.ts +47 -0
- package/dist/Feed.d.ts.map +1 -0
- package/dist/Feed.js +90 -0
- package/dist/Feed.js.map +1 -0
- package/dist/Model.cjs +6 -3
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +1 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +6 -3
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +86 -0
- package/dist/Pagination.cjs.map +1 -0
- package/dist/Pagination.d.ts +39 -0
- package/dist/Pagination.d.ts.map +1 -0
- package/dist/Pagination.js +86 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/Pending.cjs +301 -0
- package/dist/Pending.cjs.map +1 -0
- package/dist/Pending.d.ts +91 -0
- package/dist/Pending.d.ts.map +1 -0
- package/dist/Pending.js +301 -0
- package/dist/Pending.js.map +1 -0
- package/dist/PersistentCollection.cjs +9 -6
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.d.ts +6 -1
- package/dist/PersistentCollection.d.ts.map +1 -1
- package/dist/PersistentCollection.js +9 -6
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +3 -0
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.d.ts +3 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +3 -0
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +103 -0
- package/dist/Selection.cjs.map +1 -0
- package/dist/Selection.d.ts +37 -0
- package/dist/Selection.d.ts.map +1 -0
- package/dist/Selection.js +103 -0
- package/dist/Selection.js.map +1 -0
- package/dist/Service.cjs +5 -0
- package/dist/Service.cjs.map +1 -1
- package/dist/Service.d.ts +1 -0
- package/dist/Service.d.ts.map +1 -1
- package/dist/Service.js +5 -0
- package/dist/Service.js.map +1 -1
- package/dist/Sorting.cjs +116 -0
- package/dist/Sorting.cjs.map +1 -0
- package/dist/Sorting.d.ts +43 -0
- package/dist/Sorting.d.ts.map +1 -0
- package/dist/Sorting.js +116 -0
- package/dist/Sorting.js.map +1 -0
- package/dist/ViewModel.cjs +45 -17
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +13 -4
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +45 -17
- package/dist/ViewModel.js.map +1 -1
- package/dist/bindPublicMethods.cjs +27 -0
- package/dist/bindPublicMethods.cjs.map +1 -0
- package/dist/bindPublicMethods.d.ts +18 -0
- package/dist/bindPublicMethods.d.ts.map +1 -0
- package/dist/bindPublicMethods.js +27 -0
- package/dist/bindPublicMethods.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +10 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +10 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/components/CardList.cjs +42 -0
- package/dist/react/components/CardList.cjs.map +1 -0
- package/dist/react/components/CardList.d.ts +22 -0
- package/dist/react/components/CardList.d.ts.map +1 -0
- package/dist/react/components/CardList.js +42 -0
- package/dist/react/components/CardList.js.map +1 -0
- package/dist/react/components/DataTable.cjs +179 -0
- package/dist/react/components/DataTable.cjs.map +1 -0
- package/dist/react/components/DataTable.d.ts +30 -0
- package/dist/react/components/DataTable.d.ts.map +1 -0
- package/dist/react/components/DataTable.js +179 -0
- package/dist/react/components/DataTable.js.map +1 -0
- package/dist/react/components/InfiniteScroll.cjs +44 -0
- package/dist/react/components/InfiniteScroll.cjs.map +1 -0
- package/dist/react/components/InfiniteScroll.d.ts +21 -0
- package/dist/react/components/InfiniteScroll.d.ts.map +1 -0
- package/dist/react/components/InfiniteScroll.js +44 -0
- package/dist/react/components/InfiniteScroll.js.map +1 -0
- package/dist/react/components/types.cjs +15 -0
- package/dist/react/components/types.cjs.map +1 -0
- package/dist/react/components/types.d.ts +71 -0
- package/dist/react/components/types.d.ts.map +1 -0
- package/dist/react/components/types.js +15 -0
- package/dist/react/components/types.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.cjs +3 -0
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.d.ts +3 -0
- package/dist/react-native/NativeCollection.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.js +3 -0
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react.cjs +6 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +6 -0
- package/dist/react.js.map +1 -1
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.d.ts +1 -1
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.d.ts +18 -0
- package/dist/web/idb.d.ts.map +1 -1
- package/dist/web/idb.js.map +1 -1
- package/dist/wrapAsyncMethods.cjs +36 -44
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.d.ts +2 -0
- package/dist/wrapAsyncMethods.d.ts.map +1 -1
- package/dist/wrapAsyncMethods.js +36 -44
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/package.json +2 -1
|
@@ -15,13 +15,28 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
|
|
|
15
15
|
| `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
|
|
16
16
|
| `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
|
|
17
17
|
| `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
|
|
18
|
+
| `Sorting<T>` | Multi-column sort state with 3-click toggle cycle and `apply()` pipeline | ViewModel property |
|
|
19
|
+
| `Pagination` | Page/pageSize state with `apply()` slicing | ViewModel property |
|
|
20
|
+
| `Selection<K>` | Key-based selection set with toggle/select-all semantics | ViewModel property |
|
|
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 |
|
|
23
|
+
|
|
24
|
+
## Headless React Components (`mvc-kit/react`)
|
|
25
|
+
|
|
26
|
+
| Component | Description |
|
|
27
|
+
|-----------|-------------|
|
|
28
|
+
| `DataTable<T>` | Unstyled table with sort headers, selection checkboxes, pagination slots; accepts helpers directly |
|
|
29
|
+
| `CardList<T>` | Unstyled list/grid with render-prop items |
|
|
30
|
+
| `InfiniteScroll` | IntersectionObserver wrapper for infinite loading; `direction="up"` for chat UIs |
|
|
18
31
|
|
|
19
32
|
## Imports
|
|
20
33
|
|
|
21
34
|
```typescript
|
|
22
35
|
import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
|
|
36
|
+
import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
|
|
23
37
|
import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
|
|
24
38
|
import { useLocal, useSingleton, useInstance, useModel, useModelRef, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
|
|
39
|
+
import { DataTable, CardList, InfiniteScroll } from 'mvc-kit/react';
|
|
25
40
|
import { WebStorageCollection, IndexedDBCollection } from 'mvc-kit/web';
|
|
26
41
|
import { NativeCollection } from 'mvc-kit/react-native';
|
|
27
42
|
```
|
|
@@ -83,6 +98,8 @@ class ItemsViewModel extends ViewModel<ItemState> {
|
|
|
83
98
|
|
|
84
99
|
**Section order:** Private fields → Computed getters → Lifecycle → Actions → Setters.
|
|
85
100
|
|
|
101
|
+
All classes auto-bind methods — safe to pass point-free as callbacks (`onFoo={vm.foo}`, `onClick={sorting.toggle}`).
|
|
102
|
+
|
|
86
103
|
## Component Pattern
|
|
87
104
|
|
|
88
105
|
```tsx
|
|
@@ -160,6 +177,39 @@ class UserModel extends Model<UserFormState> {
|
|
|
160
177
|
}
|
|
161
178
|
```
|
|
162
179
|
|
|
180
|
+
## Composable Helpers Pattern
|
|
181
|
+
|
|
182
|
+
Declare helpers as ViewModel instance properties. They have `subscribe()` so they're auto-tracked — getters that read them recompute when helper state changes.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
class UsersListVM extends ViewModel<FilterState> {
|
|
186
|
+
private users = singleton(UsersResource);
|
|
187
|
+
readonly sorting = new Sorting<User>({ sorts: [{ key: 'name', direction: 'asc' }] });
|
|
188
|
+
readonly pagination = new Pagination({ pageSize: 25 });
|
|
189
|
+
readonly selection = new Selection<string>();
|
|
190
|
+
|
|
191
|
+
get filtered(): User[] {
|
|
192
|
+
const { search } = this.state;
|
|
193
|
+
let result = this.users.items as User[];
|
|
194
|
+
if (search) {
|
|
195
|
+
const q = search.toLowerCase();
|
|
196
|
+
result = result.filter(u => u.name.toLowerCase().includes(q));
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get items(): User[] {
|
|
202
|
+
return this.pagination.apply(this.sorting.apply(this.filtered));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Key points:**
|
|
208
|
+
- Helpers are plain classes, not ViewModels — no `init()` needed.
|
|
209
|
+
- `apply()` is a pure pipeline — chain in getters: `pagination.apply(sorting.apply(filtered))`.
|
|
210
|
+
- `Feed<T>` is for server-side cursor pagination (cursor + hasMore + optional item accumulation), not client-side slicing.
|
|
211
|
+
- Headless components (`DataTable`, `CardList`, `InfiniteScroll`) are optional — helpers work with any rendering approach. DataTable accepts helpers directly via duck-typing (`sort={vm.sorting}`, `selection={vm.selection}`, `pagination={vm.pagination}`).
|
|
212
|
+
|
|
163
213
|
## Sharing Patterns
|
|
164
214
|
|
|
165
215
|
1. **Pattern A** (default): Parent ViewModel passes props to presentational children
|
|
@@ -236,6 +286,8 @@ test('example', () => {
|
|
|
236
286
|
- `reset()` for paginated/incremental loads → use `upsert()` to accumulate data
|
|
237
287
|
- `useState`/`useMemo`/`useCallback` in connected components → ViewModel handles it
|
|
238
288
|
- Pass-through Service wrapping a typed API client → call the client directly from Resource
|
|
289
|
+
- `addCleanup` for `channel.on()`/`bus.on()` subscriptions → use `listenTo()` (auto-cleanup on dispose and reset)
|
|
290
|
+
- Missing `hydrate()` for async adapters (IndexedDB, NativeCollection) → call `hydrate()` in `onInit()` before accessing data
|
|
239
291
|
|
|
240
292
|
## Decision Framework
|
|
241
293
|
|
|
@@ -246,6 +298,10 @@ test('example', () => {
|
|
|
246
298
|
- Cross-cutting events → **EventBus**
|
|
247
299
|
- Persistent connection → **Channel**
|
|
248
300
|
- Coordinates multiple ViewModels → **Controller** (rare)
|
|
301
|
+
- Sort/paginate/select on a list → **Sorting/Pagination/Selection** helpers
|
|
302
|
+
- Cursor-based server pagination → **Feed** helper
|
|
303
|
+
- Per-item operation retry with status → **Pending** helper (on Resource)
|
|
304
|
+
- Unstyled table/list/infinite scroll → **DataTable/CardList/InfiniteScroll** components
|
|
249
305
|
|
|
250
306
|
## Dev Mode
|
|
251
307
|
|
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() {
|
package/dist/Channel.cjs.map
CHANGED
|
@@ -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":";;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,EAME,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,EAGU,SACR,QACA,OACA,SACY;AACZ,UAAM,cAAc,OAAO,GAAG,OAAO,OAAO;AAC5C,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;;"}
|
|
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. */
|
package/dist/Channel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../src/Channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
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() {
|
package/dist/Channel.js.map
CHANGED
|
@@ -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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"}
|
package/dist/Collection.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(["addCleanup"]);
|
|
4
6
|
function freeze(obj) {
|
|
5
7
|
return __DEV__ ? Object.freeze(obj) : obj;
|
|
6
8
|
}
|
|
@@ -35,8 +37,9 @@ class Collection {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
this._items = freeze(result);
|
|
38
|
-
this.
|
|
40
|
+
this._rebuildIndex();
|
|
39
41
|
this._scheduleEvictionTimer();
|
|
42
|
+
bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* Alias for Subscribable compatibility.
|
|
@@ -92,7 +95,7 @@ class Collection {
|
|
|
92
95
|
result2 = this._evictForCapacity(result2);
|
|
93
96
|
}
|
|
94
97
|
this._items = freeze(result2);
|
|
95
|
-
this.
|
|
98
|
+
this._notify(prev2);
|
|
96
99
|
this._scheduleEvictionTimer();
|
|
97
100
|
return;
|
|
98
101
|
}
|
|
@@ -120,7 +123,7 @@ class Collection {
|
|
|
120
123
|
result = this._evictForCapacity(result);
|
|
121
124
|
}
|
|
122
125
|
this._items = freeze(result);
|
|
123
|
-
this.
|
|
126
|
+
this._notify(prev);
|
|
124
127
|
this._scheduleEvictionTimer();
|
|
125
128
|
}
|
|
126
129
|
/**
|
|
@@ -134,6 +137,33 @@ class Collection {
|
|
|
134
137
|
throw new Error("Cannot upsert on disposed Collection");
|
|
135
138
|
}
|
|
136
139
|
if (items.length === 0) return;
|
|
140
|
+
if (items.length === 1) {
|
|
141
|
+
const item = items[0];
|
|
142
|
+
const existing = this._index.get(item.id);
|
|
143
|
+
if (existing) {
|
|
144
|
+
if (existing === item) return;
|
|
145
|
+
const prev2 = this._items;
|
|
146
|
+
const idx = this._items.indexOf(existing);
|
|
147
|
+
const newItems = [...prev2];
|
|
148
|
+
newItems[idx] = item;
|
|
149
|
+
this._index.set(item.id, item);
|
|
150
|
+
if (this._timestamps) this._timestamps.set(item.id, Date.now());
|
|
151
|
+
this._items = freeze(newItems);
|
|
152
|
+
this._notify(prev2);
|
|
153
|
+
} else {
|
|
154
|
+
const prev2 = this._items;
|
|
155
|
+
let result2 = [...prev2, item];
|
|
156
|
+
this._index.set(item.id, item);
|
|
157
|
+
if (this._timestamps) this._timestamps.set(item.id, Date.now());
|
|
158
|
+
if (this._maxSize > 0 && result2.length > this._maxSize) {
|
|
159
|
+
result2 = this._evictForCapacity(result2);
|
|
160
|
+
}
|
|
161
|
+
this._items = freeze(result2);
|
|
162
|
+
this._notify(prev2);
|
|
163
|
+
this._scheduleEvictionTimer();
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
137
167
|
const incoming = /* @__PURE__ */ new Map();
|
|
138
168
|
for (const item of items) {
|
|
139
169
|
incoming.set(item.id, item);
|
|
@@ -173,7 +203,7 @@ class Collection {
|
|
|
173
203
|
result = this._evictForCapacity(result);
|
|
174
204
|
}
|
|
175
205
|
this._items = freeze(result);
|
|
176
|
-
this.
|
|
206
|
+
this._notify(prev);
|
|
177
207
|
this._scheduleEvictionTimer();
|
|
178
208
|
}
|
|
179
209
|
/**
|
|
@@ -186,6 +216,17 @@ class Collection {
|
|
|
186
216
|
if (ids.length === 0) {
|
|
187
217
|
return;
|
|
188
218
|
}
|
|
219
|
+
if (ids.length === 1) {
|
|
220
|
+
const id = ids[0];
|
|
221
|
+
if (!this._index.has(id)) return;
|
|
222
|
+
const prev2 = this._items;
|
|
223
|
+
this._items = freeze(prev2.filter((item) => item.id !== id));
|
|
224
|
+
this._index.delete(id);
|
|
225
|
+
this._timestamps?.delete(id);
|
|
226
|
+
this._notify(prev2);
|
|
227
|
+
this._scheduleEvictionTimer();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
189
230
|
const idSet = new Set(ids);
|
|
190
231
|
const filtered = this._items.filter((item) => !idSet.has(item.id));
|
|
191
232
|
if (filtered.length === this._items.length) {
|
|
@@ -197,7 +238,7 @@ class Collection {
|
|
|
197
238
|
this._index.delete(id);
|
|
198
239
|
this._timestamps?.delete(id);
|
|
199
240
|
}
|
|
200
|
-
this.
|
|
241
|
+
this._notify(prev);
|
|
201
242
|
this._scheduleEvictionTimer();
|
|
202
243
|
}
|
|
203
244
|
/**
|
|
@@ -219,7 +260,7 @@ class Collection {
|
|
|
219
260
|
newItems[idx] = updated;
|
|
220
261
|
this._items = freeze(newItems);
|
|
221
262
|
this._index.set(id, updated);
|
|
222
|
-
this.
|
|
263
|
+
this._notify(prev);
|
|
223
264
|
}
|
|
224
265
|
/**
|
|
225
266
|
* Replace all items.
|
|
@@ -241,8 +282,8 @@ class Collection {
|
|
|
241
282
|
result = this._evictForCapacity(result);
|
|
242
283
|
}
|
|
243
284
|
this._items = freeze(result);
|
|
244
|
-
this.
|
|
245
|
-
this.
|
|
285
|
+
this._rebuildIndex();
|
|
286
|
+
this._notify(prev);
|
|
246
287
|
this._scheduleEvictionTimer();
|
|
247
288
|
}
|
|
248
289
|
/**
|
|
@@ -260,7 +301,7 @@ class Collection {
|
|
|
260
301
|
this._index.clear();
|
|
261
302
|
this._timestamps?.clear();
|
|
262
303
|
this._clearEvictionTimer();
|
|
263
|
-
this.
|
|
304
|
+
this._notify(prev);
|
|
264
305
|
}
|
|
265
306
|
/**
|
|
266
307
|
* Snapshot current state, apply callback mutations, and return a rollback function.
|
|
@@ -282,8 +323,8 @@ class Collection {
|
|
|
282
323
|
if (timestampSnapshot) {
|
|
283
324
|
this._timestamps = timestampSnapshot;
|
|
284
325
|
}
|
|
285
|
-
this.
|
|
286
|
-
this.
|
|
326
|
+
this._rebuildIndex();
|
|
327
|
+
this._notify(prev);
|
|
287
328
|
this._scheduleEvictionTimer();
|
|
288
329
|
};
|
|
289
330
|
}
|
|
@@ -360,12 +401,12 @@ class Collection {
|
|
|
360
401
|
}
|
|
361
402
|
this._cleanups.push(fn);
|
|
362
403
|
}
|
|
363
|
-
|
|
404
|
+
_notify(prev) {
|
|
364
405
|
for (const listener of this._listeners) {
|
|
365
406
|
listener(this._items, prev);
|
|
366
407
|
}
|
|
367
408
|
}
|
|
368
|
-
|
|
409
|
+
_rebuildIndex() {
|
|
369
410
|
this._index.clear();
|
|
370
411
|
for (const item of this._items) {
|
|
371
412
|
this._index.set(item.id, item);
|
|
@@ -440,7 +481,7 @@ class Collection {
|
|
|
440
481
|
this._index.delete(item.id);
|
|
441
482
|
this._timestamps.delete(item.id);
|
|
442
483
|
}
|
|
443
|
-
this.
|
|
484
|
+
this._notify(prev);
|
|
444
485
|
this._scheduleEvictionTimer();
|
|
445
486
|
}
|
|
446
487
|
_scheduleEvictionTimer() {
|