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.
- package/README.md +5 -4
- package/agent-config/claude-code/agents/mvc-kit-architect.md +2 -3
- package/agent-config/claude-code/skills/guide/SKILL.md +1 -1
- package/agent-config/claude-code/skills/guide/anti-patterns.md +42 -3
- package/agent-config/claude-code/skills/guide/api-reference.md +4 -3
- package/agent-config/claude-code/skills/guide/patterns.md +18 -13
- package/agent-config/claude-code/skills/review/checklist.md +1 -1
- package/agent-config/claude-code/skills/scaffold/SKILL.md +1 -1
- package/agent-config/claude-code/skills/scaffold/templates/collection.md +7 -14
- package/agent-config/claude-code/skills/scaffold/templates/viewmodel.md +13 -42
- package/agent-config/copilot/copilot-instructions.md +14 -16
- package/agent-config/cursor/cursorrules +14 -16
- package/dist/Channel.d.ts +29 -0
- package/dist/Channel.d.ts.map +1 -1
- package/dist/Collection.d.ts +16 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Controller.d.ts +9 -0
- package/dist/Controller.d.ts.map +1 -1
- package/dist/EventBus.d.ts +5 -0
- package/dist/EventBus.d.ts.map +1 -1
- package/dist/Model.d.ts +16 -0
- package/dist/Model.d.ts.map +1 -1
- package/dist/Service.d.ts +8 -0
- package/dist/Service.d.ts.map +1 -1
- package/dist/ViewModel.d.ts +35 -1
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +1 -1
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +226 -111
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/provider.d.ts +1 -0
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/use-model.d.ts +2 -0
- package/dist/react/use-model.d.ts.map +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/{singleton-C8_FRbA7.js → singleton-CaEXSbYg.js} +5 -1
- package/dist/singleton-CaEXSbYg.js.map +1 -0
- package/dist/singleton-L-u2W_lX.cjs.map +1 -1
- package/dist/singleton.d.ts +10 -0
- package/dist/singleton.d.ts.map +1 -1
- package/mvc-kit-logo.jpg +0 -0
- package/package.json +2 -1
- package/dist/singleton-C8_FRbA7.js.map +0 -1
package/dist/mvc-kit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as
|
|
2
|
-
import { h as N, s as V, t as B, a as L } from "./singleton-
|
|
3
|
-
class
|
|
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
|
|
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
|
|
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 [
|
|
59
|
-
|
|
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
|
|
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 (
|
|
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
|
-
(
|
|
135
|
+
(a) => e[a] !== this._state[a]
|
|
129
136
|
))
|
|
130
137
|
return;
|
|
131
|
-
const
|
|
132
|
-
this._state =
|
|
133
|
-
for (const
|
|
134
|
-
c
|
|
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
|
-
|
|
225
|
-
if (
|
|
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
|
|
228
|
-
let
|
|
229
|
-
const
|
|
254
|
+
const c = r.value;
|
|
255
|
+
let a = !1;
|
|
256
|
+
const l = function(...f) {
|
|
230
257
|
if (t._disposed) {
|
|
231
|
-
|
|
258
|
+
u && console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);
|
|
232
259
|
return;
|
|
233
260
|
}
|
|
234
|
-
|
|
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
|
|
264
|
+
let _;
|
|
238
265
|
try {
|
|
239
|
-
|
|
240
|
-
} catch (
|
|
241
|
-
throw
|
|
266
|
+
_ = c.apply(t, f);
|
|
267
|
+
} catch (h) {
|
|
268
|
+
throw h;
|
|
242
269
|
}
|
|
243
|
-
if (!
|
|
244
|
-
return
|
|
245
|
-
let
|
|
246
|
-
return
|
|
247
|
-
(
|
|
248
|
-
if (t._disposed) return
|
|
249
|
-
if (
|
|
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:
|
|
252
|
-
), t._notifyAsync(),
|
|
253
|
-
const
|
|
254
|
-
|
|
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
|
|
283
|
+
return h;
|
|
257
284
|
},
|
|
258
|
-
(
|
|
259
|
-
if (v(
|
|
260
|
-
if (t._disposed || (
|
|
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:
|
|
263
|
-
), t._notifyAsync()),
|
|
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
|
-
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
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:
|
|
275
|
-
), t._notifyAsync(),
|
|
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
|
|
306
|
+
throw h;
|
|
280
307
|
}
|
|
281
308
|
);
|
|
282
309
|
};
|
|
283
|
-
s.push(i), t[i] =
|
|
310
|
+
s.push(i), t[i] = l;
|
|
284
311
|
}), s.length > 0 && this.addCleanup(() => {
|
|
285
|
-
const i =
|
|
286
|
-
for (const
|
|
287
|
-
|
|
288
|
-
console.warn(`[mvc-kit] "${
|
|
289
|
-
} : t[
|
|
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(),
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
397
|
-
let
|
|
398
|
-
for (const [
|
|
399
|
-
if (this._state[
|
|
400
|
-
|
|
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 (
|
|
404
|
-
for (const [
|
|
405
|
-
const p = this._trackedSources.get(
|
|
406
|
-
if (p && p.revision !==
|
|
407
|
-
|
|
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 (
|
|
438
|
+
if (o)
|
|
412
439
|
return i = this._revision, s;
|
|
413
440
|
}
|
|
414
|
-
const
|
|
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 (
|
|
419
|
-
throw this._stateTracking =
|
|
445
|
+
} catch (o) {
|
|
446
|
+
throw this._stateTracking = l, this._sourceTracking = f, o;
|
|
420
447
|
}
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
if (this._stateTracking =
|
|
424
|
-
for (const
|
|
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 [
|
|
427
|
-
f.set(
|
|
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
|
|
433
|
-
c.set(
|
|
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
|
-
(
|
|
534
|
+
(a) => e[a] !== this._state[a]
|
|
499
535
|
))
|
|
500
536
|
return;
|
|
501
|
-
const
|
|
502
|
-
this._state =
|
|
503
|
-
for (const
|
|
504
|
-
c
|
|
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
|
|
561
|
-
if (t[
|
|
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 =
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
this.
|
|
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((
|
|
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
|
|
623
|
-
this._index.delete(
|
|
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((
|
|
711
|
+
const s = this._items.findIndex((_) => _.id === t);
|
|
633
712
|
if (s === -1)
|
|
634
713
|
return;
|
|
635
|
-
const i = this._items[s],
|
|
636
|
-
if (!Object.keys(e).some((
|
|
714
|
+
const i = this._items[s], r = { ...i, ...e, id: t };
|
|
715
|
+
if (!Object.keys(e).some((_) => e[_] !== i[_]))
|
|
637
716
|
return;
|
|
638
|
-
const
|
|
639
|
-
f[s] =
|
|
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
|
|
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
|
|
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
|
|
1112
|
+
const r = this._calculateDelay(t - 1);
|
|
998
1113
|
this._reconnectTimer = setTimeout(() => {
|
|
999
1114
|
this._reconnectTimer = null, this._attemptConnect(t);
|
|
1000
|
-
},
|
|
1115
|
+
}, r);
|
|
1001
1116
|
}
|
|
1002
1117
|
}
|
|
1003
1118
|
export {
|
|
1004
|
-
|
|
1119
|
+
P as Channel,
|
|
1005
1120
|
x as Collection,
|
|
1006
1121
|
D as Controller,
|
|
1007
|
-
|
|
1008
|
-
|
|
1122
|
+
w as EventBus,
|
|
1123
|
+
y as HttpError,
|
|
1009
1124
|
M as Model,
|
|
1010
|
-
|
|
1125
|
+
I as Service,
|
|
1011
1126
|
b as ViewModel,
|
|
1012
1127
|
O as classifyError,
|
|
1013
1128
|
N as hasSingleton,
|