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.
Files changed (129) hide show
  1. package/README.md +47 -1
  2. package/agent-config/claude-code/skills/guide/SKILL.md +1 -0
  3. package/agent-config/claude-code/skills/guide/anti-patterns.md +3 -3
  4. package/agent-config/claude-code/skills/guide/api-reference.md +146 -2
  5. package/agent-config/claude-code/skills/guide/patterns.md +120 -0
  6. package/agent-config/claude-code/skills/scaffold/templates/model.md +38 -1
  7. package/agent-config/copilot/copilot-instructions.md +54 -1
  8. package/agent-config/cursor/cursorrules +54 -1
  9. package/dist/Collection.cjs +69 -17
  10. package/dist/Collection.cjs.map +1 -1
  11. package/dist/Collection.d.ts.map +1 -1
  12. package/dist/Collection.js +69 -17
  13. package/dist/Collection.js.map +1 -1
  14. package/dist/Feed.cjs +86 -0
  15. package/dist/Feed.cjs.map +1 -0
  16. package/dist/Feed.d.ts +46 -0
  17. package/dist/Feed.d.ts.map +1 -0
  18. package/dist/Feed.js +86 -0
  19. package/dist/Feed.js.map +1 -0
  20. package/dist/Model.cjs +22 -4
  21. package/dist/Model.cjs.map +1 -1
  22. package/dist/Model.d.ts +2 -0
  23. package/dist/Model.d.ts.map +1 -1
  24. package/dist/Model.js +22 -4
  25. package/dist/Model.js.map +1 -1
  26. package/dist/Pagination.cjs +84 -0
  27. package/dist/Pagination.cjs.map +1 -0
  28. package/dist/Pagination.d.ts +39 -0
  29. package/dist/Pagination.d.ts.map +1 -0
  30. package/dist/Pagination.js +84 -0
  31. package/dist/Pagination.js.map +1 -0
  32. package/dist/PersistentCollection.cjs +16 -15
  33. package/dist/PersistentCollection.cjs.map +1 -1
  34. package/dist/PersistentCollection.d.ts +7 -1
  35. package/dist/PersistentCollection.d.ts.map +1 -1
  36. package/dist/PersistentCollection.js +16 -15
  37. package/dist/PersistentCollection.js.map +1 -1
  38. package/dist/Resource.cjs +23 -156
  39. package/dist/Resource.cjs.map +1 -1
  40. package/dist/Resource.d.ts +3 -2
  41. package/dist/Resource.d.ts.map +1 -1
  42. package/dist/Resource.js +23 -156
  43. package/dist/Resource.js.map +1 -1
  44. package/dist/Selection.cjs +99 -0
  45. package/dist/Selection.cjs.map +1 -0
  46. package/dist/Selection.d.ts +36 -0
  47. package/dist/Selection.d.ts.map +1 -0
  48. package/dist/Selection.js +99 -0
  49. package/dist/Selection.js.map +1 -0
  50. package/dist/Sorting.cjs +114 -0
  51. package/dist/Sorting.cjs.map +1 -0
  52. package/dist/Sorting.d.ts +43 -0
  53. package/dist/Sorting.d.ts.map +1 -0
  54. package/dist/Sorting.js +114 -0
  55. package/dist/Sorting.js.map +1 -0
  56. package/dist/ViewModel.cjs +177 -227
  57. package/dist/ViewModel.cjs.map +1 -1
  58. package/dist/ViewModel.d.ts +9 -12
  59. package/dist/ViewModel.d.ts.map +1 -1
  60. package/dist/ViewModel.js +177 -227
  61. package/dist/ViewModel.js.map +1 -1
  62. package/dist/index.d.ts +6 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/mvc-kit.cjs +8 -0
  65. package/dist/mvc-kit.cjs.map +1 -1
  66. package/dist/mvc-kit.js +8 -0
  67. package/dist/mvc-kit.js.map +1 -1
  68. package/dist/react/components/CardList.cjs +42 -0
  69. package/dist/react/components/CardList.cjs.map +1 -0
  70. package/dist/react/components/CardList.d.ts +22 -0
  71. package/dist/react/components/CardList.d.ts.map +1 -0
  72. package/dist/react/components/CardList.js +42 -0
  73. package/dist/react/components/CardList.js.map +1 -0
  74. package/dist/react/components/DataTable.cjs +179 -0
  75. package/dist/react/components/DataTable.cjs.map +1 -0
  76. package/dist/react/components/DataTable.d.ts +30 -0
  77. package/dist/react/components/DataTable.d.ts.map +1 -0
  78. package/dist/react/components/DataTable.js +179 -0
  79. package/dist/react/components/DataTable.js.map +1 -0
  80. package/dist/react/components/InfiniteScroll.cjs +44 -0
  81. package/dist/react/components/InfiniteScroll.cjs.map +1 -0
  82. package/dist/react/components/InfiniteScroll.d.ts +21 -0
  83. package/dist/react/components/InfiniteScroll.d.ts.map +1 -0
  84. package/dist/react/components/InfiniteScroll.js +44 -0
  85. package/dist/react/components/InfiniteScroll.js.map +1 -0
  86. package/dist/react/components/types.cjs +15 -0
  87. package/dist/react/components/types.cjs.map +1 -0
  88. package/dist/react/components/types.d.ts +71 -0
  89. package/dist/react/components/types.d.ts.map +1 -0
  90. package/dist/react/components/types.js +15 -0
  91. package/dist/react/components/types.js.map +1 -0
  92. package/dist/react/index.d.ts +8 -1
  93. package/dist/react/index.d.ts.map +1 -1
  94. package/dist/react/use-instance.cjs +31 -21
  95. package/dist/react/use-instance.cjs.map +1 -1
  96. package/dist/react/use-instance.d.ts +1 -1
  97. package/dist/react/use-instance.d.ts.map +1 -1
  98. package/dist/react/use-instance.js +32 -22
  99. package/dist/react/use-instance.js.map +1 -1
  100. package/dist/react/use-model.cjs +29 -2
  101. package/dist/react/use-model.cjs.map +1 -1
  102. package/dist/react/use-model.d.ts +9 -0
  103. package/dist/react/use-model.d.ts.map +1 -1
  104. package/dist/react/use-model.js +30 -3
  105. package/dist/react/use-model.js.map +1 -1
  106. package/dist/react-native/NativeCollection.cjs +3 -0
  107. package/dist/react-native/NativeCollection.cjs.map +1 -1
  108. package/dist/react-native/NativeCollection.d.ts +3 -0
  109. package/dist/react-native/NativeCollection.d.ts.map +1 -1
  110. package/dist/react-native/NativeCollection.js +3 -0
  111. package/dist/react-native/NativeCollection.js.map +1 -1
  112. package/dist/react.cjs +7 -0
  113. package/dist/react.cjs.map +1 -1
  114. package/dist/react.js +8 -1
  115. package/dist/react.js.map +1 -1
  116. package/dist/walkPrototypeChain.cjs.map +1 -1
  117. package/dist/walkPrototypeChain.d.ts +2 -2
  118. package/dist/walkPrototypeChain.js.map +1 -1
  119. package/dist/web/idb.cjs.map +1 -1
  120. package/dist/web/idb.d.ts +18 -0
  121. package/dist/web/idb.d.ts.map +1 -1
  122. package/dist/web/idb.js.map +1 -1
  123. package/dist/wrapAsyncMethods.cjs +159 -0
  124. package/dist/wrapAsyncMethods.cjs.map +1 -0
  125. package/dist/wrapAsyncMethods.d.ts +37 -0
  126. package/dist/wrapAsyncMethods.d.ts.map +1 -0
  127. package/dist/wrapAsyncMethods.js +159 -0
  128. package/dist/wrapAsyncMethods.js.map +1 -0
  129. package/package.json +1 -1
@@ -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
- _asyncStates = /* @__PURE__ */ new Map();
30
- _asyncSnapshots = /* @__PURE__ */ new Map();
31
- _asyncListeners = /* @__PURE__ */ new Set();
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 = Object.freeze({ ...initialState });
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._memoizeGetters();
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
- const keys = Object.keys(partial);
98
- const hasChanges = keys.some(
99
- (key) => partial[key] !== this._state[key]
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 = Object.freeze({ ...prev, ...partial });
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 ? Object.freeze({ ...newState }) : this._initialState;
192
+ this._state = newState ? freeze({ ...newState }) : this._initialState;
159
193
  this._revision++;
160
- this._asyncStates.clear();
161
- this._asyncSnapshots.clear();
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.get(prop) ?? DEFAULT_TASK_STATE;
243
+ return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;
210
244
  },
211
245
  has(_, prop) {
212
- return self._asyncSnapshots.has(prop);
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.has(prop)) {
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
- walkPrototypeChain.walkPrototypeChain(this, ViewModel.prototype, (key) => {
252
- if (RESERVED_ASYNC_KEYS.includes(key)) {
253
- throw new Error(
254
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
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
- _wrapMethods() {
260
- for (const key of RESERVED_ASYNC_KEYS) {
261
- if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) {
262
- throw new Error(
263
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
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
- const self = this;
268
- const processed = /* @__PURE__ */ new Set();
269
- const wrappedKeys = [];
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
- walkPrototypeChain.walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {
274
- if (desc.get || desc.set) return;
275
- if (typeof desc.value !== "function") return;
276
- if (key.startsWith("_")) return;
277
- if (LIFECYCLE_HOOKS.has(key)) return;
278
- if (processed.has(key)) return;
279
- processed.add(key);
280
- const original = desc.value;
281
- let pruned = false;
282
- const wrapper = function(...args) {
283
- if (self._disposed) {
284
- if (__DEV__) {
285
- console.warn(`[mvc-kit] "${key}" called after dispose — ignored.`);
286
- }
287
- return void 0;
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
- const stateProxy = new Proxy({}, {
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) return stateProxy;
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 stateDeps;
526
- let stateSnapshot;
527
- let sourceDeps;
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 (stateDeps && stateSnapshot) {
452
+ if (this._disposed) return cached;
453
+ if (stateDepKeys !== void 0) {
535
454
  let fresh = true;
536
- for (const [k, v] of stateSnapshot) {
537
- if (this._state[k] !== v) {
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 && sourceDeps) {
543
- for (const [memberKey, rev] of sourceDeps) {
544
- const ts = this._trackedSources.get(memberKey);
545
- if (ts && ts.revision !== rev) {
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
- this._stateTracking = /* @__PURE__ */ new Set();
559
- this._sourceTracking = /* @__PURE__ */ new Map();
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 stateDeps) parentStateTracking.add(d);
500
+ for (const d of trackingSet) parentStateTracking.add(d);
573
501
  }
574
502
  if (parentSourceTracking) {
575
- for (const [k, v] of capturedSourceDeps) {
503
+ for (const [k, v] of trackingMap) {
576
504
  parentSourceTracking.set(k, v);
577
505
  }
578
506
  }
579
- stateSnapshot = /* @__PURE__ */ new Map();
580
- for (const d of stateDeps) {
581
- stateSnapshot.set(d, this._state[d]);
507
+ const depCount = trackingSet.size;
508
+ if (!stateDepKeys || stateDepKeys.length !== depCount) {
509
+ stateDepKeys = new Array(depCount);
510
+ stateDepValues = new Array(depCount);
582
511
  }
583
- sourceDeps = /* @__PURE__ */ new Map();
584
- for (const [memberKey, tracked] of capturedSourceDeps) {
585
- sourceDeps.set(memberKey, tracked.revision);
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;