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