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.
- package/README.md +12 -5
- 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 +10 -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 +23 -16
- package/agent-config/cursor/cursorrules +23 -16
- package/dist/Channel.d.ts +29 -0
- package/dist/Channel.d.ts.map +1 -1
- package/dist/Collection.d.ts +38 -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 +503 -276
- 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,67 +1,67 @@
|
|
|
1
|
-
import { E as
|
|
2
|
-
import { h as N, s as
|
|
3
|
-
class
|
|
4
|
-
constructor(t,
|
|
5
|
-
super(
|
|
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
|
|
9
|
-
return
|
|
8
|
+
function y(r) {
|
|
9
|
+
return r instanceof Error && r.name === "AbortError";
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
return
|
|
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(
|
|
15
|
-
return typeof
|
|
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
|
|
18
|
-
return
|
|
17
|
+
function E(r) {
|
|
18
|
+
return r instanceof Error && r.name === "AbortError" ? {
|
|
19
19
|
code: "abort",
|
|
20
20
|
message: "Request was aborted",
|
|
21
|
-
original:
|
|
22
|
-
} :
|
|
23
|
-
code:
|
|
24
|
-
message:
|
|
25
|
-
status:
|
|
26
|
-
original:
|
|
27
|
-
} : T(
|
|
28
|
-
code:
|
|
29
|
-
message:
|
|
30
|
-
status:
|
|
31
|
-
original:
|
|
32
|
-
} :
|
|
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:
|
|
35
|
-
original:
|
|
36
|
-
} :
|
|
34
|
+
message: r.message,
|
|
35
|
+
original: r
|
|
36
|
+
} : r instanceof Error && r.name === "TimeoutError" ? {
|
|
37
37
|
code: "timeout",
|
|
38
|
-
message:
|
|
39
|
-
original:
|
|
40
|
-
} :
|
|
38
|
+
message: r.message,
|
|
39
|
+
original: r
|
|
40
|
+
} : r instanceof Error ? {
|
|
41
41
|
code: "unknown",
|
|
42
|
-
message:
|
|
43
|
-
original:
|
|
42
|
+
message: r.message,
|
|
43
|
+
original: r
|
|
44
44
|
} : {
|
|
45
45
|
code: "unknown",
|
|
46
|
-
message: String(
|
|
47
|
-
original:
|
|
46
|
+
message: String(r),
|
|
47
|
+
original: r
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
const
|
|
51
|
-
function
|
|
52
|
-
return
|
|
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
|
|
55
|
-
let
|
|
56
|
-
for (;
|
|
57
|
-
const i = Object.getOwnPropertyDescriptors(
|
|
58
|
-
for (const [
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
64
|
-
class
|
|
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
|
|
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 (
|
|
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
|
|
127
|
-
if (!Object.keys(
|
|
128
|
-
(
|
|
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
|
|
132
|
-
this._state = a, this._revision++, this.onSet?.(
|
|
133
|
-
for (const
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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(
|
|
175
|
-
return t._asyncSnapshots.get(
|
|
200
|
+
get(s, e) {
|
|
201
|
+
return t._asyncSnapshots.get(e) ?? z;
|
|
176
202
|
},
|
|
177
|
-
has(
|
|
178
|
-
return t._asyncSnapshots.has(
|
|
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(
|
|
184
|
-
if (t._asyncSnapshots.has(
|
|
185
|
-
return { configurable: !0, enumerable: !0, value: t._asyncSnapshots.get(
|
|
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
|
-
|
|
211
|
-
if (
|
|
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
|
|
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,
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
const a =
|
|
228
|
-
let
|
|
229
|
-
const
|
|
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
|
-
|
|
258
|
+
d && console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);
|
|
232
259
|
return;
|
|
233
260
|
}
|
|
234
|
-
|
|
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
|
|
264
|
+
let u;
|
|
238
265
|
try {
|
|
239
|
-
|
|
240
|
-
} catch (
|
|
241
|
-
throw
|
|
266
|
+
u = a.apply(t, h);
|
|
267
|
+
} catch (_) {
|
|
268
|
+
throw _;
|
|
242
269
|
}
|
|
243
|
-
if (!
|
|
244
|
-
return
|
|
245
|
-
let
|
|
246
|
-
return
|
|
247
|
-
(
|
|
248
|
-
if (t._disposed) return
|
|
249
|
-
if (
|
|
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:
|
|
252
|
-
), t._notifyAsync(),
|
|
253
|
-
const
|
|
254
|
-
|
|
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
|
|
283
|
+
return _;
|
|
257
284
|
},
|
|
258
|
-
(
|
|
259
|
-
if (
|
|
260
|
-
if (t._disposed || (
|
|
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:
|
|
263
|
-
), t._notifyAsync()),
|
|
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
|
-
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
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:
|
|
275
|
-
), t._notifyAsync(),
|
|
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
|
|
306
|
+
throw _;
|
|
280
307
|
}
|
|
281
308
|
);
|
|
282
309
|
};
|
|
283
|
-
|
|
284
|
-
}),
|
|
285
|
-
const i =
|
|
286
|
-
for (const
|
|
287
|
-
|
|
288
|
-
console.warn(`[mvc-kit] "${
|
|
289
|
-
} : t[
|
|
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(),
|
|
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
|
-
|
|
296
|
-
for (const [
|
|
322
|
+
d && setTimeout(() => {
|
|
323
|
+
for (const [s, e] of t)
|
|
297
324
|
console.warn(
|
|
298
|
-
`[mvc-kit] Ghost async operation detected: "${
|
|
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: (
|
|
342
|
+
get: (s, e) => (this._stateTracking?.add(e), this._state[e]),
|
|
316
343
|
ownKeys: () => Reflect.ownKeys(this._state),
|
|
317
|
-
getOwnPropertyDescriptor: (
|
|
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: (
|
|
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
|
|
347
|
-
if (!
|
|
348
|
-
const
|
|
349
|
-
source:
|
|
373
|
+
const s = this[t];
|
|
374
|
+
if (!O(s)) continue;
|
|
375
|
+
const e = {
|
|
376
|
+
source: s,
|
|
350
377
|
revision: 0,
|
|
351
|
-
unsubscribe:
|
|
378
|
+
unsubscribe: s.subscribe(() => {
|
|
352
379
|
if (!this._disposed) {
|
|
353
|
-
|
|
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,
|
|
360
|
-
get: () => (this._sourceTracking?.set(t,
|
|
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
|
-
|
|
377
|
-
!
|
|
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,
|
|
391
|
-
let
|
|
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
|
|
396
|
-
if (
|
|
397
|
-
let
|
|
398
|
-
for (const [
|
|
399
|
-
if (this._state[
|
|
400
|
-
|
|
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 (
|
|
404
|
-
for (const [
|
|
405
|
-
const p = this._trackedSources.get(
|
|
406
|
-
if (p && p.revision !==
|
|
407
|
-
|
|
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 (
|
|
412
|
-
return i = this._revision,
|
|
438
|
+
if (c)
|
|
439
|
+
return i = this._revision, e;
|
|
413
440
|
}
|
|
414
|
-
const
|
|
441
|
+
const l = this._stateTracking, h = this._sourceTracking;
|
|
415
442
|
this._stateTracking = /* @__PURE__ */ new Set(), this._sourceTracking = /* @__PURE__ */ new Map();
|
|
416
443
|
try {
|
|
417
|
-
|
|
418
|
-
} catch (
|
|
419
|
-
throw this._stateTracking =
|
|
444
|
+
e = s.call(this);
|
|
445
|
+
} catch (c) {
|
|
446
|
+
throw this._stateTracking = l, this._sourceTracking = h, c;
|
|
420
447
|
}
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
if (this._stateTracking =
|
|
424
|
-
for (const
|
|
425
|
-
if (
|
|
426
|
-
for (const [
|
|
427
|
-
|
|
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
|
|
430
|
-
a.set(
|
|
431
|
-
|
|
432
|
-
for (const [
|
|
433
|
-
|
|
434
|
-
return i = this._revision,
|
|
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
|
|
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
|
|
451
|
-
this._state =
|
|
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
|
|
497
|
-
if (!Object.keys(
|
|
498
|
-
(
|
|
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
|
|
502
|
-
this._state = a, this.onSet?.(
|
|
503
|
-
for (const
|
|
504
|
-
|
|
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
|
|
525
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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,
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
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
|
|
561
|
-
if (t[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
618
|
-
if (
|
|
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(
|
|
622
|
-
for (const
|
|
623
|
-
this._index.delete(
|
|
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,
|
|
744
|
+
update(t, s) {
|
|
630
745
|
if (this._disposed)
|
|
631
746
|
throw new Error("Cannot update disposed Collection");
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
747
|
+
const e = this._items.findIndex((u) => u.id === t);
|
|
748
|
+
if (e === -1)
|
|
634
749
|
return;
|
|
635
|
-
const i = this._items[
|
|
636
|
-
if (!Object.keys(
|
|
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
|
|
639
|
-
|
|
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
|
|
648
|
-
|
|
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.
|
|
790
|
+
const s = this._items, e = this._timestamps ? new Map(this._timestamps) : null;
|
|
669
791
|
t();
|
|
670
|
-
let
|
|
792
|
+
let i = !1;
|
|
671
793
|
return () => {
|
|
672
|
-
if (
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
this._items = e, this.rebuildIndex(), this.notify(
|
|
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
|
|
736
|
-
|
|
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
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1093
|
+
g && console.warn("[mvc-kit] connect() called after dispose — ignored.");
|
|
875
1094
|
return;
|
|
876
1095
|
}
|
|
877
|
-
|
|
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
|
-
|
|
1113
|
+
/** Call from subclass when a message arrives from the transport. @protected */
|
|
1114
|
+
receive(t, s) {
|
|
894
1115
|
if (this._disposed) {
|
|
895
|
-
|
|
1116
|
+
g && console.warn(`[mvc-kit] receive("${String(t)}") called after dispose — ignored.`);
|
|
896
1117
|
return;
|
|
897
1118
|
}
|
|
898
|
-
const
|
|
899
|
-
if (
|
|
900
|
-
for (const i of
|
|
901
|
-
i(
|
|
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
|
-
|
|
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
|
|
911
|
-
return
|
|
912
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
|
932
|
-
|
|
933
|
-
|
|
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() *
|
|
1162
|
+
return Math.random() * e;
|
|
936
1163
|
}
|
|
937
1164
|
// ── Internals ───────────────────────────────────────────────────
|
|
938
1165
|
_setStatus(t) {
|
|
939
|
-
const
|
|
940
|
-
if (!(
|
|
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
|
|
943
|
-
|
|
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
|
|
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
|
|
1183
|
+
let e;
|
|
957
1184
|
try {
|
|
958
|
-
|
|
1185
|
+
e = this.open(s);
|
|
959
1186
|
} catch (i) {
|
|
960
1187
|
this._onOpenFailed(t, i);
|
|
961
1188
|
return;
|
|
962
1189
|
}
|
|
963
|
-
|
|
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,
|
|
977
|
-
this._disposed || this._connState !== 0 && (this._connectAbort?.abort(), this._connectAbort = null, this._connState = 3, this._scheduleReconnect(t + 1,
|
|
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,
|
|
980
|
-
const
|
|
981
|
-
if (t >
|
|
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:
|
|
1213
|
+
error: s instanceof Error ? s.message : "Max reconnection attempts reached"
|
|
987
1214
|
});
|
|
988
1215
|
return;
|
|
989
1216
|
}
|
|
990
|
-
const i =
|
|
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
|
|
1224
|
+
const n = this._calculateDelay(t - 1);
|
|
998
1225
|
this._reconnectTimer = setTimeout(() => {
|
|
999
1226
|
this._reconnectTimer = null, this._attemptConnect(t);
|
|
1000
|
-
},
|
|
1227
|
+
}, n);
|
|
1001
1228
|
}
|
|
1002
1229
|
}
|
|
1003
1230
|
export {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1238
|
+
m as ViewModel,
|
|
1239
|
+
E as classifyError,
|
|
1013
1240
|
N as hasSingleton,
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1241
|
+
y as isAbortError,
|
|
1242
|
+
$ as singleton,
|
|
1243
|
+
L as teardown,
|
|
1244
|
+
B as teardownAll
|
|
1018
1245
|
};
|
|
1019
1246
|
//# sourceMappingURL=mvc-kit.js.map
|