mvc-kit 2.12.4 → 2.13.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 (186) hide show
  1. package/agent-config/bin/postinstall.mjs +4 -3
  2. package/agent-config/bin/setup.mjs +5 -1
  3. package/agent-config/claude-code/agents/mvc-kit-architect.md +11 -8
  4. package/agent-config/claude-code/skills/guide/SKILL.md +20 -7
  5. package/agent-config/claude-code/skills/guide/patterns.md +12 -0
  6. package/agent-config/claude-code/skills/guide/recipes.md +510 -0
  7. package/agent-config/claude-code/skills/guide/testing.md +297 -0
  8. package/agent-config/claude-code/skills/review/SKILL.md +3 -13
  9. package/agent-config/claude-code/skills/review/checklist.md +30 -5
  10. package/agent-config/claude-code/skills/scaffold/SKILL.md +4 -13
  11. package/agent-config/lib/install-claude.mjs +84 -25
  12. package/dist/Channel.cjs +276 -300
  13. package/dist/Channel.cjs.map +1 -1
  14. package/dist/Channel.js +275 -299
  15. package/dist/Channel.js.map +1 -1
  16. package/dist/Collection.cjs +424 -504
  17. package/dist/Collection.cjs.map +1 -1
  18. package/dist/Collection.js +423 -503
  19. package/dist/Collection.js.map +1 -1
  20. package/dist/Controller.cjs +70 -67
  21. package/dist/Controller.cjs.map +1 -1
  22. package/dist/Controller.js +69 -66
  23. package/dist/Controller.js.map +1 -1
  24. package/dist/EventBus.cjs +77 -88
  25. package/dist/EventBus.cjs.map +1 -1
  26. package/dist/EventBus.js +76 -87
  27. package/dist/EventBus.js.map +1 -1
  28. package/dist/Feed.cjs +81 -77
  29. package/dist/Feed.cjs.map +1 -1
  30. package/dist/Feed.js +80 -76
  31. package/dist/Feed.js.map +1 -1
  32. package/dist/Model.cjs +181 -207
  33. package/dist/Model.cjs.map +1 -1
  34. package/dist/Model.js +179 -205
  35. package/dist/Model.js.map +1 -1
  36. package/dist/Pagination.cjs +75 -73
  37. package/dist/Pagination.cjs.map +1 -1
  38. package/dist/Pagination.js +74 -72
  39. package/dist/Pagination.js.map +1 -1
  40. package/dist/Pending.cjs +255 -287
  41. package/dist/Pending.cjs.map +1 -1
  42. package/dist/Pending.js +253 -285
  43. package/dist/Pending.js.map +1 -1
  44. package/dist/PersistentCollection.cjs +242 -285
  45. package/dist/PersistentCollection.cjs.map +1 -1
  46. package/dist/PersistentCollection.js +241 -284
  47. package/dist/PersistentCollection.js.map +1 -1
  48. package/dist/Resource.cjs +166 -174
  49. package/dist/Resource.cjs.map +1 -1
  50. package/dist/Resource.js +164 -172
  51. package/dist/Resource.js.map +1 -1
  52. package/dist/Selection.cjs +84 -94
  53. package/dist/Selection.cjs.map +1 -1
  54. package/dist/Selection.js +83 -93
  55. package/dist/Selection.js.map +1 -1
  56. package/dist/Service.cjs +54 -55
  57. package/dist/Service.cjs.map +1 -1
  58. package/dist/Service.js +53 -54
  59. package/dist/Service.js.map +1 -1
  60. package/dist/Sorting.cjs +102 -101
  61. package/dist/Sorting.cjs.map +1 -1
  62. package/dist/Sorting.js +102 -101
  63. package/dist/Sorting.js.map +1 -1
  64. package/dist/Trackable.cjs +112 -80
  65. package/dist/Trackable.cjs.map +1 -1
  66. package/dist/Trackable.js +111 -79
  67. package/dist/Trackable.js.map +1 -1
  68. package/dist/ViewModel.cjs +528 -576
  69. package/dist/ViewModel.cjs.map +1 -1
  70. package/dist/ViewModel.js +525 -573
  71. package/dist/ViewModel.js.map +1 -1
  72. package/dist/bindPublicMethods.cjs +43 -24
  73. package/dist/bindPublicMethods.cjs.map +1 -1
  74. package/dist/bindPublicMethods.js +43 -24
  75. package/dist/bindPublicMethods.js.map +1 -1
  76. package/dist/errors.cjs +67 -68
  77. package/dist/errors.cjs.map +1 -1
  78. package/dist/errors.js +68 -71
  79. package/dist/errors.js.map +1 -1
  80. package/dist/mvc-kit.cjs +44 -46
  81. package/dist/mvc-kit.js +5 -32
  82. package/dist/produceDraft.cjs +105 -95
  83. package/dist/produceDraft.cjs.map +1 -1
  84. package/dist/produceDraft.js +106 -97
  85. package/dist/produceDraft.js.map +1 -1
  86. package/dist/react/components/CardList.cjs +30 -40
  87. package/dist/react/components/CardList.cjs.map +1 -1
  88. package/dist/react/components/CardList.js +31 -41
  89. package/dist/react/components/CardList.js.map +1 -1
  90. package/dist/react/components/DataTable.cjs +146 -169
  91. package/dist/react/components/DataTable.cjs.map +1 -1
  92. package/dist/react/components/DataTable.js +147 -170
  93. package/dist/react/components/DataTable.js.map +1 -1
  94. package/dist/react/components/InfiniteScroll.cjs +51 -42
  95. package/dist/react/components/InfiniteScroll.cjs.map +1 -1
  96. package/dist/react/components/InfiniteScroll.js +52 -43
  97. package/dist/react/components/InfiniteScroll.js.map +1 -1
  98. package/dist/react/components/types.cjs +10 -6
  99. package/dist/react/components/types.cjs.map +1 -1
  100. package/dist/react/components/types.js +11 -9
  101. package/dist/react/components/types.js.map +1 -1
  102. package/dist/react/guards.cjs +10 -6
  103. package/dist/react/guards.cjs.map +1 -1
  104. package/dist/react/guards.js +11 -9
  105. package/dist/react/guards.js.map +1 -1
  106. package/dist/react/provider.cjs +23 -20
  107. package/dist/react/provider.cjs.map +1 -1
  108. package/dist/react/provider.js +23 -21
  109. package/dist/react/provider.js.map +1 -1
  110. package/dist/react/use-event-bus.cjs +24 -20
  111. package/dist/react/use-event-bus.cjs.map +1 -1
  112. package/dist/react/use-event-bus.js +24 -21
  113. package/dist/react/use-event-bus.js.map +1 -1
  114. package/dist/react/use-instance.cjs +43 -36
  115. package/dist/react/use-instance.cjs.map +1 -1
  116. package/dist/react/use-instance.js +43 -36
  117. package/dist/react/use-instance.js.map +1 -1
  118. package/dist/react/use-local.cjs +48 -64
  119. package/dist/react/use-local.cjs.map +1 -1
  120. package/dist/react/use-local.js +47 -63
  121. package/dist/react/use-local.js.map +1 -1
  122. package/dist/react/use-model.cjs +84 -98
  123. package/dist/react/use-model.cjs.map +1 -1
  124. package/dist/react/use-model.js +84 -100
  125. package/dist/react/use-model.js.map +1 -1
  126. package/dist/react/use-singleton.cjs +19 -23
  127. package/dist/react/use-singleton.cjs.map +1 -1
  128. package/dist/react/use-singleton.js +16 -20
  129. package/dist/react/use-singleton.js.map +1 -1
  130. package/dist/react/use-subscribe-only.cjs +28 -22
  131. package/dist/react/use-subscribe-only.cjs.map +1 -1
  132. package/dist/react/use-subscribe-only.js +28 -22
  133. package/dist/react/use-subscribe-only.js.map +1 -1
  134. package/dist/react/use-teardown.cjs +20 -19
  135. package/dist/react/use-teardown.cjs.map +1 -1
  136. package/dist/react/use-teardown.js +20 -19
  137. package/dist/react/use-teardown.js.map +1 -1
  138. package/dist/react-native/NativeCollection.cjs +98 -78
  139. package/dist/react-native/NativeCollection.cjs.map +1 -1
  140. package/dist/react-native/NativeCollection.js +97 -77
  141. package/dist/react-native/NativeCollection.js.map +1 -1
  142. package/dist/react-native.cjs +2 -4
  143. package/dist/react-native.js +1 -4
  144. package/dist/react.cjs +24 -26
  145. package/dist/react.js +1 -17
  146. package/dist/singleton.cjs +28 -22
  147. package/dist/singleton.cjs.map +1 -1
  148. package/dist/singleton.js +29 -26
  149. package/dist/singleton.js.map +1 -1
  150. package/dist/walkPrototypeChain.cjs +20 -12
  151. package/dist/walkPrototypeChain.cjs.map +1 -1
  152. package/dist/walkPrototypeChain.js +21 -13
  153. package/dist/walkPrototypeChain.js.map +1 -1
  154. package/dist/web/IndexedDBCollection.cjs +53 -36
  155. package/dist/web/IndexedDBCollection.cjs.map +1 -1
  156. package/dist/web/IndexedDBCollection.js +52 -35
  157. package/dist/web/IndexedDBCollection.js.map +1 -1
  158. package/dist/web/WebStorageCollection.cjs +82 -84
  159. package/dist/web/WebStorageCollection.cjs.map +1 -1
  160. package/dist/web/WebStorageCollection.js +81 -83
  161. package/dist/web/WebStorageCollection.js.map +1 -1
  162. package/dist/web/idb.cjs +107 -99
  163. package/dist/web/idb.cjs.map +1 -1
  164. package/dist/web/idb.js +108 -105
  165. package/dist/web/idb.js.map +1 -1
  166. package/dist/web.cjs +4 -6
  167. package/dist/web.js +1 -5
  168. package/dist/wrapAsyncMethods.cjs +141 -168
  169. package/dist/wrapAsyncMethods.cjs.map +1 -1
  170. package/dist/wrapAsyncMethods.js +141 -168
  171. package/dist/wrapAsyncMethods.js.map +1 -1
  172. package/package.json +8 -8
  173. package/src/Pending.test.ts +1 -2
  174. package/src/Sorting.test.ts +1 -1
  175. package/src/produceDraft.test.ts +3 -3
  176. package/src/react/components/CardList.test.tsx +1 -1
  177. package/src/react/components/DataTable.test.tsx +1 -1
  178. package/src/react/components/InfiniteScroll.test.tsx +5 -5
  179. package/dist/mvc-kit.cjs.map +0 -1
  180. package/dist/mvc-kit.js.map +0 -1
  181. package/dist/react-native.cjs.map +0 -1
  182. package/dist/react-native.js.map +0 -1
  183. package/dist/react.cjs.map +0 -1
  184. package/dist/react.js.map +0 -1
  185. package/dist/web.cjs.map +0 -1
  186. package/dist/web.js.map +0 -1
@@ -1,583 +1,535 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const EventBus = require("./EventBus.cjs");
4
- const walkPrototypeChain = require("./walkPrototypeChain.cjs");
5
- const wrapAsyncMethods = require("./wrapAsyncMethods.cjs");
6
- const produceDraft = require("./produceDraft.cjs");
7
- const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
1
+ const require_walkPrototypeChain = require("./walkPrototypeChain.cjs");
2
+ const require_EventBus = require("./EventBus.cjs");
3
+ const require_wrapAsyncMethods = require("./wrapAsyncMethods.cjs");
4
+ const require_produceDraft = require("./produceDraft.cjs");
5
+ //#region src/ViewModel.ts
6
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
8
7
  function freeze(obj) {
9
- return __DEV__ ? Object.freeze(obj) : obj;
8
+ return __DEV__ ? Object.freeze(obj) : obj;
10
9
  }
11
- const classMembers = /* @__PURE__ */ new WeakMap();
10
+ var classMembers = /* @__PURE__ */ new WeakMap();
12
11
  function getClassMemberInfo(instance, stopPrototype, reservedKeys, lifecycleHooks) {
13
- const ctor = instance.constructor;
14
- let info = classMembers.get(ctor);
15
- if (info) return info;
16
- const getters = [];
17
- const methods = [];
18
- const found = [];
19
- const processedGetters = /* @__PURE__ */ new Set();
20
- const processedMethods = /* @__PURE__ */ new Set();
21
- walkPrototypeChain.walkPrototypeChain(instance, stopPrototype, (key, desc) => {
22
- if (reservedKeys.includes(key)) {
23
- found.push(key);
24
- }
25
- if (desc.get && !processedGetters.has(key)) {
26
- processedGetters.add(key);
27
- getters.push({ key, get: desc.get });
28
- }
29
- if (!desc.get && !desc.set && typeof desc.value === "function" && !key.startsWith("_") && !lifecycleHooks.has(key) && !processedMethods.has(key)) {
30
- processedMethods.add(key);
31
- methods.push({ key, fn: desc.value });
32
- }
33
- });
34
- info = { getters, methods, reservedKeys: found };
35
- classMembers.set(ctor, info);
36
- return info;
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
+ require_walkPrototypeChain.walkPrototypeChain(instance, stopPrototype, (key, desc) => {
21
+ if (reservedKeys.includes(key)) found.push(key);
22
+ if (desc.get && !processedGetters.has(key)) {
23
+ processedGetters.add(key);
24
+ getters.push({
25
+ key,
26
+ get: desc.get
27
+ });
28
+ }
29
+ if (!desc.get && !desc.set && typeof desc.value === "function" && !key.startsWith("_") && !lifecycleHooks.has(key) && !processedMethods.has(key)) {
30
+ processedMethods.add(key);
31
+ methods.push({
32
+ key,
33
+ fn: desc.value
34
+ });
35
+ }
36
+ });
37
+ info = {
38
+ getters,
39
+ methods,
40
+ reservedKeys: found
41
+ };
42
+ classMembers.set(ctor, info);
43
+ return info;
37
44
  }
38
- let _activeStateTracking = null;
39
- let _activeSourceTracking = null;
45
+ var _activeStateTracking = null;
46
+ var _activeSourceTracking = null;
40
47
  function isAutoTrackable(value) {
41
- return value !== null && typeof value === "object" && typeof value.subscribe === "function";
48
+ return value !== null && typeof value === "object" && typeof value.subscribe === "function";
42
49
  }
43
- const DEFAULT_TASK_STATE = Object.freeze({ loading: false, error: null, errorCode: null });
44
- const RESERVED_ASYNC_KEYS = ["async", "subscribeAsync"];
45
- const LIFECYCLE_HOOKS = /* @__PURE__ */ new Set(["onInit", "onSet", "onDispose"]);
46
- class ViewModel {
47
- _state;
48
- _initialState;
49
- _disposed = false;
50
- _initialized = false;
51
- _listeners = /* @__PURE__ */ new Set();
52
- _abortController = null;
53
- _cleanups = null;
54
- _subscriptionCleanups = null;
55
- _eventBus = null;
56
- // ── Reactive derived state (RFC 1) ─────────────────────────────
57
- _revision = 0;
58
- _trackedSources = /* @__PURE__ */ new Map();
59
- // ── Async tracking (RFC 2) ──────────────────────────────────────
60
- // Lazily allocated on first async method wrap to keep construction fast.
61
- _asyncStates = null;
62
- _asyncSnapshots = null;
63
- _asyncListeners = null;
64
- _asyncProxy = null;
65
- _activeOps = null;
66
- /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */
67
- static GHOST_TIMEOUT = 3e3;
68
- constructor(...args) {
69
- const initialState = args[0] ?? {};
70
- this._state = freeze({ ...initialState });
71
- this._initialState = this._state;
72
- this._guardAndBind();
73
- }
74
- /** Current frozen state object. */
75
- get state() {
76
- return this._state;
77
- }
78
- /** Whether this instance has been disposed. */
79
- get disposed() {
80
- return this._disposed;
81
- }
82
- /** Whether init() has been called. */
83
- get initialized() {
84
- return this._initialized;
85
- }
86
- /** AbortSignal that fires when this instance is disposed. Lazily created. */
87
- get disposeSignal() {
88
- if (!this._abortController) {
89
- this._abortController = new AbortController();
90
- }
91
- return this._abortController.signal;
92
- }
93
- /** Lazily-created typed EventBus for emitting and subscribing to events. */
94
- get events() {
95
- if (!this._eventBus) {
96
- this._eventBus = new EventBus.EventBus();
97
- }
98
- return this._eventBus;
99
- }
100
- /** Initializes the instance. Called automatically by React hooks after mount. */
101
- init() {
102
- if (this._initialized || this._disposed) return;
103
- this._initialized = true;
104
- this._trackSubscribables();
105
- this._installStateProxy();
106
- this._processMembers();
107
- return this.onInit?.();
108
- }
109
- /**
110
- * Merges partial state into current state. If no values actually
111
- * changed by reference, this is a no-op.
112
- *
113
- * Triggers React re-render via listener notification. Called when:
114
- * - User code calls set() to update source state
115
- *
116
- * NOT called for subscribable member notifications — those use
117
- * a separate notification path (see _trackSubscribables).
118
- */
119
- set(partialOrUpdater) {
120
- if (this._disposed) return;
121
- if (__DEV__ && _activeStateTracking) {
122
- console.error(
123
- "[mvc-kit] set() called inside a getter. Getters must be pure — they read state and return a value. They must never call set(), which would cause an infinite render loop. Move this logic to an action method."
124
- );
125
- return;
126
- }
127
- let partial;
128
- if (typeof partialOrUpdater === "function") {
129
- const result = produceDraft.resolveDraftUpdater(this._state, partialOrUpdater);
130
- if (!result) return;
131
- partial = result;
132
- } else {
133
- partial = partialOrUpdater;
134
- }
135
- let hasChanges = false;
136
- const current = this._state;
137
- for (const key in partial) {
138
- if (partial[key] !== current[key]) {
139
- hasChanges = true;
140
- break;
141
- }
142
- }
143
- if (!hasChanges) {
144
- return;
145
- }
146
- const prev = this._state;
147
- const next = freeze({ ...prev, ...partial });
148
- this._state = next;
149
- this._revision++;
150
- this.onSet?.(prev, next);
151
- for (const listener of this._listeners) {
152
- listener(next, prev);
153
- }
154
- }
155
- /**
156
- * Emits a typed event via the internal EventBus.
157
- * Safe to call during dispose cleanup callbacks.
158
- * @protected
159
- */
160
- emit(event, payload) {
161
- if (this._eventBus?.disposed ?? this._disposed) return;
162
- this.events.emit(event, payload);
163
- }
164
- /** Subscribes to state changes. Returns an unsubscribe function. */
165
- subscribe(listener) {
166
- if (this._disposed) {
167
- return () => {
168
- };
169
- }
170
- this._listeners.add(listener);
171
- return () => {
172
- this._listeners.delete(listener);
173
- };
174
- }
175
- /** Tears down the instance, releasing all subscriptions and resources. */
176
- dispose() {
177
- if (this._disposed) {
178
- return;
179
- }
180
- this._disposed = true;
181
- this._teardownSubscriptions();
182
- this._abortController?.abort();
183
- if (this._cleanups) {
184
- for (const fn of this._cleanups) fn();
185
- this._cleanups = null;
186
- }
187
- this._eventBus?.dispose();
188
- this.onDispose?.();
189
- this._listeners.clear();
190
- }
191
- /**
192
- * Resets state to initial values (or provided state), aborts in-flight work,
193
- * clears async tracking, and re-runs onInit().
194
- */
195
- reset(newState) {
196
- if (this._disposed) return;
197
- this._abortController?.abort();
198
- this._abortController = null;
199
- this._teardownSubscriptions();
200
- this._state = newState ? freeze({ ...newState }) : this._initialState;
201
- this._revision++;
202
- this._asyncStates?.clear();
203
- this._asyncSnapshots?.clear();
204
- this._notifyAsync();
205
- this._trackSubscribables();
206
- for (const listener of this._listeners) {
207
- listener(this._state, this._state);
208
- }
209
- return this.onInit?.();
210
- }
211
- /**
212
- * Registers a cleanup function to be called on dispose. Used internally for things like method wrapper
213
- * cleanup and event bus disposal, but can also be used by subclasses for custom cleanup logic.
214
- * @param fn
215
- * @protected
216
- */
217
- addCleanup(fn) {
218
- if (!this._cleanups) {
219
- this._cleanups = [];
220
- }
221
- this._cleanups.push(fn);
222
- }
223
- /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
224
- subscribeTo(source, listener) {
225
- const unsubscribe = source.subscribe(listener);
226
- if (!this._subscriptionCleanups) this._subscriptionCleanups = [];
227
- this._subscriptionCleanups.push(unsubscribe);
228
- return unsubscribe;
229
- }
230
- /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose and reset. @protected */
231
- listenTo(source, event, handler) {
232
- const unsubscribe = source.on(event, handler);
233
- if (!this._subscriptionCleanups) this._subscriptionCleanups = [];
234
- this._subscriptionCleanups.push(unsubscribe);
235
- return unsubscribe;
236
- }
237
- /** Pipes a Channel event into a Collection via upsert. Calls channel.init() and registers auto-cleanup on dispose and reset. @protected */
238
- pipeChannel(channel, type, target) {
239
- channel.init();
240
- return this.listenTo(channel, type, (payload) => {
241
- target.upsert(payload);
242
- });
243
- }
244
- // ── Async tracking API ──────────────────────────────────────────
245
- /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */
246
- get async() {
247
- if (!this._asyncProxy) {
248
- const self = this;
249
- this._asyncProxy = new Proxy({}, {
250
- get(_, prop) {
251
- return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;
252
- },
253
- has(_, prop) {
254
- return self._asyncSnapshots?.has(prop) ?? false;
255
- },
256
- ownKeys() {
257
- return self._asyncSnapshots ? Array.from(self._asyncSnapshots.keys()) : [];
258
- },
259
- getOwnPropertyDescriptor(_, prop) {
260
- if (self._asyncSnapshots?.has(prop)) {
261
- return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };
262
- }
263
- return void 0;
264
- }
265
- });
266
- }
267
- return this._asyncProxy;
268
- }
269
- /** Subscribes to async state changes. Used for React integration. */
270
- subscribeAsync(listener) {
271
- if (this._disposed) return () => {
272
- };
273
- if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
274
- this._asyncListeners.add(listener);
275
- return () => {
276
- this._asyncListeners.delete(listener);
277
- };
278
- }
279
- _notifyAsync() {
280
- if (!this._asyncListeners) return;
281
- for (const listener of this._asyncListeners) {
282
- listener();
283
- }
284
- }
285
- // ── Async tracking internals ────────────────────────────────────
286
- _teardownSubscriptions() {
287
- for (const tracked of this._trackedSources.values()) tracked.unsubscribe();
288
- this._trackedSources.clear();
289
- if (this._subscriptionCleanups) {
290
- for (const fn of this._subscriptionCleanups) fn();
291
- this._subscriptionCleanups = null;
292
- }
293
- }
294
- /**
295
- * Guards reserved keys and auto-binds subclass methods in a single pass
296
- * using the cached class metadata from getClassMemberInfo.
297
- */
298
- _guardAndBind() {
299
- const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
300
- if (info.reservedKeys.length > 0) {
301
- throw new Error(
302
- `[mvc-kit] "${info.reservedKeys[0]}" is a reserved property on ViewModel and cannot be overridden.`
303
- );
304
- }
305
- for (let i = 0; i < info.methods.length; i++) {
306
- this[info.methods[i].key] = info.methods[i].fn.bind(this);
307
- }
308
- }
309
- // ── Member processing (merged getter memoization + async method wrapping) ──
310
- /**
311
- * Uses cached class metadata to memoize getters (RFC 1) and delegates
312
- * async method wrapping to the shared wrapAsyncMethods helper (RFC 2).
313
- * Class metadata is computed once per class via getClassMemberInfo() and reused
314
- * across all instances — avoids repeated prototype walks.
315
- */
316
- _processMembers() {
317
- const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
318
- for (let i = 0; i < info.getters.length; i++) {
319
- this._wrapGetter(info.getters[i].key, info.getters[i].get);
320
- }
321
- if (__DEV__) {
322
- for (const key of RESERVED_ASYNC_KEYS) {
323
- if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) {
324
- throw new Error(
325
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
326
- );
327
- }
328
- }
329
- }
330
- if (info.methods.length === 0) return;
331
- if (!this._asyncStates) this._asyncStates = /* @__PURE__ */ new Map();
332
- if (!this._asyncSnapshots) this._asyncSnapshots = /* @__PURE__ */ new Map();
333
- if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
334
- if (__DEV__) {
335
- this._activeOps = /* @__PURE__ */ new Map();
336
- }
337
- wrapAsyncMethods.wrapAsyncMethods({
338
- instance: this,
339
- stopPrototype: ViewModel.prototype,
340
- reservedKeys: RESERVED_ASYNC_KEYS,
341
- lifecycleHooks: LIFECYCLE_HOOKS,
342
- isDisposed: () => this._disposed,
343
- isInitialized: () => this._initialized,
344
- asyncStates: this._asyncStates,
345
- asyncSnapshots: this._asyncSnapshots,
346
- asyncListeners: this._asyncListeners,
347
- notifyAsync: () => this._notifyAsync(),
348
- addCleanup: (fn) => this.addCleanup(fn),
349
- ghostTimeout: this.constructor.GHOST_TIMEOUT,
350
- className: "ViewModel",
351
- activeOps: this._activeOps,
352
- methods: info.methods
353
- });
354
- }
355
- // ── Auto-tracking internals ────────────────────────────────────
356
- /**
357
- * Installs a context-sensitive state getter on the instance.
358
- *
359
- * During getter tracking (_activeStateTracking is active): returns a Proxy
360
- * that records which state properties are accessed. The Proxy is created
361
- * lazily on first tracking access to keep init() fast.
362
- *
363
- * Otherwise: returns the frozen state object directly. This is critical
364
- * for React's useSyncExternalStore it needs a changing reference to
365
- * detect state updates and trigger re-renders.
366
- */
367
- _installStateProxy() {
368
- let stateProxy = null;
369
- Object.defineProperty(this, "state", {
370
- get: () => {
371
- if (_activeStateTracking) {
372
- if (!stateProxy) {
373
- stateProxy = new Proxy({}, {
374
- get: (_, prop) => {
375
- _activeStateTracking?.add(prop);
376
- return this._state[prop];
377
- },
378
- ownKeys: () => Reflect.ownKeys(this._state),
379
- getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(this._state, prop),
380
- set: () => {
381
- throw new Error("Cannot mutate state directly. Use set() instead.");
382
- },
383
- has: (_, prop) => prop in this._state
384
- });
385
- }
386
- return stateProxy;
387
- }
388
- return this._state;
389
- },
390
- configurable: true,
391
- enumerable: true
392
- });
393
- }
394
- /**
395
- * Scans own instance properties for Subscribable objects and sets up
396
- * automatic dependency tracking for each one found.
397
- *
398
- * For each subscribable member:
399
- * 1. Subscribe to it. On notification: bump its tracked revision
400
- * AND the VM's global revision, then force a new state reference
401
- * and notify listeners so React re-renders.
402
- * 2. Replace the instance property with a getter that participates
403
- * in dependency tracking.
404
- * 3. Register unsubscribe in the dispose chain.
405
- *
406
- * Called during init(), AFTER all subclass property initializers
407
- * have run (they execute during the constructor, before init()).
408
- */
409
- _trackSubscribables() {
410
- for (const key of Object.getOwnPropertyNames(this)) {
411
- const value = this[key];
412
- if (!isAutoTrackable(value)) continue;
413
- let tracked;
414
- const onSourceNotify = () => {
415
- if (this._disposed) return;
416
- tracked.revision++;
417
- this._revision++;
418
- for (const listener of this._listeners) {
419
- listener(this._state, this._state);
420
- }
421
- };
422
- const unsubState = value.subscribe(onSourceNotify);
423
- const unsubAsync = typeof value.subscribeAsync === "function" ? value.subscribeAsync(onSourceNotify) : void 0;
424
- tracked = {
425
- source: value,
426
- revision: 0,
427
- unsubscribe: unsubAsync ? () => {
428
- unsubState();
429
- unsubAsync();
430
- } : unsubState
431
- };
432
- this._trackedSources.set(key, tracked);
433
- Object.defineProperty(this, key, {
434
- get: () => {
435
- _activeSourceTracking?.set(key, tracked);
436
- return value;
437
- },
438
- configurable: true,
439
- enumerable: false
440
- });
441
- }
442
- }
443
- /**
444
- * Bubbles cached dependency records to the active parent tracking context.
445
- * Called from Tier 1/Tier 2 cache hits during nested getter composition
446
- * so the parent getter records the full transitive dependency set.
447
- * Extracted to keep the getter closure small for V8 inlining.
448
- */
449
- _bubbleDeps(stateDepKeys, sourceDepKeys) {
450
- const st = _activeStateTracking;
451
- if (stateDepKeys) {
452
- for (let i = 0; i < stateDepKeys.length; i++) st.add(stateDepKeys[i]);
453
- }
454
- if (sourceDepKeys) {
455
- const srt = _activeSourceTracking;
456
- for (let i = 0; i < sourceDepKeys.length; i++) {
457
- const ts = this._trackedSources.get(sourceDepKeys[i]);
458
- if (ts) srt.set(sourceDepKeys[i], ts);
459
- }
460
- }
461
- }
462
- /**
463
- * Replaces a single prototype getter with a memoized version on this
464
- * instance. The memoized getter tracks both state dependencies and
465
- * subscribable member dependencies, caching its result and
466
- * revalidating through a three-tier strategy:
467
- *
468
- * Tier 1 (fast): revision unchanged → return cached (1 int compare)
469
- * Tier 2 (medium): revision changed but this getter's deps didn't → return cached
470
- * Tier 3 (slow): at least one dep changed → full recompute with tracking
471
- */
472
- _wrapGetter(key, original) {
473
- let cached;
474
- let validatedAtRevision = -1;
475
- let stateDepKeys;
476
- let stateDepValues;
477
- let sourceDepKeys;
478
- let sourceDepRevisions;
479
- let trackingSet;
480
- let trackingMap;
481
- Object.defineProperty(this, key, {
482
- get: () => {
483
- if (validatedAtRevision === this._revision) {
484
- if (_activeStateTracking) this._bubbleDeps(stateDepKeys, sourceDepKeys);
485
- return cached;
486
- }
487
- if (this._disposed) return cached;
488
- if (stateDepKeys !== void 0) {
489
- let fresh = true;
490
- const state = this._state;
491
- for (let i = 0; i < stateDepKeys.length; i++) {
492
- if (state[stateDepKeys[i]] !== stateDepValues[i]) {
493
- fresh = false;
494
- break;
495
- }
496
- }
497
- if (fresh && sourceDepKeys !== void 0 && sourceDepKeys.length > 0) {
498
- for (let i = 0; i < sourceDepKeys.length; i++) {
499
- const ts = this._trackedSources.get(sourceDepKeys[i]);
500
- if (ts && ts.revision !== sourceDepRevisions[i]) {
501
- fresh = false;
502
- break;
503
- }
504
- }
505
- }
506
- if (fresh) {
507
- if (_activeStateTracking) this._bubbleDeps(stateDepKeys, sourceDepKeys);
508
- validatedAtRevision = this._revision;
509
- return cached;
510
- }
511
- }
512
- const parentStateTracking = _activeStateTracking;
513
- const parentSourceTracking = _activeSourceTracking;
514
- if (trackingSet) {
515
- trackingSet.clear();
516
- } else {
517
- trackingSet = /* @__PURE__ */ new Set();
518
- }
519
- if (trackingMap) {
520
- trackingMap.clear();
521
- } else {
522
- trackingMap = /* @__PURE__ */ new Map();
523
- }
524
- _activeStateTracking = trackingSet;
525
- _activeSourceTracking = trackingMap;
526
- try {
527
- cached = original.call(this);
528
- } catch (e) {
529
- _activeStateTracking = parentStateTracking;
530
- _activeSourceTracking = parentSourceTracking;
531
- throw e;
532
- }
533
- _activeStateTracking = parentStateTracking;
534
- _activeSourceTracking = parentSourceTracking;
535
- if (parentStateTracking) {
536
- for (const d of trackingSet) parentStateTracking.add(d);
537
- }
538
- if (parentSourceTracking) {
539
- for (const [k, v] of trackingMap) {
540
- parentSourceTracking.set(k, v);
541
- }
542
- }
543
- const depCount = trackingSet.size;
544
- if (!stateDepKeys || stateDepKeys.length !== depCount) {
545
- stateDepKeys = new Array(depCount);
546
- stateDepValues = new Array(depCount);
547
- }
548
- {
549
- let i = 0;
550
- const state = this._state;
551
- for (const d of trackingSet) {
552
- stateDepKeys[i] = d;
553
- stateDepValues[i] = state[d];
554
- i++;
555
- }
556
- }
557
- const sourceCount = trackingMap.size;
558
- if (sourceCount > 0) {
559
- if (!sourceDepKeys || sourceDepKeys.length !== sourceCount) {
560
- sourceDepKeys = new Array(sourceCount);
561
- sourceDepRevisions = new Array(sourceCount);
562
- }
563
- let i = 0;
564
- for (const [memberKey, tracked] of trackingMap) {
565
- sourceDepKeys[i] = memberKey;
566
- sourceDepRevisions[i] = tracked.revision;
567
- i++;
568
- }
569
- } else {
570
- sourceDepKeys = void 0;
571
- sourceDepRevisions = void 0;
572
- }
573
- validatedAtRevision = this._revision;
574
- return cached;
575
- },
576
- configurable: true,
577
- enumerable: true
578
- });
579
- }
580
- }
581
- exports.walkPrototypeChain = walkPrototypeChain.walkPrototypeChain;
50
+ var DEFAULT_TASK_STATE = Object.freeze({
51
+ loading: false,
52
+ error: null,
53
+ errorCode: null
54
+ });
55
+ var RESERVED_ASYNC_KEYS = ["async", "subscribeAsync"];
56
+ var LIFECYCLE_HOOKS = new Set([
57
+ "onInit",
58
+ "onSet",
59
+ "onDispose"
60
+ ]);
61
+ /**
62
+ * Reactive state container with computed getters, automatic async tracking, and typed events.
63
+ * Subclass and define state shape, getters, and action methods. Use with `useLocal` in React.
64
+ */
65
+ var ViewModel = class ViewModel {
66
+ _state;
67
+ _initialState;
68
+ _disposed = false;
69
+ _initialized = false;
70
+ _listeners = /* @__PURE__ */ new Set();
71
+ _abortController = null;
72
+ _cleanups = null;
73
+ _subscriptionCleanups = null;
74
+ _eventBus = null;
75
+ _revision = 0;
76
+ _trackedSources = /* @__PURE__ */ new Map();
77
+ _asyncStates = null;
78
+ _asyncSnapshots = null;
79
+ _asyncListeners = null;
80
+ _asyncProxy = null;
81
+ _activeOps = null;
82
+ /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */
83
+ static GHOST_TIMEOUT = 3e3;
84
+ constructor(...args) {
85
+ this._state = freeze({ ...args[0] ?? {} });
86
+ this._initialState = this._state;
87
+ this._guardAndBind();
88
+ }
89
+ /** Current frozen state object. */
90
+ get state() {
91
+ return this._state;
92
+ }
93
+ /** Whether this instance has been disposed. */
94
+ get disposed() {
95
+ return this._disposed;
96
+ }
97
+ /** Whether init() has been called. */
98
+ get initialized() {
99
+ return this._initialized;
100
+ }
101
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
102
+ get disposeSignal() {
103
+ if (!this._abortController) this._abortController = new AbortController();
104
+ return this._abortController.signal;
105
+ }
106
+ /** Lazily-created typed EventBus for emitting and subscribing to events. */
107
+ get events() {
108
+ if (!this._eventBus) this._eventBus = new require_EventBus.EventBus();
109
+ return this._eventBus;
110
+ }
111
+ /** Initializes the instance. Called automatically by React hooks after mount. */
112
+ init() {
113
+ if (this._initialized || this._disposed) return;
114
+ this._initialized = true;
115
+ this._trackSubscribables();
116
+ this._installStateProxy();
117
+ this._processMembers();
118
+ return this.onInit?.();
119
+ }
120
+ /**
121
+ * Merges partial state into current state. If no values actually
122
+ * changed by reference, this is a no-op.
123
+ *
124
+ * Triggers React re-render via listener notification. Called when:
125
+ * - User code calls set() to update source state
126
+ *
127
+ * NOT called for subscribable member notifications — those use
128
+ * a separate notification path (see _trackSubscribables).
129
+ */
130
+ set(partialOrUpdater) {
131
+ if (this._disposed) return;
132
+ if (__DEV__ && _activeStateTracking) {
133
+ console.error("[mvc-kit] set() called inside a getter. Getters must be pure — they read state and return a value. They must never call set(), which would cause an infinite render loop. Move this logic to an action method.");
134
+ return;
135
+ }
136
+ let partial;
137
+ if (typeof partialOrUpdater === "function") {
138
+ const result = require_produceDraft.resolveDraftUpdater(this._state, partialOrUpdater);
139
+ if (!result) return;
140
+ partial = result;
141
+ } else partial = partialOrUpdater;
142
+ let hasChanges = false;
143
+ const current = this._state;
144
+ for (const key in partial) if (partial[key] !== current[key]) {
145
+ hasChanges = true;
146
+ break;
147
+ }
148
+ if (!hasChanges) return;
149
+ const prev = this._state;
150
+ const next = freeze({
151
+ ...prev,
152
+ ...partial
153
+ });
154
+ this._state = next;
155
+ this._revision++;
156
+ this.onSet?.(prev, next);
157
+ for (const listener of this._listeners) listener(next, prev);
158
+ }
159
+ /**
160
+ * Emits a typed event via the internal EventBus.
161
+ * Safe to call during dispose cleanup callbacks.
162
+ * @protected
163
+ */
164
+ emit(event, payload) {
165
+ if (this._eventBus?.disposed ?? this._disposed) return;
166
+ this.events.emit(event, payload);
167
+ }
168
+ /** Subscribes to state changes. Returns an unsubscribe function. */
169
+ subscribe(listener) {
170
+ if (this._disposed) return () => {};
171
+ this._listeners.add(listener);
172
+ return () => {
173
+ this._listeners.delete(listener);
174
+ };
175
+ }
176
+ /** Tears down the instance, releasing all subscriptions and resources. */
177
+ dispose() {
178
+ if (this._disposed) return;
179
+ this._disposed = true;
180
+ this._teardownSubscriptions();
181
+ this._abortController?.abort();
182
+ if (this._cleanups) {
183
+ for (const fn of this._cleanups) fn();
184
+ this._cleanups = null;
185
+ }
186
+ this._eventBus?.dispose();
187
+ this.onDispose?.();
188
+ this._listeners.clear();
189
+ }
190
+ /**
191
+ * Resets state to initial values (or provided state), aborts in-flight work,
192
+ * clears async tracking, and re-runs onInit().
193
+ */
194
+ reset(newState) {
195
+ if (this._disposed) return;
196
+ this._abortController?.abort();
197
+ this._abortController = null;
198
+ this._teardownSubscriptions();
199
+ this._state = newState ? freeze({ ...newState }) : this._initialState;
200
+ this._revision++;
201
+ this._asyncStates?.clear();
202
+ this._asyncSnapshots?.clear();
203
+ this._notifyAsync();
204
+ this._trackSubscribables();
205
+ for (const listener of this._listeners) listener(this._state, this._state);
206
+ return this.onInit?.();
207
+ }
208
+ /**
209
+ * Registers a cleanup function to be called on dispose. Used internally for things like method wrapper
210
+ * cleanup and event bus disposal, but can also be used by subclasses for custom cleanup logic.
211
+ * @param fn
212
+ * @protected
213
+ */
214
+ addCleanup(fn) {
215
+ if (!this._cleanups) this._cleanups = [];
216
+ this._cleanups.push(fn);
217
+ }
218
+ /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
219
+ subscribeTo(source, listener) {
220
+ const unsubscribe = source.subscribe(listener);
221
+ if (!this._subscriptionCleanups) this._subscriptionCleanups = [];
222
+ this._subscriptionCleanups.push(unsubscribe);
223
+ return unsubscribe;
224
+ }
225
+ /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose and reset. @protected */
226
+ listenTo(source, event, handler) {
227
+ const unsubscribe = source.on(event, handler);
228
+ if (!this._subscriptionCleanups) this._subscriptionCleanups = [];
229
+ this._subscriptionCleanups.push(unsubscribe);
230
+ return unsubscribe;
231
+ }
232
+ /** Pipes a Channel event into a Collection via upsert. Calls channel.init() and registers auto-cleanup on dispose and reset. @protected */
233
+ pipeChannel(channel, type, target) {
234
+ channel.init();
235
+ return this.listenTo(channel, type, (payload) => {
236
+ target.upsert(payload);
237
+ });
238
+ }
239
+ /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */
240
+ get async() {
241
+ if (!this._asyncProxy) {
242
+ const self = this;
243
+ this._asyncProxy = new Proxy({}, {
244
+ get(_, prop) {
245
+ return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;
246
+ },
247
+ has(_, prop) {
248
+ return self._asyncSnapshots?.has(prop) ?? false;
249
+ },
250
+ ownKeys() {
251
+ return self._asyncSnapshots ? Array.from(self._asyncSnapshots.keys()) : [];
252
+ },
253
+ getOwnPropertyDescriptor(_, prop) {
254
+ if (self._asyncSnapshots?.has(prop)) return {
255
+ configurable: true,
256
+ enumerable: true,
257
+ value: self._asyncSnapshots.get(prop)
258
+ };
259
+ }
260
+ });
261
+ }
262
+ return this._asyncProxy;
263
+ }
264
+ /** Subscribes to async state changes. Used for React integration. */
265
+ subscribeAsync(listener) {
266
+ if (this._disposed) return () => {};
267
+ if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
268
+ this._asyncListeners.add(listener);
269
+ return () => {
270
+ this._asyncListeners.delete(listener);
271
+ };
272
+ }
273
+ _notifyAsync() {
274
+ if (!this._asyncListeners) return;
275
+ for (const listener of this._asyncListeners) listener();
276
+ }
277
+ _teardownSubscriptions() {
278
+ for (const tracked of this._trackedSources.values()) tracked.unsubscribe();
279
+ this._trackedSources.clear();
280
+ if (this._subscriptionCleanups) {
281
+ for (const fn of this._subscriptionCleanups) fn();
282
+ this._subscriptionCleanups = null;
283
+ }
284
+ }
285
+ /**
286
+ * Guards reserved keys and auto-binds subclass methods in a single pass
287
+ * using the cached class metadata from getClassMemberInfo.
288
+ */
289
+ _guardAndBind() {
290
+ const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
291
+ if (info.reservedKeys.length > 0) throw new Error(`[mvc-kit] "${info.reservedKeys[0]}" is a reserved property on ViewModel and cannot be overridden.`);
292
+ for (let i = 0; i < info.methods.length; i++) this[info.methods[i].key] = info.methods[i].fn.bind(this);
293
+ }
294
+ /**
295
+ * Uses cached class metadata to memoize getters (RFC 1) and delegates
296
+ * async method wrapping to the shared wrapAsyncMethods helper (RFC 2).
297
+ * Class metadata is computed once per class via getClassMemberInfo() and reused
298
+ * across all instances — avoids repeated prototype walks.
299
+ */
300
+ _processMembers() {
301
+ const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
302
+ for (let i = 0; i < info.getters.length; i++) this._wrapGetter(info.getters[i].key, info.getters[i].get);
303
+ if (__DEV__) {
304
+ for (const key of RESERVED_ASYNC_KEYS) if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) throw new Error(`[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`);
305
+ }
306
+ if (info.methods.length === 0) return;
307
+ if (!this._asyncStates) this._asyncStates = /* @__PURE__ */ new Map();
308
+ if (!this._asyncSnapshots) this._asyncSnapshots = /* @__PURE__ */ new Map();
309
+ if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
310
+ if (__DEV__) this._activeOps = /* @__PURE__ */ new Map();
311
+ require_wrapAsyncMethods.wrapAsyncMethods({
312
+ instance: this,
313
+ stopPrototype: ViewModel.prototype,
314
+ reservedKeys: RESERVED_ASYNC_KEYS,
315
+ lifecycleHooks: LIFECYCLE_HOOKS,
316
+ isDisposed: () => this._disposed,
317
+ isInitialized: () => this._initialized,
318
+ asyncStates: this._asyncStates,
319
+ asyncSnapshots: this._asyncSnapshots,
320
+ asyncListeners: this._asyncListeners,
321
+ notifyAsync: () => this._notifyAsync(),
322
+ addCleanup: (fn) => this.addCleanup(fn),
323
+ ghostTimeout: this.constructor.GHOST_TIMEOUT,
324
+ className: "ViewModel",
325
+ activeOps: this._activeOps,
326
+ methods: info.methods
327
+ });
328
+ }
329
+ /**
330
+ * Installs a context-sensitive state getter on the instance.
331
+ *
332
+ * During getter tracking (_activeStateTracking is active): returns a Proxy
333
+ * that records which state properties are accessed. The Proxy is created
334
+ * lazily on first tracking access to keep init() fast.
335
+ *
336
+ * Otherwise: returns the frozen state object directly. This is critical
337
+ * for React's useSyncExternalStore — it needs a changing reference to
338
+ * detect state updates and trigger re-renders.
339
+ */
340
+ _installStateProxy() {
341
+ let stateProxy = null;
342
+ Object.defineProperty(this, "state", {
343
+ get: () => {
344
+ if (_activeStateTracking) {
345
+ if (!stateProxy) stateProxy = new Proxy({}, {
346
+ get: (_, prop) => {
347
+ _activeStateTracking?.add(prop);
348
+ return this._state[prop];
349
+ },
350
+ ownKeys: () => Reflect.ownKeys(this._state),
351
+ getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(this._state, prop),
352
+ set: () => {
353
+ throw new Error("Cannot mutate state directly. Use set() instead.");
354
+ },
355
+ has: (_, prop) => prop in this._state
356
+ });
357
+ return stateProxy;
358
+ }
359
+ return this._state;
360
+ },
361
+ configurable: true,
362
+ enumerable: true
363
+ });
364
+ }
365
+ /**
366
+ * Scans own instance properties for Subscribable objects and sets up
367
+ * automatic dependency tracking for each one found.
368
+ *
369
+ * For each subscribable member:
370
+ * 1. Subscribe to it. On notification: bump its tracked revision
371
+ * AND the VM's global revision, then force a new state reference
372
+ * and notify listeners so React re-renders.
373
+ * 2. Replace the instance property with a getter that participates
374
+ * in dependency tracking.
375
+ * 3. Register unsubscribe in the dispose chain.
376
+ *
377
+ * Called during init(), AFTER all subclass property initializers
378
+ * have run (they execute during the constructor, before init()).
379
+ */
380
+ _trackSubscribables() {
381
+ for (const key of Object.getOwnPropertyNames(this)) {
382
+ const value = this[key];
383
+ if (!isAutoTrackable(value)) continue;
384
+ let tracked;
385
+ const onSourceNotify = () => {
386
+ if (this._disposed) return;
387
+ tracked.revision++;
388
+ this._revision++;
389
+ for (const listener of this._listeners) listener(this._state, this._state);
390
+ };
391
+ const unsubState = value.subscribe(onSourceNotify);
392
+ const unsubAsync = typeof value.subscribeAsync === "function" ? value.subscribeAsync(onSourceNotify) : void 0;
393
+ tracked = {
394
+ source: value,
395
+ revision: 0,
396
+ unsubscribe: unsubAsync ? () => {
397
+ unsubState();
398
+ unsubAsync();
399
+ } : unsubState
400
+ };
401
+ this._trackedSources.set(key, tracked);
402
+ Object.defineProperty(this, key, {
403
+ get: () => {
404
+ _activeSourceTracking?.set(key, tracked);
405
+ return value;
406
+ },
407
+ configurable: true,
408
+ enumerable: false
409
+ });
410
+ }
411
+ }
412
+ /**
413
+ * Bubbles cached dependency records to the active parent tracking context.
414
+ * Called from Tier 1/Tier 2 cache hits during nested getter composition
415
+ * so the parent getter records the full transitive dependency set.
416
+ * Extracted to keep the getter closure small for V8 inlining.
417
+ */
418
+ _bubbleDeps(stateDepKeys, sourceDepKeys) {
419
+ const st = _activeStateTracking;
420
+ if (stateDepKeys) for (let i = 0; i < stateDepKeys.length; i++) st.add(stateDepKeys[i]);
421
+ if (sourceDepKeys) {
422
+ const srt = _activeSourceTracking;
423
+ for (let i = 0; i < sourceDepKeys.length; i++) {
424
+ const ts = this._trackedSources.get(sourceDepKeys[i]);
425
+ if (ts) srt.set(sourceDepKeys[i], ts);
426
+ }
427
+ }
428
+ }
429
+ /**
430
+ * Replaces a single prototype getter with a memoized version on this
431
+ * instance. The memoized getter tracks both state dependencies and
432
+ * subscribable member dependencies, caching its result and
433
+ * revalidating through a three-tier strategy:
434
+ *
435
+ * Tier 1 (fast): revision unchanged → return cached (1 int compare)
436
+ * Tier 2 (medium): revision changed but this getter's deps didn't → return cached
437
+ * Tier 3 (slow): at least one dep changed → full recompute with tracking
438
+ */
439
+ _wrapGetter(key, original) {
440
+ let cached;
441
+ let validatedAtRevision = -1;
442
+ let stateDepKeys;
443
+ let stateDepValues;
444
+ let sourceDepKeys;
445
+ let sourceDepRevisions;
446
+ let trackingSet;
447
+ let trackingMap;
448
+ Object.defineProperty(this, key, {
449
+ get: () => {
450
+ if (validatedAtRevision === this._revision) {
451
+ if (_activeStateTracking) this._bubbleDeps(stateDepKeys, sourceDepKeys);
452
+ return cached;
453
+ }
454
+ if (this._disposed) return cached;
455
+ if (stateDepKeys !== void 0) {
456
+ let fresh = true;
457
+ const state = this._state;
458
+ for (let i = 0; i < stateDepKeys.length; i++) if (state[stateDepKeys[i]] !== stateDepValues[i]) {
459
+ fresh = false;
460
+ break;
461
+ }
462
+ if (fresh && sourceDepKeys !== void 0 && sourceDepKeys.length > 0) for (let i = 0; i < sourceDepKeys.length; i++) {
463
+ const ts = this._trackedSources.get(sourceDepKeys[i]);
464
+ if (ts && ts.revision !== sourceDepRevisions[i]) {
465
+ fresh = false;
466
+ break;
467
+ }
468
+ }
469
+ if (fresh) {
470
+ if (_activeStateTracking) this._bubbleDeps(stateDepKeys, sourceDepKeys);
471
+ validatedAtRevision = this._revision;
472
+ return cached;
473
+ }
474
+ }
475
+ const parentStateTracking = _activeStateTracking;
476
+ const parentSourceTracking = _activeSourceTracking;
477
+ if (trackingSet) trackingSet.clear();
478
+ else trackingSet = /* @__PURE__ */ new Set();
479
+ if (trackingMap) trackingMap.clear();
480
+ else trackingMap = /* @__PURE__ */ new Map();
481
+ _activeStateTracking = trackingSet;
482
+ _activeSourceTracking = trackingMap;
483
+ try {
484
+ cached = original.call(this);
485
+ } catch (e) {
486
+ _activeStateTracking = parentStateTracking;
487
+ _activeSourceTracking = parentSourceTracking;
488
+ throw e;
489
+ }
490
+ _activeStateTracking = parentStateTracking;
491
+ _activeSourceTracking = parentSourceTracking;
492
+ if (parentStateTracking) for (const d of trackingSet) parentStateTracking.add(d);
493
+ if (parentSourceTracking) for (const [k, v] of trackingMap) parentSourceTracking.set(k, v);
494
+ const depCount = trackingSet.size;
495
+ if (!stateDepKeys || stateDepKeys.length !== depCount) {
496
+ stateDepKeys = new Array(depCount);
497
+ stateDepValues = new Array(depCount);
498
+ }
499
+ {
500
+ let i = 0;
501
+ const state = this._state;
502
+ for (const d of trackingSet) {
503
+ stateDepKeys[i] = d;
504
+ stateDepValues[i] = state[d];
505
+ i++;
506
+ }
507
+ }
508
+ const sourceCount = trackingMap.size;
509
+ if (sourceCount > 0) {
510
+ if (!sourceDepKeys || sourceDepKeys.length !== sourceCount) {
511
+ sourceDepKeys = new Array(sourceCount);
512
+ sourceDepRevisions = new Array(sourceCount);
513
+ }
514
+ let i = 0;
515
+ for (const [memberKey, tracked] of trackingMap) {
516
+ sourceDepKeys[i] = memberKey;
517
+ sourceDepRevisions[i] = tracked.revision;
518
+ i++;
519
+ }
520
+ } else {
521
+ sourceDepKeys = void 0;
522
+ sourceDepRevisions = void 0;
523
+ }
524
+ validatedAtRevision = this._revision;
525
+ return cached;
526
+ },
527
+ configurable: true,
528
+ enumerable: true
529
+ });
530
+ }
531
+ };
532
+ //#endregion
582
533
  exports.ViewModel = ViewModel;
583
- //# sourceMappingURL=ViewModel.cjs.map
534
+
535
+ //# sourceMappingURL=ViewModel.cjs.map