mvc-kit 2.2.2 → 2.3.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.
Files changed (45) hide show
  1. package/README.md +12 -5
  2. package/agent-config/claude-code/agents/mvc-kit-architect.md +2 -3
  3. package/agent-config/claude-code/skills/guide/SKILL.md +1 -1
  4. package/agent-config/claude-code/skills/guide/anti-patterns.md +42 -3
  5. package/agent-config/claude-code/skills/guide/api-reference.md +10 -3
  6. package/agent-config/claude-code/skills/guide/patterns.md +18 -13
  7. package/agent-config/claude-code/skills/review/checklist.md +1 -1
  8. package/agent-config/claude-code/skills/scaffold/SKILL.md +1 -1
  9. package/agent-config/claude-code/skills/scaffold/templates/collection.md +7 -14
  10. package/agent-config/claude-code/skills/scaffold/templates/viewmodel.md +13 -42
  11. package/agent-config/copilot/copilot-instructions.md +23 -16
  12. package/agent-config/cursor/cursorrules +23 -16
  13. package/dist/Channel.d.ts +29 -0
  14. package/dist/Channel.d.ts.map +1 -1
  15. package/dist/Collection.d.ts +38 -1
  16. package/dist/Collection.d.ts.map +1 -1
  17. package/dist/Controller.d.ts +9 -0
  18. package/dist/Controller.d.ts.map +1 -1
  19. package/dist/EventBus.d.ts +5 -0
  20. package/dist/EventBus.d.ts.map +1 -1
  21. package/dist/Model.d.ts +16 -0
  22. package/dist/Model.d.ts.map +1 -1
  23. package/dist/Service.d.ts +8 -0
  24. package/dist/Service.d.ts.map +1 -1
  25. package/dist/ViewModel.d.ts +35 -1
  26. package/dist/ViewModel.d.ts.map +1 -1
  27. package/dist/mvc-kit.cjs +1 -1
  28. package/dist/mvc-kit.cjs.map +1 -1
  29. package/dist/mvc-kit.js +503 -276
  30. package/dist/mvc-kit.js.map +1 -1
  31. package/dist/react/provider.d.ts +1 -0
  32. package/dist/react/provider.d.ts.map +1 -1
  33. package/dist/react/use-model.d.ts +2 -0
  34. package/dist/react/use-model.d.ts.map +1 -1
  35. package/dist/react.cjs.map +1 -1
  36. package/dist/react.js +1 -1
  37. package/dist/react.js.map +1 -1
  38. package/dist/{singleton-C8_FRbA7.js → singleton-CaEXSbYg.js} +5 -1
  39. package/dist/singleton-CaEXSbYg.js.map +1 -0
  40. package/dist/singleton-L-u2W_lX.cjs.map +1 -1
  41. package/dist/singleton.d.ts +10 -0
  42. package/dist/singleton.d.ts.map +1 -1
  43. package/mvc-kit-logo.jpg +0 -0
  44. package/package.json +2 -1
  45. package/dist/singleton-C8_FRbA7.js.map +0 -1
package/dist/mvc-kit.js CHANGED
@@ -1,67 +1,67 @@
1
- import { E as y } from "./singleton-C8_FRbA7.js";
2
- import { h as N, s as V, t as B, a as L } from "./singleton-C8_FRbA7.js";
3
- class w extends Error {
4
- constructor(t, e) {
5
- super(e ?? `HTTP ${t}`), this.status = t, this.name = "HttpError";
1
+ import { E as w } from "./singleton-CaEXSbYg.js";
2
+ import { h as N, s as $, t as L, a as B } from "./singleton-CaEXSbYg.js";
3
+ class C extends Error {
4
+ constructor(t, s) {
5
+ super(s ?? `HTTP ${t}`), this.status = t, this.name = "HttpError";
6
6
  }
7
7
  }
8
- function v(n) {
9
- return n instanceof Error && n.name === "AbortError";
8
+ function y(r) {
9
+ return r instanceof Error && r.name === "AbortError";
10
10
  }
11
- function C(n) {
12
- return n === 401 ? "unauthorized" : n === 403 ? "forbidden" : n === 404 ? "not_found" : n === 422 ? "validation" : n === 429 ? "rate_limited" : n >= 500 ? "server_error" : "unknown";
11
+ function S(r) {
12
+ return r === 401 ? "unauthorized" : r === 403 ? "forbidden" : r === 404 ? "not_found" : r === 422 ? "validation" : r === 429 ? "rate_limited" : r >= 500 ? "server_error" : "unknown";
13
13
  }
14
- function T(n) {
15
- return typeof n == "object" && n !== null && typeof n.status == "number" && typeof n.statusText == "string" && !(n instanceof Error);
14
+ function T(r) {
15
+ return typeof r == "object" && r !== null && typeof r.status == "number" && typeof r.statusText == "string" && !(r instanceof Error);
16
16
  }
17
- function O(n) {
18
- return n instanceof Error && n.name === "AbortError" ? {
17
+ function E(r) {
18
+ return r instanceof Error && r.name === "AbortError" ? {
19
19
  code: "abort",
20
20
  message: "Request was aborted",
21
- original: n
22
- } : n instanceof w ? {
23
- code: C(n.status),
24
- message: n.message,
25
- status: n.status,
26
- original: n
27
- } : T(n) ? {
28
- code: C(n.status),
29
- message: n.statusText || `HTTP ${n.status}`,
30
- status: n.status,
31
- original: n
32
- } : n instanceof TypeError && n.message.toLowerCase().includes("fetch") ? {
21
+ original: r
22
+ } : r instanceof C ? {
23
+ code: S(r.status),
24
+ message: r.message,
25
+ status: r.status,
26
+ original: r
27
+ } : T(r) ? {
28
+ code: S(r.status),
29
+ message: r.statusText || `HTTP ${r.status}`,
30
+ status: r.status,
31
+ original: r
32
+ } : r instanceof TypeError && r.message.toLowerCase().includes("fetch") ? {
33
33
  code: "network",
34
- message: n.message,
35
- original: n
36
- } : n instanceof Error && n.name === "TimeoutError" ? {
34
+ message: r.message,
35
+ original: r
36
+ } : r instanceof Error && r.name === "TimeoutError" ? {
37
37
  code: "timeout",
38
- message: n.message,
39
- original: n
40
- } : n instanceof Error ? {
38
+ message: r.message,
39
+ original: r
40
+ } : r instanceof Error ? {
41
41
  code: "unknown",
42
- message: n.message,
43
- original: n
42
+ message: r.message,
43
+ original: r
44
44
  } : {
45
45
  code: "unknown",
46
- message: String(n),
47
- original: n
46
+ message: String(r),
47
+ original: r
48
48
  };
49
49
  }
50
- const _ = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__;
51
- function E(n) {
52
- return n !== null && typeof n == "object" && typeof n.subscribe == "function";
50
+ const d = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__;
51
+ function O(r) {
52
+ return r !== null && typeof r == "object" && typeof r.subscribe == "function";
53
53
  }
54
- function g(n, t, e) {
55
- let s = Object.getPrototypeOf(n);
56
- for (; s && s !== t; ) {
57
- const i = Object.getOwnPropertyDescriptors(s);
58
- for (const [o, a] of Object.entries(i))
59
- o !== "constructor" && e(o, a, s);
60
- s = Object.getPrototypeOf(s);
54
+ function b(r, t, s) {
55
+ let e = Object.getPrototypeOf(r);
56
+ for (; e && e !== t; ) {
57
+ const i = Object.getOwnPropertyDescriptors(e);
58
+ for (const [n, a] of Object.entries(i))
59
+ n !== "constructor" && s(n, a, e);
60
+ e = Object.getPrototypeOf(e);
61
61
  }
62
62
  }
63
- const A = Object.freeze({ loading: !1, error: null, errorCode: null }), S = ["async", "subscribeAsync"], z = /* @__PURE__ */ new Set(["onInit", "onSet", "onDispose"]);
64
- class b {
63
+ const z = Object.freeze({ loading: !1, error: null, errorCode: null }), v = ["async", "subscribeAsync"], A = /* @__PURE__ */ new Set(["onInit", "onSet", "onDispose"]);
64
+ class m {
65
65
  _state;
66
66
  _initialState;
67
67
  _disposed = !1;
@@ -82,25 +82,32 @@ class b {
82
82
  _asyncListeners = /* @__PURE__ */ new Set();
83
83
  _asyncProxy = null;
84
84
  _activeOps = null;
85
+ /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */
85
86
  static GHOST_TIMEOUT = 3e3;
86
87
  constructor(t) {
87
88
  this._state = Object.freeze({ ...t }), this._initialState = this._state, this._guardReservedKeys();
88
89
  }
90
+ /** Current frozen state object. */
89
91
  get state() {
90
92
  return this._state;
91
93
  }
94
+ /** Whether this instance has been disposed. */
92
95
  get disposed() {
93
96
  return this._disposed;
94
97
  }
98
+ /** Whether init() has been called. */
95
99
  get initialized() {
96
100
  return this._initialized;
97
101
  }
102
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
98
103
  get disposeSignal() {
99
104
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
100
105
  }
106
+ /** Lazily-created typed EventBus for emitting and subscribing to events. */
101
107
  get events() {
102
- return this._eventBus || (this._eventBus = new y()), this._eventBus;
108
+ return this._eventBus || (this._eventBus = new w()), this._eventBus;
103
109
  }
110
+ /** Initializes the instance. Called automatically by React hooks after mount. */
104
111
  init() {
105
112
  if (!(this._initialized || this._disposed))
106
113
  return this._initialized = !0, this._trackSubscribables(), this._installStateProxy(), this._memoizeGetters(), this._wrapMethods(), this.onInit?.();
@@ -117,31 +124,38 @@ class b {
117
124
  */
118
125
  set(t) {
119
126
  if (this._disposed) return;
120
- if (_ && this._stateTracking) {
127
+ if (d && this._stateTracking) {
121
128
  console.error(
122
129
  "[mvc-kit] set() called inside a getter. Getters must be pure — they read state and return a value. They must never call set(), which would cause an infinite render loop. Move this logic to an action method."
123
130
  );
124
131
  return;
125
132
  }
126
- const e = typeof t == "function" ? t(this._state) : t;
127
- if (!Object.keys(e).some(
128
- (c) => e[c] !== this._state[c]
133
+ const s = typeof t == "function" ? t(this._state) : t;
134
+ if (!Object.keys(s).some(
135
+ (o) => s[o] !== this._state[o]
129
136
  ))
130
137
  return;
131
- const o = this._state, a = Object.freeze({ ...o, ...e });
132
- this._state = a, this._revision++, this.onSet?.(o, a);
133
- for (const c of this._listeners)
134
- c(a, o);
138
+ const n = this._state, a = Object.freeze({ ...n, ...s });
139
+ this._state = a, this._revision++, this.onSet?.(n, a);
140
+ for (const o of this._listeners)
141
+ o(a, n);
135
142
  }
136
- emit(t, e) {
137
- (this._eventBus?.disposed ?? this._disposed) || this.events.emit(t, e);
143
+ /**
144
+ * Emits a typed event via the internal EventBus.
145
+ * Safe to call during dispose cleanup callbacks.
146
+ * @protected
147
+ */
148
+ emit(t, s) {
149
+ (this._eventBus?.disposed ?? this._disposed) || this.events.emit(t, s);
138
150
  }
151
+ /** Subscribes to state changes. Returns an unsubscribe function. */
139
152
  subscribe(t) {
140
153
  return this._disposed ? () => {
141
154
  } : (this._listeners.add(t), () => {
142
155
  this._listeners.delete(t);
143
156
  });
144
157
  }
158
+ /** Tears down the instance, releasing all subscriptions and resources. */
145
159
  dispose() {
146
160
  if (!this._disposed) {
147
161
  if (this._disposed = !0, this._teardownSubscriptions(), this._abortController?.abort(), this._cleanups) {
@@ -151,43 +165,56 @@ class b {
151
165
  this._eventBus?.dispose(), this.onDispose?.(), this._listeners.clear();
152
166
  }
153
167
  }
168
+ /**
169
+ * Resets state to initial values (or provided state), aborts in-flight work,
170
+ * clears async tracking, and re-runs onInit().
171
+ */
154
172
  reset(t) {
155
173
  if (!this._disposed) {
156
174
  this._abortController?.abort(), this._abortController = null, this._teardownSubscriptions(), this._state = t ? Object.freeze({ ...t }) : this._initialState, this._revision++, this._asyncStates.clear(), this._asyncSnapshots.clear(), this._notifyAsync(), this._trackSubscribables();
157
- for (const e of this._listeners)
158
- e(this._state, this._state);
175
+ for (const s of this._listeners)
176
+ s(this._state, this._state);
159
177
  return this.onInit?.();
160
178
  }
161
179
  }
180
+ /**
181
+ * Registers a cleanup function to be called on dispose. Used internally for things like method wrapper
182
+ * cleanup and event bus disposal, but can also be used by subclasses for custom cleanup logic.
183
+ * @param fn
184
+ * @protected
185
+ */
162
186
  addCleanup(t) {
163
187
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
164
188
  }
165
- subscribeTo(t, e) {
166
- const s = t.subscribe(e);
167
- return this._subscriptionCleanups || (this._subscriptionCleanups = []), this._subscriptionCleanups.push(s), s;
189
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
190
+ subscribeTo(t, s) {
191
+ const e = t.subscribe(s);
192
+ return this._subscriptionCleanups || (this._subscriptionCleanups = []), this._subscriptionCleanups.push(e), e;
168
193
  }
169
194
  // ── Async tracking API ──────────────────────────────────────────
195
+ /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */
170
196
  get async() {
171
197
  if (!this._asyncProxy) {
172
198
  const t = this;
173
199
  this._asyncProxy = new Proxy({}, {
174
- get(e, s) {
175
- return t._asyncSnapshots.get(s) ?? A;
200
+ get(s, e) {
201
+ return t._asyncSnapshots.get(e) ?? z;
176
202
  },
177
- has(e, s) {
178
- return t._asyncSnapshots.has(s);
203
+ has(s, e) {
204
+ return t._asyncSnapshots.has(e);
179
205
  },
180
206
  ownKeys() {
181
207
  return Array.from(t._asyncSnapshots.keys());
182
208
  },
183
- getOwnPropertyDescriptor(e, s) {
184
- if (t._asyncSnapshots.has(s))
185
- return { configurable: !0, enumerable: !0, value: t._asyncSnapshots.get(s) };
209
+ getOwnPropertyDescriptor(s, e) {
210
+ if (t._asyncSnapshots.has(e))
211
+ return { configurable: !0, enumerable: !0, value: t._asyncSnapshots.get(e) };
186
212
  }
187
213
  });
188
214
  }
189
215
  return this._asyncProxy;
190
216
  }
217
+ /** Subscribes to async state changes. Used by `useAsync` for React integration. */
191
218
  subscribeAsync(t) {
192
219
  return this._disposed ? () => {
193
220
  } : (this._asyncListeners.add(t), () => {
@@ -207,95 +234,95 @@ class b {
207
234
  }
208
235
  }
209
236
  _guardReservedKeys() {
210
- g(this, b.prototype, (t) => {
211
- if (S.includes(t))
237
+ b(this, m.prototype, (t) => {
238
+ if (v.includes(t))
212
239
  throw new Error(
213
240
  `[mvc-kit] "${t}" is a reserved property on ViewModel and cannot be overridden.`
214
241
  );
215
242
  });
216
243
  }
217
244
  _wrapMethods() {
218
- for (const i of S)
245
+ for (const i of v)
219
246
  if (Object.getOwnPropertyDescriptor(this, i)?.value !== void 0)
220
247
  throw new Error(
221
248
  `[mvc-kit] "${i}" is a reserved property on ViewModel and cannot be overridden.`
222
249
  );
223
- const t = this, e = /* @__PURE__ */ new Set(), s = [];
224
- _ && (this._activeOps = /* @__PURE__ */ new Map()), g(this, b.prototype, (i, o) => {
225
- if (o.get || o.set || typeof o.value != "function" || i.startsWith("_") || z.has(i) || e.has(i)) return;
226
- e.add(i);
227
- const a = o.value;
228
- let c = !1;
229
- const d = function(...f) {
250
+ const t = this, s = /* @__PURE__ */ new Set(), e = [];
251
+ d && (this._activeOps = /* @__PURE__ */ new Map()), b(this, m.prototype, (i, n) => {
252
+ if (n.get || n.set || typeof n.value != "function" || i.startsWith("_") || A.has(i) || s.has(i)) return;
253
+ s.add(i);
254
+ const a = n.value;
255
+ let o = !1;
256
+ const l = function(...h) {
230
257
  if (t._disposed) {
231
- _ && console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);
258
+ d && console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);
232
259
  return;
233
260
  }
234
- _ && !t._initialized && console.warn(
261
+ d && !t._initialized && console.warn(
235
262
  `[mvc-kit] "${i}" called before init(). Async tracking is active only after init().`
236
263
  );
237
- let h;
264
+ let u;
238
265
  try {
239
- h = a.apply(t, f);
240
- } catch (l) {
241
- throw l;
266
+ u = a.apply(t, h);
267
+ } catch (_) {
268
+ throw _;
242
269
  }
243
- if (!h || typeof h.then != "function")
244
- return c || (c = !0, t._asyncStates.delete(i), t._asyncSnapshots.delete(i), t[i] = a.bind(t)), h;
245
- let r = t._asyncStates.get(i);
246
- return r || (r = { loading: !1, error: null, errorCode: null, count: 0 }, t._asyncStates.set(i, r)), r.count++, r.loading = !0, r.error = null, r.errorCode = null, t._asyncSnapshots.set(i, Object.freeze({ loading: !0, error: null, errorCode: null })), t._notifyAsync(), _ && t._activeOps && t._activeOps.set(i, (t._activeOps.get(i) ?? 0) + 1), h.then(
247
- (l) => {
248
- if (t._disposed) return l;
249
- if (r.count--, r.loading = r.count > 0, t._asyncSnapshots.set(
270
+ if (!u || typeof u.then != "function")
271
+ return o || (o = !0, t._asyncStates.delete(i), t._asyncSnapshots.delete(i), t[i] = a.bind(t)), u;
272
+ let c = t._asyncStates.get(i);
273
+ return c || (c = { loading: !1, error: null, errorCode: null, count: 0 }, t._asyncStates.set(i, c)), c.count++, c.loading = !0, c.error = null, c.errorCode = null, t._asyncSnapshots.set(i, Object.freeze({ loading: !0, error: null, errorCode: null })), t._notifyAsync(), d && t._activeOps && t._activeOps.set(i, (t._activeOps.get(i) ?? 0) + 1), u.then(
274
+ (_) => {
275
+ if (t._disposed) return _;
276
+ if (c.count--, c.loading = c.count > 0, t._asyncSnapshots.set(
250
277
  i,
251
- Object.freeze({ loading: r.loading, error: r.error, errorCode: r.errorCode })
252
- ), t._notifyAsync(), _ && t._activeOps) {
253
- const u = (t._activeOps.get(i) ?? 1) - 1;
254
- u <= 0 ? t._activeOps.delete(i) : t._activeOps.set(i, u);
278
+ Object.freeze({ loading: c.loading, error: c.error, errorCode: c.errorCode })
279
+ ), t._notifyAsync(), d && t._activeOps) {
280
+ const f = (t._activeOps.get(i) ?? 1) - 1;
281
+ f <= 0 ? t._activeOps.delete(i) : t._activeOps.set(i, f);
255
282
  }
256
- return l;
283
+ return _;
257
284
  },
258
- (l) => {
259
- if (v(l)) {
260
- if (t._disposed || (r.count--, r.loading = r.count > 0, t._asyncSnapshots.set(
285
+ (_) => {
286
+ if (y(_)) {
287
+ if (t._disposed || (c.count--, c.loading = c.count > 0, t._asyncSnapshots.set(
261
288
  i,
262
- Object.freeze({ loading: r.loading, error: r.error, errorCode: r.errorCode })
263
- ), t._notifyAsync()), _ && t._activeOps) {
289
+ Object.freeze({ loading: c.loading, error: c.error, errorCode: c.errorCode })
290
+ ), t._notifyAsync()), d && t._activeOps) {
264
291
  const p = (t._activeOps.get(i) ?? 1) - 1;
265
292
  p <= 0 ? t._activeOps.delete(i) : t._activeOps.set(i, p);
266
293
  }
267
294
  return;
268
295
  }
269
296
  if (t._disposed) return;
270
- r.count--, r.loading = r.count > 0;
271
- const u = O(l);
272
- if (r.error = u.message, r.errorCode = u.code, t._asyncSnapshots.set(
297
+ c.count--, c.loading = c.count > 0;
298
+ const f = E(_);
299
+ if (c.error = f.message, c.errorCode = f.code, t._asyncSnapshots.set(
273
300
  i,
274
- Object.freeze({ loading: r.loading, error: u.message, errorCode: u.code })
275
- ), t._notifyAsync(), _ && t._activeOps) {
301
+ Object.freeze({ loading: c.loading, error: f.message, errorCode: f.code })
302
+ ), t._notifyAsync(), d && t._activeOps) {
276
303
  const p = (t._activeOps.get(i) ?? 1) - 1;
277
304
  p <= 0 ? t._activeOps.delete(i) : t._activeOps.set(i, p);
278
305
  }
279
- throw l;
306
+ throw _;
280
307
  }
281
308
  );
282
309
  };
283
- s.push(i), t[i] = d;
284
- }), s.length > 0 && this.addCleanup(() => {
285
- const i = _ && t._activeOps ? new Map(t._activeOps) : null;
286
- for (const o of s)
287
- _ ? t[o] = () => {
288
- console.warn(`[mvc-kit] "${o}" called after dispose — ignored.`);
289
- } : t[o] = () => {
310
+ e.push(i), t[i] = l;
311
+ }), e.length > 0 && this.addCleanup(() => {
312
+ const i = d && t._activeOps ? new Map(t._activeOps) : null;
313
+ for (const n of e)
314
+ d ? t[n] = () => {
315
+ console.warn(`[mvc-kit] "${n}" called after dispose — ignored.`);
316
+ } : t[n] = () => {
290
317
  };
291
- t._asyncListeners.clear(), t._asyncStates.clear(), t._asyncSnapshots.clear(), _ && i && i.size > 0 && t._scheduleGhostCheck(i);
318
+ t._asyncListeners.clear(), t._asyncStates.clear(), t._asyncSnapshots.clear(), d && i && i.size > 0 && t._scheduleGhostCheck(i);
292
319
  });
293
320
  }
294
321
  _scheduleGhostCheck(t) {
295
- _ && setTimeout(() => {
296
- for (const [e, s] of t)
322
+ d && setTimeout(() => {
323
+ for (const [s, e] of t)
297
324
  console.warn(
298
- `[mvc-kit] Ghost async operation detected: "${e}" had ${s} pending call(s) when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.`
325
+ `[mvc-kit] Ghost async operation detected: "${s}" had ${e} pending call(s) when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.`
299
326
  );
300
327
  }, this.constructor.GHOST_TIMEOUT);
301
328
  }
@@ -312,13 +339,13 @@ class b {
312
339
  */
313
340
  _installStateProxy() {
314
341
  const t = new Proxy({}, {
315
- get: (e, s) => (this._stateTracking?.add(s), this._state[s]),
342
+ get: (s, e) => (this._stateTracking?.add(e), this._state[e]),
316
343
  ownKeys: () => Reflect.ownKeys(this._state),
317
- getOwnPropertyDescriptor: (e, s) => Reflect.getOwnPropertyDescriptor(this._state, s),
344
+ getOwnPropertyDescriptor: (s, e) => Reflect.getOwnPropertyDescriptor(this._state, e),
318
345
  set: () => {
319
346
  throw new Error("Cannot mutate state directly. Use set() instead.");
320
347
  },
321
- has: (e, s) => s in this._state
348
+ has: (s, e) => e in this._state
322
349
  });
323
350
  Object.defineProperty(this, "state", {
324
351
  get: () => this._stateTracking ? t : this._state,
@@ -343,21 +370,21 @@ class b {
343
370
  */
344
371
  _trackSubscribables() {
345
372
  for (const t of Object.getOwnPropertyNames(this)) {
346
- const e = this[t];
347
- if (!E(e)) continue;
348
- const s = {
349
- source: e,
373
+ const s = this[t];
374
+ if (!O(s)) continue;
375
+ const e = {
376
+ source: s,
350
377
  revision: 0,
351
- unsubscribe: e.subscribe(() => {
378
+ unsubscribe: s.subscribe(() => {
352
379
  if (!this._disposed) {
353
- s.revision++, this._revision++, this._state = Object.freeze({ ...this._state });
380
+ e.revision++, this._revision++, this._state = Object.freeze({ ...this._state });
354
381
  for (const i of this._listeners)
355
382
  i(this._state, this._state);
356
383
  }
357
384
  })
358
385
  };
359
- this._trackedSources.set(t, s), Object.defineProperty(this, t, {
360
- get: () => (this._sourceTracking?.set(t, s), e),
386
+ this._trackedSources.set(t, e), Object.defineProperty(this, t, {
387
+ get: () => (this._sourceTracking?.set(t, e), s),
361
388
  configurable: !0,
362
389
  enumerable: !1
363
390
  });
@@ -373,8 +400,8 @@ class b {
373
400
  */
374
401
  _memoizeGetters() {
375
402
  const t = /* @__PURE__ */ new Set();
376
- g(this, b.prototype, (e, s) => {
377
- !s.get || t.has(e) || (t.add(e), this._wrapGetter(e, s.get));
403
+ b(this, m.prototype, (s, e) => {
404
+ !e.get || t.has(s) || (t.add(s), this._wrapGetter(s, e.get));
378
405
  });
379
406
  }
380
407
  /**
@@ -387,58 +414,58 @@ class b {
387
414
  * Tier 2 (medium): revision changed but this getter's deps didn't → return cached
388
415
  * Tier 3 (slow): at least one dep changed → full recompute with tracking
389
416
  */
390
- _wrapGetter(t, e) {
391
- let s, i = -1, o, a, c;
417
+ _wrapGetter(t, s) {
418
+ let e, i = -1, n, a, o;
392
419
  Object.defineProperty(this, t, {
393
420
  get: () => {
394
421
  if (this._disposed || i === this._revision)
395
- return s;
396
- if (o && a) {
397
- let r = !0;
398
- for (const [l, u] of a)
399
- if (this._state[l] !== u) {
400
- r = !1;
422
+ return e;
423
+ if (n && a) {
424
+ let c = !0;
425
+ for (const [_, f] of a)
426
+ if (this._state[_] !== f) {
427
+ c = !1;
401
428
  break;
402
429
  }
403
- if (r && c)
404
- for (const [l, u] of c) {
405
- const p = this._trackedSources.get(l);
406
- if (p && p.revision !== u) {
407
- r = !1;
430
+ if (c && o)
431
+ for (const [_, f] of o) {
432
+ const p = this._trackedSources.get(_);
433
+ if (p && p.revision !== f) {
434
+ c = !1;
408
435
  break;
409
436
  }
410
437
  }
411
- if (r)
412
- return i = this._revision, s;
438
+ if (c)
439
+ return i = this._revision, e;
413
440
  }
414
- const d = this._stateTracking, f = this._sourceTracking;
441
+ const l = this._stateTracking, h = this._sourceTracking;
415
442
  this._stateTracking = /* @__PURE__ */ new Set(), this._sourceTracking = /* @__PURE__ */ new Map();
416
443
  try {
417
- s = e.call(this);
418
- } catch (r) {
419
- throw this._stateTracking = d, this._sourceTracking = f, r;
444
+ e = s.call(this);
445
+ } catch (c) {
446
+ throw this._stateTracking = l, this._sourceTracking = h, c;
420
447
  }
421
- o = this._stateTracking;
422
- const h = this._sourceTracking;
423
- if (this._stateTracking = d, this._sourceTracking = f, d)
424
- for (const r of o) d.add(r);
425
- if (f)
426
- for (const [r, l] of h)
427
- f.set(r, l);
448
+ n = this._stateTracking;
449
+ const u = this._sourceTracking;
450
+ if (this._stateTracking = l, this._sourceTracking = h, l)
451
+ for (const c of n) l.add(c);
452
+ if (h)
453
+ for (const [c, _] of u)
454
+ h.set(c, _);
428
455
  a = /* @__PURE__ */ new Map();
429
- for (const r of o)
430
- a.set(r, this._state[r]);
431
- c = /* @__PURE__ */ new Map();
432
- for (const [r, l] of h)
433
- c.set(r, l.revision);
434
- return i = this._revision, s;
456
+ for (const c of n)
457
+ a.set(c, this._state[c]);
458
+ o = /* @__PURE__ */ new Map();
459
+ for (const [c, _] of u)
460
+ o.set(c, _.revision);
461
+ return i = this._revision, e;
435
462
  },
436
463
  configurable: !0,
437
464
  enumerable: !0
438
465
  });
439
466
  }
440
467
  }
441
- class M {
468
+ class j {
442
469
  _state;
443
470
  _committed;
444
471
  _disposed = !1;
@@ -447,9 +474,10 @@ class M {
447
474
  _abortController = null;
448
475
  _cleanups = null;
449
476
  constructor(t) {
450
- const e = Object.freeze({ ...t });
451
- this._state = e, this._committed = e;
477
+ const s = Object.freeze({ ...t });
478
+ this._state = s, this._committed = s;
452
479
  }
480
+ /** Current frozen state object. */
453
481
  get state() {
454
482
  return this._state;
455
483
  }
@@ -477,31 +505,39 @@ class M {
477
505
  get valid() {
478
506
  return Object.keys(this.errors).length === 0;
479
507
  }
508
+ /** Whether this instance has been disposed. */
480
509
  get disposed() {
481
510
  return this._disposed;
482
511
  }
512
+ /** Whether init() has been called. */
483
513
  get initialized() {
484
514
  return this._initialized;
485
515
  }
516
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
486
517
  get disposeSignal() {
487
518
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
488
519
  }
520
+ /** Initializes the instance. Called automatically by React hooks after mount. */
489
521
  init() {
490
522
  if (!(this._initialized || this._disposed))
491
523
  return this._initialized = !0, this.onInit?.();
492
524
  }
525
+ /**
526
+ * Merges partial state with validation. No-op if no values changed by reference.
527
+ * @protected
528
+ */
493
529
  set(t) {
494
530
  if (this._disposed)
495
531
  throw new Error("Cannot set state on disposed Model");
496
- const e = typeof t == "function" ? t(this._state) : t;
497
- if (!Object.keys(e).some(
498
- (c) => e[c] !== this._state[c]
532
+ const s = typeof t == "function" ? t(this._state) : t;
533
+ if (!Object.keys(s).some(
534
+ (o) => s[o] !== this._state[o]
499
535
  ))
500
536
  return;
501
- const o = this._state, a = Object.freeze({ ...o, ...e });
502
- this._state = a, this.onSet?.(o, a);
503
- for (const c of this._listeners)
504
- c(a, o);
537
+ const n = this._state, a = Object.freeze({ ...n, ...s });
538
+ this._state = a, this.onSet?.(n, a);
539
+ for (const o of this._listeners)
540
+ o(a, n);
505
541
  }
506
542
  /**
507
543
  * Mark current state as the new baseline (not dirty).
@@ -521,15 +557,17 @@ class M {
521
557
  return;
522
558
  const t = this._state;
523
559
  this._state = this._committed, this.onSet?.(t, this._state);
524
- for (const e of this._listeners)
525
- e(this._state, t);
560
+ for (const s of this._listeners)
561
+ s(this._state, t);
526
562
  }
563
+ /** Subscribes to state changes. Returns an unsubscribe function. */
527
564
  subscribe(t) {
528
565
  return this._disposed ? () => {
529
566
  } : (this._listeners.add(t), () => {
530
567
  this._listeners.delete(t);
531
568
  });
532
569
  }
570
+ /** Tears down the instance, releasing all subscriptions and resources. */
533
571
  dispose() {
534
572
  if (!this._disposed) {
535
573
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -546,32 +584,54 @@ class M {
546
584
  validate(t) {
547
585
  return {};
548
586
  }
587
+ /** Registers a cleanup function to be called on dispose. @protected */
549
588
  addCleanup(t) {
550
589
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
551
590
  }
552
- subscribeTo(t, e) {
553
- const s = t.subscribe(e);
554
- return this.addCleanup(s), s;
591
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
592
+ subscribeTo(t, s) {
593
+ const e = t.subscribe(s);
594
+ return this.addCleanup(e), e;
555
595
  }
556
- shallowEqual(t, e) {
557
- const s = Object.keys(t), i = Object.keys(e);
558
- if (s.length !== i.length)
596
+ shallowEqual(t, s) {
597
+ const e = Object.keys(t), i = Object.keys(s);
598
+ if (e.length !== i.length)
559
599
  return !1;
560
- for (const o of s)
561
- if (t[o] !== e[o])
600
+ for (const n of e)
601
+ if (t[n] !== s[n])
562
602
  return !1;
563
603
  return !0;
564
604
  }
565
605
  }
566
- class x {
606
+ const x = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__;
607
+ class D {
608
+ /** Maximum number of items before FIFO eviction. 0 = unlimited. */
609
+ static MAX_SIZE = 0;
610
+ /** Time-to-live in milliseconds. 0 = no expiry. */
611
+ static TTL = 0;
567
612
  _items = [];
568
613
  _disposed = !1;
569
614
  _listeners = /* @__PURE__ */ new Set();
570
615
  _index = /* @__PURE__ */ new Map();
571
616
  _abortController = null;
572
617
  _cleanups = null;
618
+ _timestamps = null;
619
+ _evictionTimer = null;
573
620
  constructor(t = []) {
574
- this._items = Object.freeze([...t]), this.rebuildIndex();
621
+ let s = [...t];
622
+ if (this._ttl > 0) {
623
+ this._timestamps = /* @__PURE__ */ new Map();
624
+ const e = Date.now();
625
+ for (const i of s)
626
+ this._timestamps.set(i.id, e);
627
+ }
628
+ if (this._maxSize > 0 && s.length > this._maxSize) {
629
+ const e = s.length - this._maxSize, i = s.slice(0, e);
630
+ s = s.slice(e);
631
+ for (const n of i)
632
+ this._timestamps?.delete(n.id);
633
+ }
634
+ this._items = Object.freeze(s), this.rebuildIndex(), this._scheduleEvictionTimer();
575
635
  }
576
636
  /**
577
637
  * Alias for Subscribable compatibility.
@@ -579,32 +639,87 @@ class x {
579
639
  get state() {
580
640
  return this._items;
581
641
  }
642
+ /** The raw readonly array of items. */
582
643
  get items() {
583
644
  return this._items;
584
645
  }
646
+ /** Number of items in the collection. */
585
647
  get length() {
586
648
  return this._items.length;
587
649
  }
650
+ /** Whether this instance has been disposed. */
588
651
  get disposed() {
589
652
  return this._disposed;
590
653
  }
654
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
591
655
  get disposeSignal() {
592
656
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
593
657
  }
658
+ // ── Config Accessors ──
659
+ get _maxSize() {
660
+ return this.constructor.MAX_SIZE;
661
+ }
662
+ get _ttl() {
663
+ return this.constructor.TTL;
664
+ }
594
665
  // ── CRUD Methods (notify listeners) ──
595
666
  /**
596
- * Add one or more items.
667
+ * Add one or more items. Items with existing IDs are silently skipped.
597
668
  */
598
669
  add(...t) {
599
670
  if (this._disposed)
600
671
  throw new Error("Cannot add to disposed Collection");
601
672
  if (t.length === 0)
602
673
  return;
674
+ const s = /* @__PURE__ */ new Set(), e = [];
675
+ for (const a of t)
676
+ !this._index.has(a.id) && !s.has(a.id) && (e.push(a), s.add(a.id));
677
+ if (e.length === 0) return;
678
+ const i = this._items;
679
+ let n = [...i, ...e];
680
+ for (const a of e)
681
+ this._index.set(a.id, a);
682
+ if (this._timestamps) {
683
+ const a = Date.now();
684
+ for (const o of e)
685
+ this._timestamps.set(o.id, a);
686
+ }
687
+ this._maxSize > 0 && n.length > this._maxSize && (n = this._evictForCapacity(n)), this._items = Object.freeze(n), this.notify(i), this._scheduleEvictionTimer();
688
+ }
689
+ /**
690
+ * Add or replace items by ID. Existing items are replaced in-place
691
+ * (preserving array position); new items are appended. Deduplicates
692
+ * input — last occurrence wins. No-op if nothing changed (reference
693
+ * comparison).
694
+ */
695
+ upsert(...t) {
696
+ if (this._disposed)
697
+ throw new Error("Cannot upsert on disposed Collection");
698
+ if (t.length === 0) return;
699
+ const s = /* @__PURE__ */ new Map();
700
+ for (const l of t)
701
+ s.set(l.id, l);
603
702
  const e = this._items;
604
- this._items = Object.freeze([...e, ...t]);
605
- for (const s of t)
606
- this._index.set(s.id, s);
607
- this.notify(e);
703
+ let i = !1;
704
+ const n = /* @__PURE__ */ new Set(), a = [];
705
+ for (const l of e)
706
+ if (s.has(l.id)) {
707
+ const h = s.get(l.id);
708
+ h !== l && (i = !0), a.push(h), n.add(l.id);
709
+ } else
710
+ a.push(l);
711
+ for (const [l, h] of s)
712
+ n.has(l) || (a.push(h), i = !0);
713
+ if (!i) return;
714
+ if (this._timestamps) {
715
+ const l = Date.now();
716
+ for (const [h] of s)
717
+ this._timestamps.set(h, l);
718
+ }
719
+ for (const [l, h] of s)
720
+ this._index.set(l, h);
721
+ let o = a;
722
+ this._maxSize > 0 && o.length > this._maxSize && (o = this._evictForCapacity(o)), this._items = Object.freeze(o), this.notify(e), this._scheduleEvictionTimer();
608
723
  }
609
724
  /**
610
725
  * Remove items by id(s).
@@ -614,29 +729,29 @@ class x {
614
729
  throw new Error("Cannot remove from disposed Collection");
615
730
  if (t.length === 0)
616
731
  return;
617
- const e = new Set(t), s = this._items.filter((o) => !e.has(o.id));
618
- if (s.length === this._items.length)
732
+ const s = new Set(t), e = this._items.filter((n) => !s.has(n.id));
733
+ if (e.length === this._items.length)
619
734
  return;
620
735
  const i = this._items;
621
- this._items = Object.freeze(s);
622
- for (const o of t)
623
- this._index.delete(o);
624
- this.notify(i);
736
+ this._items = Object.freeze(e);
737
+ for (const n of t)
738
+ this._index.delete(n), this._timestamps?.delete(n);
739
+ this.notify(i), this._scheduleEvictionTimer();
625
740
  }
626
741
  /**
627
742
  * Update an item by id with partial changes.
628
743
  */
629
- update(t, e) {
744
+ update(t, s) {
630
745
  if (this._disposed)
631
746
  throw new Error("Cannot update disposed Collection");
632
- const s = this._items.findIndex((h) => h.id === t);
633
- if (s === -1)
747
+ const e = this._items.findIndex((u) => u.id === t);
748
+ if (e === -1)
634
749
  return;
635
- const i = this._items[s], o = { ...i, ...e, id: t };
636
- if (!Object.keys(e).some((h) => e[h] !== i[h]))
750
+ const i = this._items[e], n = { ...i, ...s, id: t };
751
+ if (!Object.keys(s).some((u) => s[u] !== i[u]))
637
752
  return;
638
- const d = this._items, f = [...d];
639
- f[s] = o, this._items = Object.freeze(f), this._index.set(t, o), this.notify(d);
753
+ const l = this._items, h = [...l];
754
+ h[e] = n, this._items = Object.freeze(h), this._index.set(t, n), this.notify(l);
640
755
  }
641
756
  /**
642
757
  * Replace all items.
@@ -644,8 +759,15 @@ class x {
644
759
  reset(t) {
645
760
  if (this._disposed)
646
761
  throw new Error("Cannot reset disposed Collection");
647
- const e = this._items;
648
- this._items = Object.freeze([...t]), this.rebuildIndex(), this.notify(e);
762
+ const s = this._items;
763
+ if (this._timestamps) {
764
+ this._timestamps.clear();
765
+ const i = Date.now();
766
+ for (const n of t)
767
+ this._timestamps.set(n.id, i);
768
+ }
769
+ let e = [...t];
770
+ this._maxSize > 0 && e.length > this._maxSize && (e = this._evictForCapacity(e)), this._items = Object.freeze(e), this.rebuildIndex(), this.notify(s), this._scheduleEvictionTimer();
649
771
  }
650
772
  /**
651
773
  * Remove all items.
@@ -656,7 +778,7 @@ class x {
656
778
  if (this._items.length === 0)
657
779
  return;
658
780
  const t = this._items;
659
- this._items = Object.freeze([]), this._index.clear(), this.notify(t);
781
+ this._items = Object.freeze([]), this._index.clear(), this._timestamps?.clear(), this._clearEvictionTimer(), this.notify(t);
660
782
  }
661
783
  /**
662
784
  * Snapshot current state, apply callback mutations, and return a rollback function.
@@ -665,14 +787,14 @@ class x {
665
787
  optimistic(t) {
666
788
  if (this._disposed)
667
789
  throw new Error("Cannot perform optimistic update on disposed Collection");
668
- const e = this._items;
790
+ const s = this._items, e = this._timestamps ? new Map(this._timestamps) : null;
669
791
  t();
670
- let s = !1;
792
+ let i = !1;
671
793
  return () => {
672
- if (s || this._disposed) return;
673
- s = !0;
674
- const i = this._items;
675
- this._items = e, this.rebuildIndex(), this.notify(i);
794
+ if (i || this._disposed) return;
795
+ i = !0;
796
+ const n = this._items;
797
+ this._items = s, e && (this._timestamps = e), this.rebuildIndex(), this.notify(n), this._scheduleEvictionTimer();
676
798
  };
677
799
  }
678
800
  // ── Query Methods (pure, no notification) ──
@@ -713,52 +835,129 @@ class x {
713
835
  return this._items.map(t);
714
836
  }
715
837
  // ── Subscribable interface ──
838
+ /** Subscribes to state changes. Returns an unsubscribe function. */
716
839
  subscribe(t) {
717
840
  return this._disposed ? () => {
718
841
  } : (this._listeners.add(t), () => {
719
842
  this._listeners.delete(t);
720
843
  });
721
844
  }
845
+ /** Tears down the instance, releasing all subscriptions and resources. */
722
846
  dispose() {
723
847
  if (!this._disposed) {
724
- if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
848
+ if (this._disposed = !0, this._clearEvictionTimer(), this._abortController?.abort(), this._cleanups) {
725
849
  for (const t of this._cleanups) t();
726
850
  this._cleanups = null;
727
851
  }
728
- this.onDispose?.(), this._listeners.clear(), this._index.clear();
852
+ this.onDispose?.(), this._listeners.clear(), this._index.clear(), this._timestamps?.clear();
729
853
  }
730
854
  }
855
+ /** Registers a cleanup function to be called on dispose. @protected */
731
856
  addCleanup(t) {
732
857
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
733
858
  }
734
859
  notify(t) {
735
- for (const e of this._listeners)
736
- e(this._items, t);
860
+ for (const s of this._listeners)
861
+ s(this._items, t);
737
862
  }
738
863
  rebuildIndex() {
739
864
  this._index.clear();
740
865
  for (const t of this._items)
741
866
  this._index.set(t.id, t);
742
867
  }
868
+ // ── Eviction Internals ──
869
+ _evictForCapacity(t) {
870
+ const s = t.length - this._maxSize;
871
+ if (s <= 0) return t;
872
+ const e = t.slice(0, s), i = this._applyOnEvict(e, "capacity");
873
+ if (i === !1 || i.length === 0) return t;
874
+ const n = new Set(i.map((o) => o.id)), a = t.filter((o) => !n.has(o.id));
875
+ for (const o of i)
876
+ this._index.delete(o.id), this._timestamps?.delete(o.id);
877
+ return a;
878
+ }
879
+ _applyOnEvict(t, s) {
880
+ if (!this.onEvict) return t;
881
+ const e = this.onEvict(t, s);
882
+ if (e === !1) {
883
+ if (x && s === "capacity" && this._maxSize > 0) {
884
+ const i = this._items.length + t.length;
885
+ i > this._maxSize * 2 && console.warn(
886
+ `[mvc-kit] Collection exceeded 2x MAX_SIZE (${i}/${this._maxSize}). onEvict is vetoing eviction — this may cause unbounded growth.`
887
+ );
888
+ }
889
+ return !1;
890
+ }
891
+ if (Array.isArray(e)) {
892
+ const i = new Set(t.map((n) => n.id));
893
+ return e.filter((n) => i.has(n.id));
894
+ }
895
+ return t;
896
+ }
897
+ _sweepExpired() {
898
+ if (this._disposed || !this._timestamps || this._ttl <= 0) return;
899
+ const t = Date.now(), s = this._ttl, e = [];
900
+ for (const o of this._items) {
901
+ const l = this._timestamps.get(o.id);
902
+ l !== void 0 && t - l >= s && e.push(o);
903
+ }
904
+ if (e.length === 0) {
905
+ this._scheduleEvictionTimer();
906
+ return;
907
+ }
908
+ const i = this._applyOnEvict(e, "ttl");
909
+ if (i === !1) {
910
+ this._scheduleEvictionTimer();
911
+ return;
912
+ }
913
+ if (i.length === 0) {
914
+ this._scheduleEvictionTimer();
915
+ return;
916
+ }
917
+ const n = new Set(i.map((o) => o.id)), a = this._items;
918
+ this._items = Object.freeze(
919
+ a.filter((o) => !n.has(o.id))
920
+ );
921
+ for (const o of i)
922
+ this._index.delete(o.id), this._timestamps.delete(o.id);
923
+ this.notify(a), this._scheduleEvictionTimer();
924
+ }
925
+ _scheduleEvictionTimer() {
926
+ if (this._clearEvictionTimer(), this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;
927
+ const t = Date.now(), s = this._ttl;
928
+ let e = 1 / 0;
929
+ for (const n of this._timestamps.values())
930
+ n < e && (e = n);
931
+ const i = Math.max(0, e + s - t);
932
+ this._evictionTimer = setTimeout(() => this._sweepExpired(), i);
933
+ }
934
+ _clearEvictionTimer() {
935
+ this._evictionTimer !== null && (clearTimeout(this._evictionTimer), this._evictionTimer = null);
936
+ }
743
937
  }
744
- class D {
938
+ class I {
745
939
  _disposed = !1;
746
940
  _initialized = !1;
747
941
  _abortController = null;
748
942
  _cleanups = null;
943
+ /** Whether this instance has been disposed. */
749
944
  get disposed() {
750
945
  return this._disposed;
751
946
  }
947
+ /** Whether init() has been called. */
752
948
  get initialized() {
753
949
  return this._initialized;
754
950
  }
951
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
755
952
  get disposeSignal() {
756
953
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
757
954
  }
955
+ /** Initializes the instance. Called automatically by React hooks after mount. */
758
956
  init() {
759
957
  if (!(this._initialized || this._disposed))
760
958
  return this._initialized = !0, this.onInit?.();
761
959
  }
960
+ /** Tears down the instance, releasing all subscriptions and resources. */
762
961
  dispose() {
763
962
  if (!this._disposed) {
764
963
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -768,12 +967,14 @@ class D {
768
967
  this.onDispose?.();
769
968
  }
770
969
  }
970
+ /** Registers a cleanup function to be called on dispose. @protected */
771
971
  addCleanup(t) {
772
972
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
773
973
  }
774
- subscribeTo(t, e) {
775
- const s = t.subscribe(e);
776
- return this.addCleanup(s), s;
974
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
975
+ subscribeTo(t, s) {
976
+ const e = t.subscribe(s);
977
+ return this.addCleanup(e), e;
777
978
  }
778
979
  }
779
980
  class P {
@@ -781,19 +982,24 @@ class P {
781
982
  _initialized = !1;
782
983
  _abortController = null;
783
984
  _cleanups = null;
985
+ /** Whether this instance has been disposed. */
784
986
  get disposed() {
785
987
  return this._disposed;
786
988
  }
989
+ /** Whether init() has been called. */
787
990
  get initialized() {
788
991
  return this._initialized;
789
992
  }
993
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
790
994
  get disposeSignal() {
791
995
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
792
996
  }
997
+ /** Initializes the instance. Called automatically by React hooks after mount. */
793
998
  init() {
794
999
  if (!(this._initialized || this._disposed))
795
1000
  return this._initialized = !0, this.onInit?.();
796
1001
  }
1002
+ /** Tears down the instance, releasing all subscriptions and resources. */
797
1003
  dispose() {
798
1004
  if (!this._disposed) {
799
1005
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -803,21 +1009,26 @@ class P {
803
1009
  this.onDispose?.();
804
1010
  }
805
1011
  }
1012
+ /** Registers a cleanup function to be called on dispose. @protected */
806
1013
  addCleanup(t) {
807
1014
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
808
1015
  }
809
1016
  }
810
- const m = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__, k = Object.freeze({
1017
+ const g = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__, k = Object.freeze({
811
1018
  connected: !1,
812
1019
  reconnecting: !1,
813
1020
  attempt: 0,
814
1021
  error: null
815
1022
  });
816
- class I {
1023
+ class R {
817
1024
  // Static config (subclass overrides)
1025
+ /** Base delay (ms) for reconnection backoff. */
818
1026
  static RECONNECT_BASE = 1e3;
1027
+ /** Maximum delay cap (ms) for reconnection backoff. */
819
1028
  static RECONNECT_MAX = 3e4;
1029
+ /** Exponential backoff multiplier for reconnection delay. */
820
1030
  static RECONNECT_FACTOR = 2;
1031
+ /** Maximum number of reconnection attempts before giving up. */
821
1032
  static MAX_ATTEMPTS = 1 / 0;
822
1033
  // ── Internal state ──────────────────────────────────────────────
823
1034
  _status = k;
@@ -831,9 +1042,11 @@ class I {
831
1042
  _reconnectTimer = null;
832
1043
  _cleanups = null;
833
1044
  // ── Subscribable<ChannelStatus> ─────────────────────────────────
1045
+ /** Current connection status. */
834
1046
  get state() {
835
1047
  return this._status;
836
1048
  }
1049
+ /** Subscribes to connection status changes. Returns an unsubscribe function. */
837
1050
  subscribe(t) {
838
1051
  return this._disposed ? () => {
839
1052
  } : (this._listeners.add(t), () => {
@@ -841,19 +1054,24 @@ class I {
841
1054
  });
842
1055
  }
843
1056
  // ── Disposable / Initializable ──────────────────────────────────
1057
+ /** Whether this instance has been disposed. */
844
1058
  get disposed() {
845
1059
  return this._disposed;
846
1060
  }
1061
+ /** Whether init() has been called. */
847
1062
  get initialized() {
848
1063
  return this._initialized;
849
1064
  }
1065
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
850
1066
  get disposeSignal() {
851
1067
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
852
1068
  }
1069
+ /** Initializes the instance. Called automatically by React hooks after mount. */
853
1070
  init() {
854
1071
  if (!(this._initialized || this._disposed))
855
1072
  return this._initialized = !0, this.onInit?.();
856
1073
  }
1074
+ /** Tears down the instance, releasing all subscriptions and resources. */
857
1075
  dispose() {
858
1076
  if (!this._disposed) {
859
1077
  this._disposed = !0, this._connState = 4, this._reconnectTimer !== null && (clearTimeout(this._reconnectTimer), this._reconnectTimer = null), this._connectAbort?.abort(), this._connectAbort = null, this._abortController?.abort();
@@ -869,13 +1087,15 @@ class I {
869
1087
  }
870
1088
  }
871
1089
  // ── Connection control ──────────────────────────────────────────
1090
+ /** Initiates a connection with automatic reconnection on failure. */
872
1091
  connect() {
873
1092
  if (this._disposed) {
874
- m && console.warn("[mvc-kit] connect() called after dispose — ignored.");
1093
+ g && console.warn("[mvc-kit] connect() called after dispose — ignored.");
875
1094
  return;
876
1095
  }
877
- m && !this._initialized && console.warn("[mvc-kit] connect() called before init()."), !(this._connState === 1 || this._connState === 2) && (this._reconnectTimer !== null && (clearTimeout(this._reconnectTimer), this._reconnectTimer = null), this._attemptConnect(0));
1096
+ g && !this._initialized && console.warn("[mvc-kit] connect() called before init()."), !(this._connState === 1 || this._connState === 2) && (this._reconnectTimer !== null && (clearTimeout(this._reconnectTimer), this._reconnectTimer = null), this._attemptConnect(0));
878
1097
  }
1098
+ /** Closes the connection and cancels any pending reconnection. */
879
1099
  disconnect() {
880
1100
  if (!this._disposed) {
881
1101
  if (this._reconnectTimer !== null && (clearTimeout(this._reconnectTimer), this._reconnectTimer = null), this._connectAbort?.abort(), this._connectAbort = null, this._connState === 2 || this._connState === 1) {
@@ -890,77 +1110,84 @@ class I {
890
1110
  }
891
1111
  }
892
1112
  // ── Subclass signals ────────────────────────────────────────────
893
- receive(t, e) {
1113
+ /** Call from subclass when a message arrives from the transport. @protected */
1114
+ receive(t, s) {
894
1115
  if (this._disposed) {
895
- m && console.warn(`[mvc-kit] receive("${String(t)}") called after dispose — ignored.`);
1116
+ g && console.warn(`[mvc-kit] receive("${String(t)}") called after dispose — ignored.`);
896
1117
  return;
897
1118
  }
898
- const s = this._handlers.get(t);
899
- if (s)
900
- for (const i of s)
901
- i(e);
1119
+ const e = this._handlers.get(t);
1120
+ if (e)
1121
+ for (const i of e)
1122
+ i(s);
902
1123
  }
1124
+ /** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */
903
1125
  disconnected() {
904
1126
  this._disposed || this._connState !== 2 && this._connState !== 1 || (this._connectAbort?.abort(), this._connectAbort = null, this._connState = 3, this._scheduleReconnect(1));
905
1127
  }
906
1128
  // ── Consumer API ────────────────────────────────────────────────
907
- on(t, e) {
1129
+ /** Subscribes to a specific message type. Returns an unsubscribe function. */
1130
+ on(t, s) {
908
1131
  if (this._disposed) return () => {
909
1132
  };
910
- let s = this._handlers.get(t);
911
- return s || (s = /* @__PURE__ */ new Set(), this._handlers.set(t, s)), s.add(e), () => {
912
- s.delete(e);
1133
+ let e = this._handlers.get(t);
1134
+ return e || (e = /* @__PURE__ */ new Set(), this._handlers.set(t, e)), e.add(s), () => {
1135
+ e.delete(s);
913
1136
  };
914
1137
  }
915
- once(t, e) {
916
- const s = this.on(t, (i) => {
917
- s(), e(i);
1138
+ /** Subscribes to a message type, auto-removing the handler after the first invocation. */
1139
+ once(t, s) {
1140
+ const e = this.on(t, (i) => {
1141
+ e(), s(i);
918
1142
  });
919
- return s;
1143
+ return e;
920
1144
  }
921
1145
  // ── Infrastructure ──────────────────────────────────────────────
1146
+ /** Registers a cleanup function to be called on dispose. @protected */
922
1147
  addCleanup(t) {
923
1148
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
924
1149
  }
925
- subscribeTo(t, e) {
926
- const s = t.subscribe(e);
927
- return this.addCleanup(s), s;
1150
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
1151
+ subscribeTo(t, s) {
1152
+ const e = t.subscribe(s);
1153
+ return this.addCleanup(e), e;
928
1154
  }
929
1155
  // ── Backoff ─────────────────────────────────────────────────────
1156
+ /** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */
930
1157
  _calculateDelay(t) {
931
- const e = this.constructor, s = Math.min(
932
- e.RECONNECT_BASE * Math.pow(e.RECONNECT_FACTOR, t),
933
- e.RECONNECT_MAX
1158
+ const s = this.constructor, e = Math.min(
1159
+ s.RECONNECT_BASE * Math.pow(s.RECONNECT_FACTOR, t),
1160
+ s.RECONNECT_MAX
934
1161
  );
935
- return Math.random() * s;
1162
+ return Math.random() * e;
936
1163
  }
937
1164
  // ── Internals ───────────────────────────────────────────────────
938
1165
  _setStatus(t) {
939
- const e = this._status;
940
- if (!(e.connected === t.connected && e.reconnecting === t.reconnecting && e.attempt === t.attempt && e.error === t.error)) {
1166
+ const s = this._status;
1167
+ if (!(s.connected === t.connected && s.reconnecting === t.reconnecting && s.attempt === t.attempt && s.error === t.error)) {
941
1168
  this._status = Object.freeze(t);
942
- for (const s of this._listeners)
943
- s(this._status, e);
1169
+ for (const e of this._listeners)
1170
+ e(this._status, s);
944
1171
  }
945
1172
  }
946
1173
  _attemptConnect(t) {
947
1174
  if (this._disposed) return;
948
1175
  this._connState = 1, this._connectAbort?.abort(), this._connectAbort = new AbortController();
949
- const e = this._abortController ? AbortSignal.any([this._abortController.signal, this._connectAbort.signal]) : this._connectAbort.signal;
1176
+ const s = this._abortController ? AbortSignal.any([this._abortController.signal, this._connectAbort.signal]) : this._connectAbort.signal;
950
1177
  this._setStatus({
951
1178
  connected: !1,
952
1179
  reconnecting: t > 0,
953
1180
  attempt: t,
954
1181
  error: null
955
1182
  });
956
- let s;
1183
+ let e;
957
1184
  try {
958
- s = this.open(e);
1185
+ e = this.open(s);
959
1186
  } catch (i) {
960
1187
  this._onOpenFailed(t, i);
961
1188
  return;
962
1189
  }
963
- s && typeof s.then == "function" ? s.then(
1190
+ e && typeof e.then == "function" ? e.then(
964
1191
  () => this._onOpenSucceeded(),
965
1192
  (i) => this._onOpenFailed(t, i)
966
1193
  ) : this._onOpenSucceeded();
@@ -973,47 +1200,47 @@ class I {
973
1200
  error: null
974
1201
  }));
975
1202
  }
976
- _onOpenFailed(t, e) {
977
- this._disposed || this._connState !== 0 && (this._connectAbort?.abort(), this._connectAbort = null, this._connState = 3, this._scheduleReconnect(t + 1, e));
1203
+ _onOpenFailed(t, s) {
1204
+ this._disposed || this._connState !== 0 && (this._connectAbort?.abort(), this._connectAbort = null, this._connState = 3, this._scheduleReconnect(t + 1, s));
978
1205
  }
979
- _scheduleReconnect(t, e) {
980
- const s = this.constructor;
981
- if (t > s.MAX_ATTEMPTS) {
1206
+ _scheduleReconnect(t, s) {
1207
+ const e = this.constructor;
1208
+ if (t > e.MAX_ATTEMPTS) {
982
1209
  this._connState = 0, this._setStatus({
983
1210
  connected: !1,
984
1211
  reconnecting: !1,
985
1212
  attempt: t,
986
- error: e instanceof Error ? e.message : "Max reconnection attempts reached"
1213
+ error: s instanceof Error ? s.message : "Max reconnection attempts reached"
987
1214
  });
988
1215
  return;
989
1216
  }
990
- const i = e instanceof Error ? e.message : e ? String(e) : null;
1217
+ const i = s instanceof Error ? s.message : s ? String(s) : null;
991
1218
  this._setStatus({
992
1219
  connected: !1,
993
1220
  reconnecting: !0,
994
1221
  attempt: t,
995
1222
  error: i
996
1223
  });
997
- const o = this._calculateDelay(t - 1);
1224
+ const n = this._calculateDelay(t - 1);
998
1225
  this._reconnectTimer = setTimeout(() => {
999
1226
  this._reconnectTimer = null, this._attemptConnect(t);
1000
- }, o);
1227
+ }, n);
1001
1228
  }
1002
1229
  }
1003
1230
  export {
1004
- I as Channel,
1005
- x as Collection,
1006
- D as Controller,
1007
- y as EventBus,
1008
- w as HttpError,
1009
- M as Model,
1231
+ R as Channel,
1232
+ D as Collection,
1233
+ I as Controller,
1234
+ w as EventBus,
1235
+ C as HttpError,
1236
+ j as Model,
1010
1237
  P as Service,
1011
- b as ViewModel,
1012
- O as classifyError,
1238
+ m as ViewModel,
1239
+ E as classifyError,
1013
1240
  N as hasSingleton,
1014
- v as isAbortError,
1015
- V as singleton,
1016
- B as teardown,
1017
- L as teardownAll
1241
+ y as isAbortError,
1242
+ $ as singleton,
1243
+ L as teardown,
1244
+ B as teardownAll
1018
1245
  };
1019
1246
  //# sourceMappingURL=mvc-kit.js.map