mvc-kit 2.7.1 → 2.9.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 +47 -1
- package/agent-config/claude-code/skills/guide/SKILL.md +1 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +3 -3
- package/agent-config/claude-code/skills/guide/api-reference.md +146 -2
- package/agent-config/claude-code/skills/guide/patterns.md +120 -0
- package/agent-config/claude-code/skills/scaffold/templates/model.md +38 -1
- package/agent-config/copilot/copilot-instructions.md +54 -1
- package/agent-config/cursor/cursorrules +54 -1
- package/dist/Collection.cjs +69 -17
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +69 -17
- package/dist/Collection.js.map +1 -1
- package/dist/Feed.cjs +86 -0
- package/dist/Feed.cjs.map +1 -0
- package/dist/Feed.d.ts +46 -0
- package/dist/Feed.d.ts.map +1 -0
- package/dist/Feed.js +86 -0
- package/dist/Feed.js.map +1 -0
- package/dist/Model.cjs +22 -4
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +2 -0
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +22 -4
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +84 -0
- package/dist/Pagination.cjs.map +1 -0
- package/dist/Pagination.d.ts +39 -0
- package/dist/Pagination.d.ts.map +1 -0
- package/dist/Pagination.js +84 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/PersistentCollection.cjs +16 -15
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.d.ts +7 -1
- package/dist/PersistentCollection.d.ts.map +1 -1
- package/dist/PersistentCollection.js +16 -15
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +23 -156
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.d.ts +3 -2
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +23 -156
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +99 -0
- package/dist/Selection.cjs.map +1 -0
- package/dist/Selection.d.ts +36 -0
- package/dist/Selection.d.ts.map +1 -0
- package/dist/Selection.js +99 -0
- package/dist/Selection.js.map +1 -0
- package/dist/Sorting.cjs +114 -0
- package/dist/Sorting.cjs.map +1 -0
- package/dist/Sorting.d.ts +43 -0
- package/dist/Sorting.d.ts.map +1 -0
- package/dist/Sorting.js +114 -0
- package/dist/Sorting.js.map +1 -0
- package/dist/ViewModel.cjs +177 -227
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +9 -12
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +177 -227
- package/dist/ViewModel.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +8 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +8 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/components/CardList.cjs +42 -0
- package/dist/react/components/CardList.cjs.map +1 -0
- package/dist/react/components/CardList.d.ts +22 -0
- package/dist/react/components/CardList.d.ts.map +1 -0
- package/dist/react/components/CardList.js +42 -0
- package/dist/react/components/CardList.js.map +1 -0
- package/dist/react/components/DataTable.cjs +179 -0
- package/dist/react/components/DataTable.cjs.map +1 -0
- package/dist/react/components/DataTable.d.ts +30 -0
- package/dist/react/components/DataTable.d.ts.map +1 -0
- package/dist/react/components/DataTable.js +179 -0
- package/dist/react/components/DataTable.js.map +1 -0
- package/dist/react/components/InfiniteScroll.cjs +44 -0
- package/dist/react/components/InfiniteScroll.cjs.map +1 -0
- package/dist/react/components/InfiniteScroll.d.ts +21 -0
- package/dist/react/components/InfiniteScroll.d.ts.map +1 -0
- package/dist/react/components/InfiniteScroll.js +44 -0
- package/dist/react/components/InfiniteScroll.js.map +1 -0
- package/dist/react/components/types.cjs +15 -0
- package/dist/react/components/types.cjs.map +1 -0
- package/dist/react/components/types.d.ts +71 -0
- package/dist/react/components/types.d.ts.map +1 -0
- package/dist/react/components/types.js +15 -0
- package/dist/react/components/types.js.map +1 -0
- package/dist/react/index.d.ts +8 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/use-instance.cjs +31 -21
- package/dist/react/use-instance.cjs.map +1 -1
- package/dist/react/use-instance.d.ts +1 -1
- package/dist/react/use-instance.d.ts.map +1 -1
- package/dist/react/use-instance.js +32 -22
- package/dist/react/use-instance.js.map +1 -1
- package/dist/react/use-model.cjs +29 -2
- package/dist/react/use-model.cjs.map +1 -1
- package/dist/react/use-model.d.ts +9 -0
- package/dist/react/use-model.d.ts.map +1 -1
- package/dist/react/use-model.js +30 -3
- package/dist/react/use-model.js.map +1 -1
- package/dist/react-native/NativeCollection.cjs +3 -0
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.d.ts +3 -0
- package/dist/react-native/NativeCollection.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.js +3 -0
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react.cjs +7 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +8 -1
- package/dist/react.js.map +1 -1
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.d.ts +2 -2
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.d.ts +18 -0
- package/dist/web/idb.d.ts.map +1 -1
- package/dist/web/idb.js.map +1 -1
- package/dist/wrapAsyncMethods.cjs +159 -0
- package/dist/wrapAsyncMethods.cjs.map +1 -0
- package/dist/wrapAsyncMethods.d.ts +37 -0
- package/dist/wrapAsyncMethods.d.ts.map +1 -0
- package/dist/wrapAsyncMethods.js +159 -0
- package/dist/wrapAsyncMethods.js.map +1 -0
- package/package.json +1 -1
package/dist/ViewModel.cjs
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const EventBus = require("./EventBus.cjs");
|
|
4
|
-
const errors = require("./errors.cjs");
|
|
5
4
|
const walkPrototypeChain = require("./walkPrototypeChain.cjs");
|
|
5
|
+
const wrapAsyncMethods = require("./wrapAsyncMethods.cjs");
|
|
6
6
|
const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
|
|
7
|
+
function freeze(obj) {
|
|
8
|
+
return __DEV__ ? Object.freeze(obj) : obj;
|
|
9
|
+
}
|
|
10
|
+
const classMembers = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
function getClassMemberInfo(instance, stopPrototype, reservedKeys, lifecycleHooks) {
|
|
12
|
+
const ctor = instance.constructor;
|
|
13
|
+
let info = classMembers.get(ctor);
|
|
14
|
+
if (info) return info;
|
|
15
|
+
const getters = [];
|
|
16
|
+
const methods = [];
|
|
17
|
+
const found = [];
|
|
18
|
+
const processedGetters = /* @__PURE__ */ new Set();
|
|
19
|
+
const processedMethods = /* @__PURE__ */ new Set();
|
|
20
|
+
walkPrototypeChain.walkPrototypeChain(instance, stopPrototype, (key, desc) => {
|
|
21
|
+
if (reservedKeys.includes(key)) {
|
|
22
|
+
found.push(key);
|
|
23
|
+
}
|
|
24
|
+
if (desc.get && !processedGetters.has(key)) {
|
|
25
|
+
processedGetters.add(key);
|
|
26
|
+
getters.push({ key, get: desc.get });
|
|
27
|
+
}
|
|
28
|
+
if (!desc.get && !desc.set && typeof desc.value === "function" && !key.startsWith("_") && !lifecycleHooks.has(key) && !processedMethods.has(key)) {
|
|
29
|
+
processedMethods.add(key);
|
|
30
|
+
methods.push({ key, fn: desc.value });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
info = { getters, methods, reservedKeys: found };
|
|
34
|
+
classMembers.set(ctor, info);
|
|
35
|
+
return info;
|
|
36
|
+
}
|
|
7
37
|
function isAutoTrackable(value) {
|
|
8
38
|
return value !== null && typeof value === "object" && typeof value.subscribe === "function";
|
|
9
39
|
}
|
|
@@ -26,16 +56,17 @@ class ViewModel {
|
|
|
26
56
|
_sourceTracking = null;
|
|
27
57
|
_trackedSources = /* @__PURE__ */ new Map();
|
|
28
58
|
// ── Async tracking (RFC 2) ──────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
// Lazily allocated on first async method wrap to keep construction fast.
|
|
60
|
+
_asyncStates = null;
|
|
61
|
+
_asyncSnapshots = null;
|
|
62
|
+
_asyncListeners = null;
|
|
32
63
|
_asyncProxy = null;
|
|
33
64
|
_activeOps = null;
|
|
34
65
|
/** DEV-only timeout (ms) for detecting ghost async operations after dispose. */
|
|
35
66
|
static GHOST_TIMEOUT = 3e3;
|
|
36
67
|
constructor(...args) {
|
|
37
68
|
const initialState = args[0] ?? {};
|
|
38
|
-
this._state =
|
|
69
|
+
this._state = freeze({ ...initialState });
|
|
39
70
|
this._initialState = this._state;
|
|
40
71
|
this._guardReservedKeys();
|
|
41
72
|
}
|
|
@@ -71,8 +102,7 @@ class ViewModel {
|
|
|
71
102
|
this._initialized = true;
|
|
72
103
|
this._trackSubscribables();
|
|
73
104
|
this._installStateProxy();
|
|
74
|
-
this.
|
|
75
|
-
this._wrapMethods();
|
|
105
|
+
this._processMembers();
|
|
76
106
|
return this.onInit?.();
|
|
77
107
|
}
|
|
78
108
|
/**
|
|
@@ -94,15 +124,19 @@ class ViewModel {
|
|
|
94
124
|
return;
|
|
95
125
|
}
|
|
96
126
|
const partial = typeof partialOrUpdater === "function" ? partialOrUpdater(this._state) : partialOrUpdater;
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
let hasChanges = false;
|
|
128
|
+
const current = this._state;
|
|
129
|
+
for (const key in partial) {
|
|
130
|
+
if (partial[key] !== current[key]) {
|
|
131
|
+
hasChanges = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
101
135
|
if (!hasChanges) {
|
|
102
136
|
return;
|
|
103
137
|
}
|
|
104
138
|
const prev = this._state;
|
|
105
|
-
const next =
|
|
139
|
+
const next = freeze({ ...prev, ...partial });
|
|
106
140
|
this._state = next;
|
|
107
141
|
this._revision++;
|
|
108
142
|
this.onSet?.(prev, next);
|
|
@@ -155,10 +189,10 @@ class ViewModel {
|
|
|
155
189
|
this._abortController?.abort();
|
|
156
190
|
this._abortController = null;
|
|
157
191
|
this._teardownSubscriptions();
|
|
158
|
-
this._state = newState ?
|
|
192
|
+
this._state = newState ? freeze({ ...newState }) : this._initialState;
|
|
159
193
|
this._revision++;
|
|
160
|
-
this._asyncStates
|
|
161
|
-
this._asyncSnapshots
|
|
194
|
+
this._asyncStates?.clear();
|
|
195
|
+
this._asyncSnapshots?.clear();
|
|
162
196
|
this._notifyAsync();
|
|
163
197
|
this._trackSubscribables();
|
|
164
198
|
for (const listener of this._listeners) {
|
|
@@ -206,16 +240,16 @@ class ViewModel {
|
|
|
206
240
|
const self = this;
|
|
207
241
|
this._asyncProxy = new Proxy({}, {
|
|
208
242
|
get(_, prop) {
|
|
209
|
-
return self._asyncSnapshots
|
|
243
|
+
return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;
|
|
210
244
|
},
|
|
211
245
|
has(_, prop) {
|
|
212
|
-
return self._asyncSnapshots
|
|
246
|
+
return self._asyncSnapshots?.has(prop) ?? false;
|
|
213
247
|
},
|
|
214
248
|
ownKeys() {
|
|
215
|
-
return Array.from(self._asyncSnapshots.keys());
|
|
249
|
+
return self._asyncSnapshots ? Array.from(self._asyncSnapshots.keys()) : [];
|
|
216
250
|
},
|
|
217
251
|
getOwnPropertyDescriptor(_, prop) {
|
|
218
|
-
if (self._asyncSnapshots
|
|
252
|
+
if (self._asyncSnapshots?.has(prop)) {
|
|
219
253
|
return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };
|
|
220
254
|
}
|
|
221
255
|
return void 0;
|
|
@@ -228,12 +262,14 @@ class ViewModel {
|
|
|
228
262
|
subscribeAsync(listener) {
|
|
229
263
|
if (this._disposed) return () => {
|
|
230
264
|
};
|
|
265
|
+
if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
|
|
231
266
|
this._asyncListeners.add(listener);
|
|
232
267
|
return () => {
|
|
233
268
|
this._asyncListeners.delete(listener);
|
|
234
269
|
};
|
|
235
270
|
}
|
|
236
271
|
_notifyAsync() {
|
|
272
|
+
if (!this._asyncListeners) return;
|
|
237
273
|
for (const listener of this._asyncListeners) {
|
|
238
274
|
listener();
|
|
239
275
|
}
|
|
@@ -248,195 +284,92 @@ class ViewModel {
|
|
|
248
284
|
}
|
|
249
285
|
}
|
|
250
286
|
_guardReservedKeys() {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
287
|
+
const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
|
|
288
|
+
if (info.reservedKeys.length > 0) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`[mvc-kit] "${info.reservedKeys[0]}" is a reserved property on ViewModel and cannot be overridden.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
258
293
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
294
|
+
// ── Member processing (merged getter memoization + async method wrapping) ──
|
|
295
|
+
/**
|
|
296
|
+
* Uses cached class metadata to memoize getters (RFC 1) and delegates
|
|
297
|
+
* async method wrapping to the shared wrapAsyncMethods helper (RFC 2).
|
|
298
|
+
* Class metadata is computed once per class via getClassMemberInfo() and reused
|
|
299
|
+
* across all instances — avoids repeated prototype walks.
|
|
300
|
+
*/
|
|
301
|
+
_processMembers() {
|
|
302
|
+
const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
|
|
303
|
+
for (let i = 0; i < info.getters.length; i++) {
|
|
304
|
+
this._wrapGetter(info.getters[i].key, info.getters[i].get);
|
|
305
|
+
}
|
|
306
|
+
if (__DEV__) {
|
|
307
|
+
for (const key of RESERVED_ASYNC_KEYS) {
|
|
308
|
+
if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
265
313
|
}
|
|
266
314
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
315
|
+
if (info.methods.length === 0) return;
|
|
316
|
+
if (!this._asyncStates) this._asyncStates = /* @__PURE__ */ new Map();
|
|
317
|
+
if (!this._asyncSnapshots) this._asyncSnapshots = /* @__PURE__ */ new Map();
|
|
318
|
+
if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
|
|
270
319
|
if (__DEV__) {
|
|
271
320
|
this._activeOps = /* @__PURE__ */ new Map();
|
|
272
321
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (__DEV__ && !self._initialized) {
|
|
290
|
-
console.warn(
|
|
291
|
-
`[mvc-kit] "${key}" called before init(). Async tracking is active only after init().`
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
let result;
|
|
295
|
-
try {
|
|
296
|
-
result = original.apply(self, args);
|
|
297
|
-
} catch (e) {
|
|
298
|
-
throw e;
|
|
299
|
-
}
|
|
300
|
-
if (!result || typeof result.then !== "function") {
|
|
301
|
-
if (!pruned) {
|
|
302
|
-
pruned = true;
|
|
303
|
-
self._asyncStates.delete(key);
|
|
304
|
-
self._asyncSnapshots.delete(key);
|
|
305
|
-
self[key] = original.bind(self);
|
|
306
|
-
}
|
|
307
|
-
return result;
|
|
308
|
-
}
|
|
309
|
-
let internal = self._asyncStates.get(key);
|
|
310
|
-
if (!internal) {
|
|
311
|
-
internal = { loading: false, error: null, errorCode: null, count: 0 };
|
|
312
|
-
self._asyncStates.set(key, internal);
|
|
313
|
-
}
|
|
314
|
-
internal.count++;
|
|
315
|
-
internal.loading = true;
|
|
316
|
-
internal.error = null;
|
|
317
|
-
internal.errorCode = null;
|
|
318
|
-
self._asyncSnapshots.set(key, Object.freeze({ loading: true, error: null, errorCode: null }));
|
|
319
|
-
self._notifyAsync();
|
|
320
|
-
if (__DEV__ && self._activeOps) {
|
|
321
|
-
self._activeOps.set(key, (self._activeOps.get(key) ?? 0) + 1);
|
|
322
|
-
}
|
|
323
|
-
return result.then(
|
|
324
|
-
(value) => {
|
|
325
|
-
if (self._disposed) return value;
|
|
326
|
-
internal.count--;
|
|
327
|
-
internal.loading = internal.count > 0;
|
|
328
|
-
self._asyncSnapshots.set(
|
|
329
|
-
key,
|
|
330
|
-
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
331
|
-
);
|
|
332
|
-
self._notifyAsync();
|
|
333
|
-
if (__DEV__ && self._activeOps) {
|
|
334
|
-
const c = (self._activeOps.get(key) ?? 1) - 1;
|
|
335
|
-
if (c <= 0) self._activeOps.delete(key);
|
|
336
|
-
else self._activeOps.set(key, c);
|
|
337
|
-
}
|
|
338
|
-
return value;
|
|
339
|
-
},
|
|
340
|
-
(error) => {
|
|
341
|
-
if (errors.isAbortError(error)) {
|
|
342
|
-
if (!self._disposed) {
|
|
343
|
-
internal.count--;
|
|
344
|
-
internal.loading = internal.count > 0;
|
|
345
|
-
self._asyncSnapshots.set(
|
|
346
|
-
key,
|
|
347
|
-
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
348
|
-
);
|
|
349
|
-
self._notifyAsync();
|
|
350
|
-
}
|
|
351
|
-
if (__DEV__ && self._activeOps) {
|
|
352
|
-
const c = (self._activeOps.get(key) ?? 1) - 1;
|
|
353
|
-
if (c <= 0) self._activeOps.delete(key);
|
|
354
|
-
else self._activeOps.set(key, c);
|
|
355
|
-
}
|
|
356
|
-
return void 0;
|
|
357
|
-
}
|
|
358
|
-
if (self._disposed) return void 0;
|
|
359
|
-
internal.count--;
|
|
360
|
-
internal.loading = internal.count > 0;
|
|
361
|
-
const classified = errors.classifyError(error);
|
|
362
|
-
internal.error = classified.message;
|
|
363
|
-
internal.errorCode = classified.code;
|
|
364
|
-
self._asyncSnapshots.set(
|
|
365
|
-
key,
|
|
366
|
-
Object.freeze({ loading: internal.loading, error: classified.message, errorCode: classified.code })
|
|
367
|
-
);
|
|
368
|
-
self._notifyAsync();
|
|
369
|
-
if (__DEV__ && self._activeOps) {
|
|
370
|
-
const c = (self._activeOps.get(key) ?? 1) - 1;
|
|
371
|
-
if (c <= 0) self._activeOps.delete(key);
|
|
372
|
-
else self._activeOps.set(key, c);
|
|
373
|
-
}
|
|
374
|
-
throw error;
|
|
375
|
-
}
|
|
376
|
-
);
|
|
377
|
-
};
|
|
378
|
-
wrappedKeys.push(key);
|
|
379
|
-
self[key] = wrapper;
|
|
322
|
+
wrapAsyncMethods.wrapAsyncMethods({
|
|
323
|
+
instance: this,
|
|
324
|
+
stopPrototype: ViewModel.prototype,
|
|
325
|
+
reservedKeys: RESERVED_ASYNC_KEYS,
|
|
326
|
+
lifecycleHooks: LIFECYCLE_HOOKS,
|
|
327
|
+
isDisposed: () => this._disposed,
|
|
328
|
+
isInitialized: () => this._initialized,
|
|
329
|
+
asyncStates: this._asyncStates,
|
|
330
|
+
asyncSnapshots: this._asyncSnapshots,
|
|
331
|
+
asyncListeners: this._asyncListeners,
|
|
332
|
+
notifyAsync: () => this._notifyAsync(),
|
|
333
|
+
addCleanup: (fn) => this.addCleanup(fn),
|
|
334
|
+
ghostTimeout: this.constructor.GHOST_TIMEOUT,
|
|
335
|
+
className: "ViewModel",
|
|
336
|
+
activeOps: this._activeOps,
|
|
337
|
+
methods: info.methods
|
|
380
338
|
});
|
|
381
|
-
if (wrappedKeys.length > 0) {
|
|
382
|
-
this.addCleanup(() => {
|
|
383
|
-
const opsSnapshot = __DEV__ && self._activeOps ? new Map(self._activeOps) : null;
|
|
384
|
-
for (const k of wrappedKeys) {
|
|
385
|
-
if (__DEV__) {
|
|
386
|
-
self[k] = () => {
|
|
387
|
-
console.warn(`[mvc-kit] "${k}" called after dispose — ignored.`);
|
|
388
|
-
return void 0;
|
|
389
|
-
};
|
|
390
|
-
} else {
|
|
391
|
-
self[k] = () => void 0;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
self._asyncListeners.clear();
|
|
395
|
-
self._asyncStates.clear();
|
|
396
|
-
self._asyncSnapshots.clear();
|
|
397
|
-
if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {
|
|
398
|
-
self._scheduleGhostCheck(opsSnapshot);
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
_scheduleGhostCheck(opsSnapshot) {
|
|
404
|
-
if (!__DEV__) return;
|
|
405
|
-
setTimeout(() => {
|
|
406
|
-
for (const [key, count] of opsSnapshot) {
|
|
407
|
-
console.warn(
|
|
408
|
-
`[mvc-kit] Ghost async operation detected: "${key}" had ${count} pending call(s) when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.`
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
}, this.constructor.GHOST_TIMEOUT);
|
|
412
339
|
}
|
|
413
340
|
// ── Auto-tracking internals ────────────────────────────────────
|
|
414
341
|
/**
|
|
415
342
|
* Installs a context-sensitive state getter on the instance.
|
|
416
343
|
*
|
|
417
344
|
* During getter tracking (_stateTracking is active): returns a Proxy
|
|
418
|
-
* that records which state properties are accessed.
|
|
345
|
+
* that records which state properties are accessed. The Proxy is created
|
|
346
|
+
* lazily on first tracking access to keep init() fast.
|
|
419
347
|
*
|
|
420
348
|
* Otherwise: returns the frozen state object directly. This is critical
|
|
421
349
|
* for React's useSyncExternalStore — it needs a changing reference to
|
|
422
350
|
* detect state updates and trigger re-renders.
|
|
423
351
|
*/
|
|
424
352
|
_installStateProxy() {
|
|
425
|
-
|
|
426
|
-
get: (_, prop) => {
|
|
427
|
-
this._stateTracking?.add(prop);
|
|
428
|
-
return this._state[prop];
|
|
429
|
-
},
|
|
430
|
-
ownKeys: () => Reflect.ownKeys(this._state),
|
|
431
|
-
getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(this._state, prop),
|
|
432
|
-
set: () => {
|
|
433
|
-
throw new Error("Cannot mutate state directly. Use set() instead.");
|
|
434
|
-
},
|
|
435
|
-
has: (_, prop) => prop in this._state
|
|
436
|
-
});
|
|
353
|
+
let stateProxy = null;
|
|
437
354
|
Object.defineProperty(this, "state", {
|
|
438
355
|
get: () => {
|
|
439
|
-
if (this._stateTracking)
|
|
356
|
+
if (this._stateTracking) {
|
|
357
|
+
if (!stateProxy) {
|
|
358
|
+
stateProxy = new Proxy({}, {
|
|
359
|
+
get: (_, prop) => {
|
|
360
|
+
this._stateTracking?.add(prop);
|
|
361
|
+
return this._state[prop];
|
|
362
|
+
},
|
|
363
|
+
ownKeys: () => Reflect.ownKeys(this._state),
|
|
364
|
+
getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(this._state, prop),
|
|
365
|
+
set: () => {
|
|
366
|
+
throw new Error("Cannot mutate state directly. Use set() instead.");
|
|
367
|
+
},
|
|
368
|
+
has: (_, prop) => prop in this._state
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return stateProxy;
|
|
372
|
+
}
|
|
440
373
|
return this._state;
|
|
441
374
|
},
|
|
442
375
|
configurable: true,
|
|
@@ -467,7 +400,6 @@ class ViewModel {
|
|
|
467
400
|
if (this._disposed) return;
|
|
468
401
|
tracked.revision++;
|
|
469
402
|
this._revision++;
|
|
470
|
-
this._state = Object.freeze({ ...this._state });
|
|
471
403
|
for (const listener of this._listeners) {
|
|
472
404
|
listener(this._state, this._state);
|
|
473
405
|
}
|
|
@@ -493,22 +425,6 @@ class ViewModel {
|
|
|
493
425
|
});
|
|
494
426
|
}
|
|
495
427
|
}
|
|
496
|
-
/**
|
|
497
|
-
* Walks the prototype chain from the subclass up to (but not including)
|
|
498
|
-
* ViewModel.prototype. For every getter found, replaces it on the
|
|
499
|
-
* instance with a memoized version that tracks dependencies and caches.
|
|
500
|
-
*
|
|
501
|
-
* Processing order: most-derived class first. If a subclass overrides
|
|
502
|
-
* a parent getter, only the subclass version is memoized.
|
|
503
|
-
*/
|
|
504
|
-
_memoizeGetters() {
|
|
505
|
-
const processed = /* @__PURE__ */ new Set();
|
|
506
|
-
walkPrototypeChain.walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {
|
|
507
|
-
if (!desc.get || processed.has(key)) return;
|
|
508
|
-
processed.add(key);
|
|
509
|
-
this._wrapGetter(key, desc.get);
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
428
|
/**
|
|
513
429
|
* Replaces a single prototype getter with a memoized version on this
|
|
514
430
|
* instance. The memoized getter tracks both state dependencies and
|
|
@@ -522,27 +438,31 @@ class ViewModel {
|
|
|
522
438
|
_wrapGetter(key, original) {
|
|
523
439
|
let cached;
|
|
524
440
|
let validatedAtRevision = -1;
|
|
525
|
-
let
|
|
526
|
-
let
|
|
527
|
-
let
|
|
441
|
+
let stateDepKeys;
|
|
442
|
+
let stateDepValues;
|
|
443
|
+
let sourceDepKeys;
|
|
444
|
+
let sourceDepRevisions;
|
|
445
|
+
let trackingSet;
|
|
446
|
+
let trackingMap;
|
|
528
447
|
Object.defineProperty(this, key, {
|
|
529
448
|
get: () => {
|
|
530
|
-
if (this._disposed) return cached;
|
|
531
449
|
if (validatedAtRevision === this._revision) {
|
|
532
450
|
return cached;
|
|
533
451
|
}
|
|
534
|
-
if (
|
|
452
|
+
if (this._disposed) return cached;
|
|
453
|
+
if (stateDepKeys !== void 0) {
|
|
535
454
|
let fresh = true;
|
|
536
|
-
|
|
537
|
-
|
|
455
|
+
const state = this._state;
|
|
456
|
+
for (let i = 0; i < stateDepKeys.length; i++) {
|
|
457
|
+
if (state[stateDepKeys[i]] !== stateDepValues[i]) {
|
|
538
458
|
fresh = false;
|
|
539
459
|
break;
|
|
540
460
|
}
|
|
541
461
|
}
|
|
542
|
-
if (fresh &&
|
|
543
|
-
for (
|
|
544
|
-
const ts = this._trackedSources.get(
|
|
545
|
-
if (ts && ts.revision !==
|
|
462
|
+
if (fresh && sourceDepKeys !== void 0 && sourceDepKeys.length > 0) {
|
|
463
|
+
for (let i = 0; i < sourceDepKeys.length; i++) {
|
|
464
|
+
const ts = this._trackedSources.get(sourceDepKeys[i]);
|
|
465
|
+
if (ts && ts.revision !== sourceDepRevisions[i]) {
|
|
546
466
|
fresh = false;
|
|
547
467
|
break;
|
|
548
468
|
}
|
|
@@ -555,8 +475,18 @@ class ViewModel {
|
|
|
555
475
|
}
|
|
556
476
|
const parentStateTracking = this._stateTracking;
|
|
557
477
|
const parentSourceTracking = this._sourceTracking;
|
|
558
|
-
|
|
559
|
-
|
|
478
|
+
if (trackingSet) {
|
|
479
|
+
trackingSet.clear();
|
|
480
|
+
} else {
|
|
481
|
+
trackingSet = /* @__PURE__ */ new Set();
|
|
482
|
+
}
|
|
483
|
+
if (trackingMap) {
|
|
484
|
+
trackingMap.clear();
|
|
485
|
+
} else {
|
|
486
|
+
trackingMap = /* @__PURE__ */ new Map();
|
|
487
|
+
}
|
|
488
|
+
this._stateTracking = trackingSet;
|
|
489
|
+
this._sourceTracking = trackingMap;
|
|
560
490
|
try {
|
|
561
491
|
cached = original.call(this);
|
|
562
492
|
} catch (e) {
|
|
@@ -564,25 +494,45 @@ class ViewModel {
|
|
|
564
494
|
this._sourceTracking = parentSourceTracking;
|
|
565
495
|
throw e;
|
|
566
496
|
}
|
|
567
|
-
stateDeps = this._stateTracking;
|
|
568
|
-
const capturedSourceDeps = this._sourceTracking;
|
|
569
497
|
this._stateTracking = parentStateTracking;
|
|
570
498
|
this._sourceTracking = parentSourceTracking;
|
|
571
499
|
if (parentStateTracking) {
|
|
572
|
-
for (const d of
|
|
500
|
+
for (const d of trackingSet) parentStateTracking.add(d);
|
|
573
501
|
}
|
|
574
502
|
if (parentSourceTracking) {
|
|
575
|
-
for (const [k, v] of
|
|
503
|
+
for (const [k, v] of trackingMap) {
|
|
576
504
|
parentSourceTracking.set(k, v);
|
|
577
505
|
}
|
|
578
506
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
507
|
+
const depCount = trackingSet.size;
|
|
508
|
+
if (!stateDepKeys || stateDepKeys.length !== depCount) {
|
|
509
|
+
stateDepKeys = new Array(depCount);
|
|
510
|
+
stateDepValues = new Array(depCount);
|
|
582
511
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
512
|
+
{
|
|
513
|
+
let i = 0;
|
|
514
|
+
const state = this._state;
|
|
515
|
+
for (const d of trackingSet) {
|
|
516
|
+
stateDepKeys[i] = d;
|
|
517
|
+
stateDepValues[i] = state[d];
|
|
518
|
+
i++;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const sourceCount = trackingMap.size;
|
|
522
|
+
if (sourceCount > 0) {
|
|
523
|
+
if (!sourceDepKeys || sourceDepKeys.length !== sourceCount) {
|
|
524
|
+
sourceDepKeys = new Array(sourceCount);
|
|
525
|
+
sourceDepRevisions = new Array(sourceCount);
|
|
526
|
+
}
|
|
527
|
+
let i = 0;
|
|
528
|
+
for (const [memberKey, tracked] of trackingMap) {
|
|
529
|
+
sourceDepKeys[i] = memberKey;
|
|
530
|
+
sourceDepRevisions[i] = tracked.revision;
|
|
531
|
+
i++;
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
sourceDepKeys = void 0;
|
|
535
|
+
sourceDepRevisions = void 0;
|
|
586
536
|
}
|
|
587
537
|
validatedAtRevision = this._revision;
|
|
588
538
|
return cached;
|