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.
Files changed (62) hide show
  1. package/README.md +18 -1
  2. package/agent-config/claude-code/skills/guide/SKILL.md +1 -0
  3. package/agent-config/claude-code/skills/guide/api-reference.md +8 -1
  4. package/agent-config/claude-code/skills/scaffold/templates/model.md +38 -1
  5. package/agent-config/copilot/copilot-instructions.md +2 -1
  6. package/agent-config/cursor/cursorrules +2 -1
  7. package/dist/Collection.cjs +31 -17
  8. package/dist/Collection.cjs.map +1 -1
  9. package/dist/Collection.d.ts.map +1 -1
  10. package/dist/Collection.js +31 -17
  11. package/dist/Collection.js.map +1 -1
  12. package/dist/Model.cjs +22 -4
  13. package/dist/Model.cjs.map +1 -1
  14. package/dist/Model.d.ts +2 -0
  15. package/dist/Model.d.ts.map +1 -1
  16. package/dist/Model.js +22 -4
  17. package/dist/Model.js.map +1 -1
  18. package/dist/PersistentCollection.cjs +8 -10
  19. package/dist/PersistentCollection.cjs.map +1 -1
  20. package/dist/PersistentCollection.d.ts +1 -0
  21. package/dist/PersistentCollection.d.ts.map +1 -1
  22. package/dist/PersistentCollection.js +8 -10
  23. package/dist/PersistentCollection.js.map +1 -1
  24. package/dist/Resource.cjs +21 -157
  25. package/dist/Resource.cjs.map +1 -1
  26. package/dist/Resource.d.ts +1 -3
  27. package/dist/Resource.d.ts.map +1 -1
  28. package/dist/Resource.js +21 -157
  29. package/dist/Resource.js.map +1 -1
  30. package/dist/ViewModel.cjs +178 -228
  31. package/dist/ViewModel.cjs.map +1 -1
  32. package/dist/ViewModel.d.ts +10 -13
  33. package/dist/ViewModel.d.ts.map +1 -1
  34. package/dist/ViewModel.js +178 -228
  35. package/dist/ViewModel.js.map +1 -1
  36. package/dist/react/index.d.ts +1 -1
  37. package/dist/react/index.d.ts.map +1 -1
  38. package/dist/react/use-instance.cjs +31 -21
  39. package/dist/react/use-instance.cjs.map +1 -1
  40. package/dist/react/use-instance.d.ts +1 -1
  41. package/dist/react/use-instance.d.ts.map +1 -1
  42. package/dist/react/use-instance.js +32 -22
  43. package/dist/react/use-instance.js.map +1 -1
  44. package/dist/react/use-model.cjs +29 -2
  45. package/dist/react/use-model.cjs.map +1 -1
  46. package/dist/react/use-model.d.ts +9 -0
  47. package/dist/react/use-model.d.ts.map +1 -1
  48. package/dist/react/use-model.js +30 -3
  49. package/dist/react/use-model.js.map +1 -1
  50. package/dist/react.cjs +1 -0
  51. package/dist/react.cjs.map +1 -1
  52. package/dist/react.js +2 -1
  53. package/dist/walkPrototypeChain.cjs.map +1 -1
  54. package/dist/walkPrototypeChain.d.ts +2 -2
  55. package/dist/walkPrototypeChain.js.map +1 -1
  56. package/dist/wrapAsyncMethods.cjs +179 -0
  57. package/dist/wrapAsyncMethods.cjs.map +1 -0
  58. package/dist/wrapAsyncMethods.d.ts +35 -0
  59. package/dist/wrapAsyncMethods.d.ts.map +1 -0
  60. package/dist/wrapAsyncMethods.js +179 -0
  61. package/dist/wrapAsyncMethods.js.map +1 -0
  62. 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
- _asyncStates = /* @__PURE__ */ new Map();
28
- _asyncSnapshots = /* @__PURE__ */ new Map();
29
- _asyncListeners = /* @__PURE__ */ new Set();
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 = Object.freeze({ ...initialState });
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._memoizeGetters();
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
- const keys = Object.keys(partial);
96
- const hasChanges = keys.some(
97
- (key) => partial[key] !== this._state[key]
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 = Object.freeze({ ...prev, ...partial });
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 ? Object.freeze({ ...newState }) : this._initialState;
190
+ this._state = newState ? freeze({ ...newState }) : this._initialState;
157
191
  this._revision++;
158
- this._asyncStates.clear();
159
- this._asyncSnapshots.clear();
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.get(prop) ?? DEFAULT_TASK_STATE;
241
+ return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;
208
242
  },
209
243
  has(_, prop) {
210
- return self._asyncSnapshots.has(prop);
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.has(prop)) {
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 by `useAsync` for React integration. */
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
- walkPrototypeChain(this, ViewModel.prototype, (key) => {
250
- if (RESERVED_ASYNC_KEYS.includes(key)) {
251
- throw new Error(
252
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
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
- _wrapMethods() {
258
- for (const key of RESERVED_ASYNC_KEYS) {
259
- if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) {
260
- throw new Error(
261
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
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
- const self = this;
266
- const processed = /* @__PURE__ */ new Set();
267
- const wrappedKeys = [];
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
- walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {
272
- if (desc.get || desc.set) return;
273
- if (typeof desc.value !== "function") return;
274
- if (key.startsWith("_")) return;
275
- if (LIFECYCLE_HOOKS.has(key)) return;
276
- if (processed.has(key)) return;
277
- processed.add(key);
278
- const original = desc.value;
279
- let pruned = false;
280
- const wrapper = function(...args) {
281
- if (self._disposed) {
282
- if (__DEV__) {
283
- console.warn(`[mvc-kit] "${key}" called after dispose — ignored.`);
284
- }
285
- return void 0;
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
- const stateProxy = new Proxy({}, {
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) return stateProxy;
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 stateDeps;
524
- let stateSnapshot;
525
- let sourceDeps;
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 (stateDeps && stateSnapshot) {
450
+ if (this._disposed) return cached;
451
+ if (stateDepKeys !== void 0) {
533
452
  let fresh = true;
534
- for (const [k, v] of stateSnapshot) {
535
- if (this._state[k] !== v) {
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 && sourceDeps) {
541
- for (const [memberKey, rev] of sourceDeps) {
542
- const ts = this._trackedSources.get(memberKey);
543
- if (ts && ts.revision !== rev) {
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
- this._stateTracking = /* @__PURE__ */ new Set();
557
- this._sourceTracking = /* @__PURE__ */ new Map();
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 stateDeps) parentStateTracking.add(d);
498
+ for (const d of trackingSet) parentStateTracking.add(d);
571
499
  }
572
500
  if (parentSourceTracking) {
573
- for (const [k, v] of capturedSourceDeps) {
501
+ for (const [k, v] of trackingMap) {
574
502
  parentSourceTracking.set(k, v);
575
503
  }
576
504
  }
577
- stateSnapshot = /* @__PURE__ */ new Map();
578
- for (const d of stateDeps) {
579
- stateSnapshot.set(d, this._state[d]);
505
+ const depCount = trackingSet.size;
506
+ if (!stateDepKeys || stateDepKeys.length !== depCount) {
507
+ stateDepKeys = new Array(depCount);
508
+ stateDepValues = new Array(depCount);
580
509
  }
581
- sourceDeps = /* @__PURE__ */ new Map();
582
- for (const [memberKey, tracked] of capturedSourceDeps) {
583
- sourceDeps.set(memberKey, tracked.revision);
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;