mvc-kit 2.2.2 → 2.2.3

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 +5 -4
  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 +4 -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 +14 -16
  12. package/agent-config/cursor/cursorrules +14 -16
  13. package/dist/Channel.d.ts +29 -0
  14. package/dist/Channel.d.ts.map +1 -1
  15. package/dist/Collection.d.ts +16 -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 +226 -111
  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,6 +1,6 @@
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 {
1
+ import { E as w } from "./singleton-CaEXSbYg.js";
2
+ import { h as N, s as V, t as B, a as L } from "./singleton-CaEXSbYg.js";
3
+ class y extends Error {
4
4
  constructor(t, e) {
5
5
  super(e ?? `HTTP ${t}`), this.status = t, this.name = "HttpError";
6
6
  }
@@ -19,7 +19,7 @@ function O(n) {
19
19
  code: "abort",
20
20
  message: "Request was aborted",
21
21
  original: n
22
- } : n instanceof w ? {
22
+ } : n instanceof y ? {
23
23
  code: C(n.status),
24
24
  message: n.message,
25
25
  status: n.status,
@@ -47,7 +47,7 @@ function O(n) {
47
47
  original: n
48
48
  };
49
49
  }
50
- const _ = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__;
50
+ const u = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__;
51
51
  function E(n) {
52
52
  return n !== null && typeof n == "object" && typeof n.subscribe == "function";
53
53
  }
@@ -55,8 +55,8 @@ function g(n, t, e) {
55
55
  let s = Object.getPrototypeOf(n);
56
56
  for (; s && s !== t; ) {
57
57
  const i = Object.getOwnPropertyDescriptors(s);
58
- for (const [o, a] of Object.entries(i))
59
- o !== "constructor" && e(o, a, s);
58
+ for (const [r, c] of Object.entries(i))
59
+ r !== "constructor" && e(r, c, s);
60
60
  s = Object.getPrototypeOf(s);
61
61
  }
62
62
  }
@@ -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,7 +124,7 @@ class b {
117
124
  */
118
125
  set(t) {
119
126
  if (this._disposed) return;
120
- if (_ && this._stateTracking) {
127
+ if (u && 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
  );
@@ -125,23 +132,30 @@ class b {
125
132
  }
126
133
  const e = typeof t == "function" ? t(this._state) : t;
127
134
  if (!Object.keys(e).some(
128
- (c) => e[c] !== this._state[c]
135
+ (a) => e[a] !== this._state[a]
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 r = this._state, c = Object.freeze({ ...r, ...e });
139
+ this._state = c, this._revision++, this.onSet?.(r, c);
140
+ for (const a of this._listeners)
141
+ a(c, r);
135
142
  }
143
+ /**
144
+ * Emits a typed event via the internal EventBus.
145
+ * Safe to call during dispose cleanup callbacks.
146
+ * @protected
147
+ */
136
148
  emit(t, e) {
137
149
  (this._eventBus?.disposed ?? this._disposed) || this.events.emit(t, e);
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,6 +165,10 @@ 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();
@@ -159,14 +177,22 @@ class b {
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
  }
189
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
165
190
  subscribeTo(t, e) {
166
191
  const s = t.subscribe(e);
167
192
  return this._subscriptionCleanups || (this._subscriptionCleanups = []), this._subscriptionCleanups.push(s), s;
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;
@@ -188,6 +214,7 @@ class b {
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), () => {
@@ -221,78 +248,78 @@ class b {
221
248
  `[mvc-kit] "${i}" is a reserved property on ViewModel and cannot be overridden.`
222
249
  );
223
250
  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;
251
+ u && (this._activeOps = /* @__PURE__ */ new Map()), g(this, b.prototype, (i, r) => {
252
+ if (r.get || r.set || typeof r.value != "function" || i.startsWith("_") || z.has(i) || e.has(i)) return;
226
253
  e.add(i);
227
- const a = o.value;
228
- let c = !1;
229
- const d = function(...f) {
254
+ const c = r.value;
255
+ let a = !1;
256
+ const l = function(...f) {
230
257
  if (t._disposed) {
231
- _ && console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);
258
+ u && console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);
232
259
  return;
233
260
  }
234
- _ && !t._initialized && console.warn(
261
+ u && !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 _;
238
265
  try {
239
- h = a.apply(t, f);
240
- } catch (l) {
241
- throw l;
266
+ _ = c.apply(t, f);
267
+ } catch (h) {
268
+ throw h;
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 (!_ || typeof _.then != "function")
271
+ return a || (a = !0, t._asyncStates.delete(i), t._asyncSnapshots.delete(i), t[i] = c.bind(t)), _;
272
+ let o = t._asyncStates.get(i);
273
+ return o || (o = { loading: !1, error: null, errorCode: null, count: 0 }, t._asyncStates.set(i, o)), o.count++, o.loading = !0, o.error = null, o.errorCode = null, t._asyncSnapshots.set(i, Object.freeze({ loading: !0, error: null, errorCode: null })), t._notifyAsync(), u && t._activeOps && t._activeOps.set(i, (t._activeOps.get(i) ?? 0) + 1), _.then(
274
+ (h) => {
275
+ if (t._disposed) return h;
276
+ if (o.count--, o.loading = o.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: o.loading, error: o.error, errorCode: o.errorCode })
279
+ ), t._notifyAsync(), u && t._activeOps) {
280
+ const d = (t._activeOps.get(i) ?? 1) - 1;
281
+ d <= 0 ? t._activeOps.delete(i) : t._activeOps.set(i, d);
255
282
  }
256
- return l;
283
+ return h;
257
284
  },
258
- (l) => {
259
- if (v(l)) {
260
- if (t._disposed || (r.count--, r.loading = r.count > 0, t._asyncSnapshots.set(
285
+ (h) => {
286
+ if (v(h)) {
287
+ if (t._disposed || (o.count--, o.loading = o.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: o.loading, error: o.error, errorCode: o.errorCode })
290
+ ), t._notifyAsync()), u && 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
+ o.count--, o.loading = o.count > 0;
298
+ const d = O(h);
299
+ if (o.error = d.message, o.errorCode = d.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: o.loading, error: d.message, errorCode: d.code })
302
+ ), t._notifyAsync(), u && 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 h;
280
307
  }
281
308
  );
282
309
  };
283
- s.push(i), t[i] = d;
310
+ s.push(i), t[i] = l;
284
311
  }), 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] = () => {
312
+ const i = u && t._activeOps ? new Map(t._activeOps) : null;
313
+ for (const r of s)
314
+ u ? t[r] = () => {
315
+ console.warn(`[mvc-kit] "${r}" called after dispose — ignored.`);
316
+ } : t[r] = () => {
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(), u && i && i.size > 0 && t._scheduleGhostCheck(i);
292
319
  });
293
320
  }
294
321
  _scheduleGhostCheck(t) {
295
- _ && setTimeout(() => {
322
+ u && setTimeout(() => {
296
323
  for (const [e, s] of t)
297
324
  console.warn(
298
325
  `[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.`
@@ -388,49 +415,49 @@ class b {
388
415
  * Tier 3 (slow): at least one dep changed → full recompute with tracking
389
416
  */
390
417
  _wrapGetter(t, e) {
391
- let s, i = -1, o, a, c;
418
+ let s, i = -1, r, c, a;
392
419
  Object.defineProperty(this, t, {
393
420
  get: () => {
394
421
  if (this._disposed || i === this._revision)
395
422
  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;
423
+ if (r && c) {
424
+ let o = !0;
425
+ for (const [h, d] of c)
426
+ if (this._state[h] !== d) {
427
+ o = !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 (o && a)
431
+ for (const [h, d] of a) {
432
+ const p = this._trackedSources.get(h);
433
+ if (p && p.revision !== d) {
434
+ o = !1;
408
435
  break;
409
436
  }
410
437
  }
411
- if (r)
438
+ if (o)
412
439
  return i = this._revision, s;
413
440
  }
414
- const d = this._stateTracking, f = this._sourceTracking;
441
+ const l = this._stateTracking, f = this._sourceTracking;
415
442
  this._stateTracking = /* @__PURE__ */ new Set(), this._sourceTracking = /* @__PURE__ */ new Map();
416
443
  try {
417
444
  s = e.call(this);
418
- } catch (r) {
419
- throw this._stateTracking = d, this._sourceTracking = f, r;
445
+ } catch (o) {
446
+ throw this._stateTracking = l, this._sourceTracking = f, o;
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);
448
+ r = this._stateTracking;
449
+ const _ = this._sourceTracking;
450
+ if (this._stateTracking = l, this._sourceTracking = f, l)
451
+ for (const o of r) l.add(o);
425
452
  if (f)
426
- for (const [r, l] of h)
427
- f.set(r, l);
428
- a = /* @__PURE__ */ new Map();
429
- for (const r of o)
430
- a.set(r, this._state[r]);
453
+ for (const [o, h] of _)
454
+ f.set(o, h);
431
455
  c = /* @__PURE__ */ new Map();
432
- for (const [r, l] of h)
433
- c.set(r, l.revision);
456
+ for (const o of r)
457
+ c.set(o, this._state[o]);
458
+ a = /* @__PURE__ */ new Map();
459
+ for (const [o, h] of _)
460
+ a.set(o, h.revision);
434
461
  return i = this._revision, s;
435
462
  },
436
463
  configurable: !0,
@@ -450,6 +477,7 @@ class M {
450
477
  const e = Object.freeze({ ...t });
451
478
  this._state = e, this._committed = e;
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
532
  const e = typeof t == "function" ? t(this._state) : t;
497
533
  if (!Object.keys(e).some(
498
- (c) => e[c] !== this._state[c]
534
+ (a) => e[a] !== this._state[a]
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 r = this._state, c = Object.freeze({ ...r, ...e });
538
+ this._state = c, this.onSet?.(r, c);
539
+ for (const a of this._listeners)
540
+ a(c, r);
505
541
  }
506
542
  /**
507
543
  * Mark current state as the new baseline (not dirty).
@@ -524,12 +560,14 @@ class M {
524
560
  for (const e of this._listeners)
525
561
  e(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,9 +584,11 @@ 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
  }
591
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
552
592
  subscribeTo(t, e) {
553
593
  const s = t.subscribe(e);
554
594
  return this.addCleanup(s), s;
@@ -557,8 +597,8 @@ class M {
557
597
  const s = Object.keys(t), i = Object.keys(e);
558
598
  if (s.length !== i.length)
559
599
  return !1;
560
- for (const o of s)
561
- if (t[o] !== e[o])
600
+ for (const r of s)
601
+ if (t[r] !== e[r])
562
602
  return !1;
563
603
  return !0;
564
604
  }
@@ -579,32 +619,71 @@ class x {
579
619
  get state() {
580
620
  return this._items;
581
621
  }
622
+ /** The raw readonly array of items. */
582
623
  get items() {
583
624
  return this._items;
584
625
  }
626
+ /** Number of items in the collection. */
585
627
  get length() {
586
628
  return this._items.length;
587
629
  }
630
+ /** Whether this instance has been disposed. */
588
631
  get disposed() {
589
632
  return this._disposed;
590
633
  }
634
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
591
635
  get disposeSignal() {
592
636
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
593
637
  }
594
638
  // ── CRUD Methods (notify listeners) ──
595
639
  /**
596
- * Add one or more items.
640
+ * Add one or more items. Items with existing IDs are silently skipped.
597
641
  */
598
642
  add(...t) {
599
643
  if (this._disposed)
600
644
  throw new Error("Cannot add to disposed Collection");
601
645
  if (t.length === 0)
602
646
  return;
603
- 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);
647
+ const e = /* @__PURE__ */ new Set(), s = [];
648
+ for (const r of t)
649
+ !this._index.has(r.id) && !e.has(r.id) && (s.push(r), e.add(r.id));
650
+ if (s.length === 0) return;
651
+ const i = this._items;
652
+ this._items = Object.freeze([...i, ...s]);
653
+ for (const r of s)
654
+ this._index.set(r.id, r);
655
+ this.notify(i);
656
+ }
657
+ /**
658
+ * Add or replace items by ID. Existing items are replaced in-place
659
+ * (preserving array position); new items are appended. Deduplicates
660
+ * input — last occurrence wins. No-op if nothing changed (reference
661
+ * comparison).
662
+ */
663
+ upsert(...t) {
664
+ if (this._disposed)
665
+ throw new Error("Cannot upsert on disposed Collection");
666
+ if (t.length === 0) return;
667
+ const e = /* @__PURE__ */ new Map();
668
+ for (const a of t)
669
+ e.set(a.id, a);
670
+ const s = this._items;
671
+ let i = !1;
672
+ const r = /* @__PURE__ */ new Set(), c = [];
673
+ for (const a of s)
674
+ if (e.has(a.id)) {
675
+ const l = e.get(a.id);
676
+ l !== a && (i = !0), c.push(l), r.add(a.id);
677
+ } else
678
+ c.push(a);
679
+ for (const [a, l] of e)
680
+ r.has(a) || (c.push(l), i = !0);
681
+ if (i) {
682
+ this._items = Object.freeze(c);
683
+ for (const [a, l] of e)
684
+ this._index.set(a, l);
685
+ this.notify(s);
686
+ }
608
687
  }
609
688
  /**
610
689
  * Remove items by id(s).
@@ -614,13 +693,13 @@ class x {
614
693
  throw new Error("Cannot remove from disposed Collection");
615
694
  if (t.length === 0)
616
695
  return;
617
- const e = new Set(t), s = this._items.filter((o) => !e.has(o.id));
696
+ const e = new Set(t), s = this._items.filter((r) => !e.has(r.id));
618
697
  if (s.length === this._items.length)
619
698
  return;
620
699
  const i = this._items;
621
700
  this._items = Object.freeze(s);
622
- for (const o of t)
623
- this._index.delete(o);
701
+ for (const r of t)
702
+ this._index.delete(r);
624
703
  this.notify(i);
625
704
  }
626
705
  /**
@@ -629,14 +708,14 @@ class x {
629
708
  update(t, e) {
630
709
  if (this._disposed)
631
710
  throw new Error("Cannot update disposed Collection");
632
- const s = this._items.findIndex((h) => h.id === t);
711
+ const s = this._items.findIndex((_) => _.id === t);
633
712
  if (s === -1)
634
713
  return;
635
- const i = this._items[s], o = { ...i, ...e, id: t };
636
- if (!Object.keys(e).some((h) => e[h] !== i[h]))
714
+ const i = this._items[s], r = { ...i, ...e, id: t };
715
+ if (!Object.keys(e).some((_) => e[_] !== i[_]))
637
716
  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);
717
+ const l = this._items, f = [...l];
718
+ f[s] = r, this._items = Object.freeze(f), this._index.set(t, r), this.notify(l);
640
719
  }
641
720
  /**
642
721
  * Replace all items.
@@ -713,12 +792,14 @@ class x {
713
792
  return this._items.map(t);
714
793
  }
715
794
  // ── Subscribable interface ──
795
+ /** Subscribes to state changes. Returns an unsubscribe function. */
716
796
  subscribe(t) {
717
797
  return this._disposed ? () => {
718
798
  } : (this._listeners.add(t), () => {
719
799
  this._listeners.delete(t);
720
800
  });
721
801
  }
802
+ /** Tears down the instance, releasing all subscriptions and resources. */
722
803
  dispose() {
723
804
  if (!this._disposed) {
724
805
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -728,6 +809,7 @@ class x {
728
809
  this.onDispose?.(), this._listeners.clear(), this._index.clear();
729
810
  }
730
811
  }
812
+ /** Registers a cleanup function to be called on dispose. @protected */
731
813
  addCleanup(t) {
732
814
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
733
815
  }
@@ -746,19 +828,24 @@ class D {
746
828
  _initialized = !1;
747
829
  _abortController = null;
748
830
  _cleanups = null;
831
+ /** Whether this instance has been disposed. */
749
832
  get disposed() {
750
833
  return this._disposed;
751
834
  }
835
+ /** Whether init() has been called. */
752
836
  get initialized() {
753
837
  return this._initialized;
754
838
  }
839
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
755
840
  get disposeSignal() {
756
841
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
757
842
  }
843
+ /** Initializes the instance. Called automatically by React hooks after mount. */
758
844
  init() {
759
845
  if (!(this._initialized || this._disposed))
760
846
  return this._initialized = !0, this.onInit?.();
761
847
  }
848
+ /** Tears down the instance, releasing all subscriptions and resources. */
762
849
  dispose() {
763
850
  if (!this._disposed) {
764
851
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -768,32 +855,39 @@ class D {
768
855
  this.onDispose?.();
769
856
  }
770
857
  }
858
+ /** Registers a cleanup function to be called on dispose. @protected */
771
859
  addCleanup(t) {
772
860
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
773
861
  }
862
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
774
863
  subscribeTo(t, e) {
775
864
  const s = t.subscribe(e);
776
865
  return this.addCleanup(s), s;
777
866
  }
778
867
  }
779
- class P {
868
+ class I {
780
869
  _disposed = !1;
781
870
  _initialized = !1;
782
871
  _abortController = null;
783
872
  _cleanups = null;
873
+ /** Whether this instance has been disposed. */
784
874
  get disposed() {
785
875
  return this._disposed;
786
876
  }
877
+ /** Whether init() has been called. */
787
878
  get initialized() {
788
879
  return this._initialized;
789
880
  }
881
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
790
882
  get disposeSignal() {
791
883
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
792
884
  }
885
+ /** Initializes the instance. Called automatically by React hooks after mount. */
793
886
  init() {
794
887
  if (!(this._initialized || this._disposed))
795
888
  return this._initialized = !0, this.onInit?.();
796
889
  }
890
+ /** Tears down the instance, releasing all subscriptions and resources. */
797
891
  dispose() {
798
892
  if (!this._disposed) {
799
893
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -803,6 +897,7 @@ class P {
803
897
  this.onDispose?.();
804
898
  }
805
899
  }
900
+ /** Registers a cleanup function to be called on dispose. @protected */
806
901
  addCleanup(t) {
807
902
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
808
903
  }
@@ -813,11 +908,15 @@ const m = typeof __MVC_KIT_DEV__ < "u" && __MVC_KIT_DEV__, k = Object.freeze({
813
908
  attempt: 0,
814
909
  error: null
815
910
  });
816
- class I {
911
+ class P {
817
912
  // Static config (subclass overrides)
913
+ /** Base delay (ms) for reconnection backoff. */
818
914
  static RECONNECT_BASE = 1e3;
915
+ /** Maximum delay cap (ms) for reconnection backoff. */
819
916
  static RECONNECT_MAX = 3e4;
917
+ /** Exponential backoff multiplier for reconnection delay. */
820
918
  static RECONNECT_FACTOR = 2;
919
+ /** Maximum number of reconnection attempts before giving up. */
821
920
  static MAX_ATTEMPTS = 1 / 0;
822
921
  // ── Internal state ──────────────────────────────────────────────
823
922
  _status = k;
@@ -831,9 +930,11 @@ class I {
831
930
  _reconnectTimer = null;
832
931
  _cleanups = null;
833
932
  // ── Subscribable<ChannelStatus> ─────────────────────────────────
933
+ /** Current connection status. */
834
934
  get state() {
835
935
  return this._status;
836
936
  }
937
+ /** Subscribes to connection status changes. Returns an unsubscribe function. */
837
938
  subscribe(t) {
838
939
  return this._disposed ? () => {
839
940
  } : (this._listeners.add(t), () => {
@@ -841,19 +942,24 @@ class I {
841
942
  });
842
943
  }
843
944
  // ── Disposable / Initializable ──────────────────────────────────
945
+ /** Whether this instance has been disposed. */
844
946
  get disposed() {
845
947
  return this._disposed;
846
948
  }
949
+ /** Whether init() has been called. */
847
950
  get initialized() {
848
951
  return this._initialized;
849
952
  }
953
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
850
954
  get disposeSignal() {
851
955
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
852
956
  }
957
+ /** Initializes the instance. Called automatically by React hooks after mount. */
853
958
  init() {
854
959
  if (!(this._initialized || this._disposed))
855
960
  return this._initialized = !0, this.onInit?.();
856
961
  }
962
+ /** Tears down the instance, releasing all subscriptions and resources. */
857
963
  dispose() {
858
964
  if (!this._disposed) {
859
965
  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,6 +975,7 @@ class I {
869
975
  }
870
976
  }
871
977
  // ── Connection control ──────────────────────────────────────────
978
+ /** Initiates a connection with automatic reconnection on failure. */
872
979
  connect() {
873
980
  if (this._disposed) {
874
981
  m && console.warn("[mvc-kit] connect() called after dispose — ignored.");
@@ -876,6 +983,7 @@ class I {
876
983
  }
877
984
  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));
878
985
  }
986
+ /** Closes the connection and cancels any pending reconnection. */
879
987
  disconnect() {
880
988
  if (!this._disposed) {
881
989
  if (this._reconnectTimer !== null && (clearTimeout(this._reconnectTimer), this._reconnectTimer = null), this._connectAbort?.abort(), this._connectAbort = null, this._connState === 2 || this._connState === 1) {
@@ -890,6 +998,7 @@ class I {
890
998
  }
891
999
  }
892
1000
  // ── Subclass signals ────────────────────────────────────────────
1001
+ /** Call from subclass when a message arrives from the transport. @protected */
893
1002
  receive(t, e) {
894
1003
  if (this._disposed) {
895
1004
  m && console.warn(`[mvc-kit] receive("${String(t)}") called after dispose — ignored.`);
@@ -900,10 +1009,12 @@ class I {
900
1009
  for (const i of s)
901
1010
  i(e);
902
1011
  }
1012
+ /** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */
903
1013
  disconnected() {
904
1014
  this._disposed || this._connState !== 2 && this._connState !== 1 || (this._connectAbort?.abort(), this._connectAbort = null, this._connState = 3, this._scheduleReconnect(1));
905
1015
  }
906
1016
  // ── Consumer API ────────────────────────────────────────────────
1017
+ /** Subscribes to a specific message type. Returns an unsubscribe function. */
907
1018
  on(t, e) {
908
1019
  if (this._disposed) return () => {
909
1020
  };
@@ -912,6 +1023,7 @@ class I {
912
1023
  s.delete(e);
913
1024
  };
914
1025
  }
1026
+ /** Subscribes to a message type, auto-removing the handler after the first invocation. */
915
1027
  once(t, e) {
916
1028
  const s = this.on(t, (i) => {
917
1029
  s(), e(i);
@@ -919,14 +1031,17 @@ class I {
919
1031
  return s;
920
1032
  }
921
1033
  // ── Infrastructure ──────────────────────────────────────────────
1034
+ /** Registers a cleanup function to be called on dispose. @protected */
922
1035
  addCleanup(t) {
923
1036
  this._cleanups || (this._cleanups = []), this._cleanups.push(t);
924
1037
  }
1038
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
925
1039
  subscribeTo(t, e) {
926
1040
  const s = t.subscribe(e);
927
1041
  return this.addCleanup(s), s;
928
1042
  }
929
1043
  // ── Backoff ─────────────────────────────────────────────────────
1044
+ /** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */
930
1045
  _calculateDelay(t) {
931
1046
  const e = this.constructor, s = Math.min(
932
1047
  e.RECONNECT_BASE * Math.pow(e.RECONNECT_FACTOR, t),
@@ -994,20 +1109,20 @@ class I {
994
1109
  attempt: t,
995
1110
  error: i
996
1111
  });
997
- const o = this._calculateDelay(t - 1);
1112
+ const r = this._calculateDelay(t - 1);
998
1113
  this._reconnectTimer = setTimeout(() => {
999
1114
  this._reconnectTimer = null, this._attemptConnect(t);
1000
- }, o);
1115
+ }, r);
1001
1116
  }
1002
1117
  }
1003
1118
  export {
1004
- I as Channel,
1119
+ P as Channel,
1005
1120
  x as Collection,
1006
1121
  D as Controller,
1007
- y as EventBus,
1008
- w as HttpError,
1122
+ w as EventBus,
1123
+ y as HttpError,
1009
1124
  M as Model,
1010
- P as Service,
1125
+ I as Service,
1011
1126
  b as ViewModel,
1012
1127
  O as classifyError,
1013
1128
  N as hasSingleton,