mvc-kit 2.12.5 → 2.13.1

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 (382) hide show
  1. package/BEST_PRACTICES.md +1390 -0
  2. package/agent-config/bin/postinstall.mjs +4 -3
  3. package/agent-config/bin/setup.mjs +5 -1
  4. package/agent-config/claude-code/agents/mvc-kit-architect.md +16 -8
  5. package/agent-config/claude-code/skills/guide/SKILL.md +29 -7
  6. package/agent-config/claude-code/skills/guide/patterns.md +12 -0
  7. package/agent-config/claude-code/skills/guide/recipes.md +510 -0
  8. package/agent-config/claude-code/skills/guide/testing.md +297 -0
  9. package/agent-config/claude-code/skills/review/SKILL.md +3 -13
  10. package/agent-config/claude-code/skills/review/checklist.md +30 -5
  11. package/agent-config/claude-code/skills/scaffold/SKILL.md +4 -13
  12. package/agent-config/lib/install-claude.mjs +90 -25
  13. package/dist/Channel.cjs +276 -300
  14. package/dist/Channel.cjs.map +1 -1
  15. package/dist/Channel.js +275 -299
  16. package/dist/Channel.js.map +1 -1
  17. package/dist/Collection.cjs +424 -504
  18. package/dist/Collection.cjs.map +1 -1
  19. package/dist/Collection.js +423 -503
  20. package/dist/Collection.js.map +1 -1
  21. package/dist/Controller.cjs +70 -67
  22. package/dist/Controller.cjs.map +1 -1
  23. package/dist/Controller.js +69 -66
  24. package/dist/Controller.js.map +1 -1
  25. package/dist/EventBus.cjs +77 -88
  26. package/dist/EventBus.cjs.map +1 -1
  27. package/dist/EventBus.js +76 -87
  28. package/dist/EventBus.js.map +1 -1
  29. package/dist/Feed.cjs +81 -77
  30. package/dist/Feed.cjs.map +1 -1
  31. package/dist/Feed.js +80 -76
  32. package/dist/Feed.js.map +1 -1
  33. package/dist/Model.cjs +181 -207
  34. package/dist/Model.cjs.map +1 -1
  35. package/dist/Model.js +179 -205
  36. package/dist/Model.js.map +1 -1
  37. package/dist/Pagination.cjs +75 -73
  38. package/dist/Pagination.cjs.map +1 -1
  39. package/dist/Pagination.js +74 -72
  40. package/dist/Pagination.js.map +1 -1
  41. package/dist/Pending.cjs +255 -287
  42. package/dist/Pending.cjs.map +1 -1
  43. package/dist/Pending.js +253 -285
  44. package/dist/Pending.js.map +1 -1
  45. package/dist/PersistentCollection.cjs +242 -285
  46. package/dist/PersistentCollection.cjs.map +1 -1
  47. package/dist/PersistentCollection.js +241 -284
  48. package/dist/PersistentCollection.js.map +1 -1
  49. package/dist/Resource.cjs +166 -174
  50. package/dist/Resource.cjs.map +1 -1
  51. package/dist/Resource.js +164 -172
  52. package/dist/Resource.js.map +1 -1
  53. package/dist/Selection.cjs +84 -94
  54. package/dist/Selection.cjs.map +1 -1
  55. package/dist/Selection.js +83 -93
  56. package/dist/Selection.js.map +1 -1
  57. package/dist/Service.cjs +54 -55
  58. package/dist/Service.cjs.map +1 -1
  59. package/dist/Service.js +53 -54
  60. package/dist/Service.js.map +1 -1
  61. package/dist/Sorting.cjs +102 -101
  62. package/dist/Sorting.cjs.map +1 -1
  63. package/dist/Sorting.js +102 -101
  64. package/dist/Sorting.js.map +1 -1
  65. package/dist/Trackable.cjs +112 -80
  66. package/dist/Trackable.cjs.map +1 -1
  67. package/dist/Trackable.js +111 -79
  68. package/dist/Trackable.js.map +1 -1
  69. package/dist/ViewModel.cjs +528 -576
  70. package/dist/ViewModel.cjs.map +1 -1
  71. package/dist/ViewModel.js +525 -573
  72. package/dist/ViewModel.js.map +1 -1
  73. package/dist/bindPublicMethods.cjs +43 -24
  74. package/dist/bindPublicMethods.cjs.map +1 -1
  75. package/dist/bindPublicMethods.js +43 -24
  76. package/dist/bindPublicMethods.js.map +1 -1
  77. package/dist/errors.cjs +67 -68
  78. package/dist/errors.cjs.map +1 -1
  79. package/dist/errors.js +68 -71
  80. package/dist/errors.js.map +1 -1
  81. package/dist/mvc-kit.cjs +44 -46
  82. package/dist/mvc-kit.js +5 -32
  83. package/dist/produceDraft.cjs +105 -95
  84. package/dist/produceDraft.cjs.map +1 -1
  85. package/dist/produceDraft.js +106 -97
  86. package/dist/produceDraft.js.map +1 -1
  87. package/dist/react/components/CardList.cjs +30 -40
  88. package/dist/react/components/CardList.cjs.map +1 -1
  89. package/dist/react/components/CardList.js +31 -41
  90. package/dist/react/components/CardList.js.map +1 -1
  91. package/dist/react/components/DataTable.cjs +146 -169
  92. package/dist/react/components/DataTable.cjs.map +1 -1
  93. package/dist/react/components/DataTable.js +147 -170
  94. package/dist/react/components/DataTable.js.map +1 -1
  95. package/dist/react/components/InfiniteScroll.cjs +51 -42
  96. package/dist/react/components/InfiniteScroll.cjs.map +1 -1
  97. package/dist/react/components/InfiniteScroll.js +52 -43
  98. package/dist/react/components/InfiniteScroll.js.map +1 -1
  99. package/dist/react/components/types.cjs +10 -6
  100. package/dist/react/components/types.cjs.map +1 -1
  101. package/dist/react/components/types.js +11 -9
  102. package/dist/react/components/types.js.map +1 -1
  103. package/dist/react/guards.cjs +10 -6
  104. package/dist/react/guards.cjs.map +1 -1
  105. package/dist/react/guards.js +11 -9
  106. package/dist/react/guards.js.map +1 -1
  107. package/dist/react/provider.cjs +23 -20
  108. package/dist/react/provider.cjs.map +1 -1
  109. package/dist/react/provider.js +23 -21
  110. package/dist/react/provider.js.map +1 -1
  111. package/dist/react/use-event-bus.cjs +24 -20
  112. package/dist/react/use-event-bus.cjs.map +1 -1
  113. package/dist/react/use-event-bus.js +24 -21
  114. package/dist/react/use-event-bus.js.map +1 -1
  115. package/dist/react/use-instance.cjs +43 -36
  116. package/dist/react/use-instance.cjs.map +1 -1
  117. package/dist/react/use-instance.js +43 -36
  118. package/dist/react/use-instance.js.map +1 -1
  119. package/dist/react/use-local.cjs +48 -64
  120. package/dist/react/use-local.cjs.map +1 -1
  121. package/dist/react/use-local.js +47 -63
  122. package/dist/react/use-local.js.map +1 -1
  123. package/dist/react/use-model.cjs +84 -98
  124. package/dist/react/use-model.cjs.map +1 -1
  125. package/dist/react/use-model.js +84 -100
  126. package/dist/react/use-model.js.map +1 -1
  127. package/dist/react/use-singleton.cjs +19 -23
  128. package/dist/react/use-singleton.cjs.map +1 -1
  129. package/dist/react/use-singleton.js +16 -20
  130. package/dist/react/use-singleton.js.map +1 -1
  131. package/dist/react/use-subscribe-only.cjs +28 -22
  132. package/dist/react/use-subscribe-only.cjs.map +1 -1
  133. package/dist/react/use-subscribe-only.js +28 -22
  134. package/dist/react/use-subscribe-only.js.map +1 -1
  135. package/dist/react/use-teardown.cjs +20 -19
  136. package/dist/react/use-teardown.cjs.map +1 -1
  137. package/dist/react/use-teardown.js +20 -19
  138. package/dist/react/use-teardown.js.map +1 -1
  139. package/dist/react-native/NativeCollection.cjs +98 -78
  140. package/dist/react-native/NativeCollection.cjs.map +1 -1
  141. package/dist/react-native/NativeCollection.js +97 -77
  142. package/dist/react-native/NativeCollection.js.map +1 -1
  143. package/dist/react-native.cjs +2 -4
  144. package/dist/react-native.js +1 -4
  145. package/dist/react.cjs +24 -26
  146. package/dist/react.js +1 -17
  147. package/dist/singleton.cjs +28 -22
  148. package/dist/singleton.cjs.map +1 -1
  149. package/dist/singleton.js +29 -26
  150. package/dist/singleton.js.map +1 -1
  151. package/dist/walkPrototypeChain.cjs +20 -12
  152. package/dist/walkPrototypeChain.cjs.map +1 -1
  153. package/dist/walkPrototypeChain.js +21 -13
  154. package/dist/walkPrototypeChain.js.map +1 -1
  155. package/dist/web/IndexedDBCollection.cjs +53 -36
  156. package/dist/web/IndexedDBCollection.cjs.map +1 -1
  157. package/dist/web/IndexedDBCollection.js +52 -35
  158. package/dist/web/IndexedDBCollection.js.map +1 -1
  159. package/dist/web/WebStorageCollection.cjs +82 -84
  160. package/dist/web/WebStorageCollection.cjs.map +1 -1
  161. package/dist/web/WebStorageCollection.js +81 -83
  162. package/dist/web/WebStorageCollection.js.map +1 -1
  163. package/dist/web/idb.cjs +107 -99
  164. package/dist/web/idb.cjs.map +1 -1
  165. package/dist/web/idb.js +108 -105
  166. package/dist/web/idb.js.map +1 -1
  167. package/dist/web.cjs +4 -6
  168. package/dist/web.js +1 -5
  169. package/dist/wrapAsyncMethods.cjs +141 -168
  170. package/dist/wrapAsyncMethods.cjs.map +1 -1
  171. package/dist/wrapAsyncMethods.js +141 -168
  172. package/dist/wrapAsyncMethods.js.map +1 -1
  173. package/examples/primitive/channel.ts +109 -0
  174. package/examples/primitive/collection.ts +118 -0
  175. package/examples/primitive/controller.ts +118 -0
  176. package/examples/primitive/counter.ts +108 -0
  177. package/examples/primitive/env.d.ts +1 -0
  178. package/examples/primitive/eventbus.ts +77 -0
  179. package/examples/primitive/feed.ts +162 -0
  180. package/examples/primitive/model.ts +82 -0
  181. package/examples/primitive/pagination.ts +91 -0
  182. package/examples/primitive/pending.ts +189 -0
  183. package/examples/primitive/persistent-collection.ts +116 -0
  184. package/examples/primitive/resource.ts +114 -0
  185. package/examples/primitive/selection.ts +96 -0
  186. package/examples/primitive/sorting.ts +112 -0
  187. package/examples/primitive/timer.ts +58 -0
  188. package/examples/primitive/trackable.ts +225 -0
  189. package/examples/primitive/tsconfig.json +20 -0
  190. package/examples/primitive/viewmodel-service.ts +161 -0
  191. package/examples/react/AuthExample/index.html +12 -0
  192. package/examples/react/AuthExample/src/App.tsx +29 -0
  193. package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
  194. package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
  195. package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
  196. package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
  197. package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
  198. package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
  199. package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
  200. package/examples/react/AuthExample/src/env.d.ts +10 -0
  201. package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
  202. package/examples/react/AuthExample/src/main.tsx +10 -0
  203. package/examples/react/AuthExample/src/mock/api.ts +78 -0
  204. package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
  205. package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
  206. package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
  207. package/examples/react/AuthExample/src/styles.css +445 -0
  208. package/examples/react/AuthExample/src/types/auth.ts +12 -0
  209. package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
  210. package/examples/react/AuthExample/tsconfig.json +22 -0
  211. package/examples/react/AuthExample/vite.config.ts +18 -0
  212. package/examples/react/ComplexApp/index.html +12 -0
  213. package/examples/react/ComplexApp/src/App.tsx +17 -0
  214. package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
  215. package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
  216. package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
  217. package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
  218. package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
  219. package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
  220. package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
  221. package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
  222. package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
  223. package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
  224. package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
  225. package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
  226. package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
  227. package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
  228. package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
  229. package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
  230. package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
  231. package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
  232. package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
  233. package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
  234. package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
  235. package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
  236. package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
  237. package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
  238. package/examples/react/ComplexApp/src/env.d.ts +10 -0
  239. package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
  240. package/examples/react/ComplexApp/src/main.tsx +10 -0
  241. package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
  242. package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
  243. package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
  244. package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
  245. package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
  246. package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
  247. package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
  248. package/examples/react/ComplexApp/src/styles.css +463 -0
  249. package/examples/react/ComplexApp/src/types/activity.ts +8 -0
  250. package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
  251. package/examples/react/ComplexApp/src/types/social.ts +8 -0
  252. package/examples/react/ComplexApp/src/types/users.ts +6 -0
  253. package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
  254. package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
  255. package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
  256. package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
  257. package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
  258. package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
  259. package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
  260. package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
  261. package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
  262. package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
  263. package/examples/react/ComplexApp/tsconfig.json +22 -0
  264. package/examples/react/ComplexApp/vite.config.ts +18 -0
  265. package/examples/react/FullApp/index.html +12 -0
  266. package/examples/react/FullApp/src/App.tsx +28 -0
  267. package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
  268. package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
  269. package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
  270. package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
  271. package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
  272. package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
  273. package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
  274. package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
  275. package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
  276. package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
  277. package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
  278. package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
  279. package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
  280. package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
  281. package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
  282. package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
  283. package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
  284. package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
  285. package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
  286. package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
  287. package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
  288. package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
  289. package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
  290. package/examples/react/FullApp/src/env.d.ts +10 -0
  291. package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
  292. package/examples/react/FullApp/src/main.tsx +10 -0
  293. package/examples/react/FullApp/src/mock/delay.ts +21 -0
  294. package/examples/react/FullApp/src/mock/locations.ts +76 -0
  295. package/examples/react/FullApp/src/mock/messages.ts +237 -0
  296. package/examples/react/FullApp/src/mock/users.ts +84 -0
  297. package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
  298. package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
  299. package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
  300. package/examples/react/FullApp/src/services/AuthService.ts +18 -0
  301. package/examples/react/FullApp/src/services/LocationService.ts +23 -0
  302. package/examples/react/FullApp/src/services/MessageService.ts +65 -0
  303. package/examples/react/FullApp/src/services/UserService.ts +23 -0
  304. package/examples/react/FullApp/src/styles.css +767 -0
  305. package/examples/react/FullApp/src/types/conversation.ts +7 -0
  306. package/examples/react/FullApp/src/types/location.ts +12 -0
  307. package/examples/react/FullApp/src/types/message.ts +7 -0
  308. package/examples/react/FullApp/src/types/user.ts +10 -0
  309. package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
  310. package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
  311. package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
  312. package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
  313. package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
  314. package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
  315. package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
  316. package/examples/react/FullApp/tsconfig.json +22 -0
  317. package/examples/react/FullApp/vite.config.ts +18 -0
  318. package/examples/react/WorkerApp/index.html +12 -0
  319. package/examples/react/WorkerApp/src/App.tsx +24 -0
  320. package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
  321. package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
  322. package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
  323. package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
  324. package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
  325. package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
  326. package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
  327. package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
  328. package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
  329. package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
  330. package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
  331. package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
  332. package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
  333. package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
  334. package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
  335. package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
  336. package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
  337. package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
  338. package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
  339. package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
  340. package/examples/react/WorkerApp/src/env.d.ts +10 -0
  341. package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
  342. package/examples/react/WorkerApp/src/main.tsx +10 -0
  343. package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
  344. package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
  345. package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
  346. package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
  347. package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
  348. package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
  349. package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
  350. package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
  351. package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
  352. package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
  353. package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
  354. package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
  355. package/examples/react/WorkerApp/src/styles.css +756 -0
  356. package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
  357. package/examples/react/WorkerApp/src/types/message.ts +7 -0
  358. package/examples/react/WorkerApp/src/types/shift.ts +13 -0
  359. package/examples/react/WorkerApp/src/types/site.ts +8 -0
  360. package/examples/react/WorkerApp/src/types/worker.ts +8 -0
  361. package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
  362. package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
  363. package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
  364. package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
  365. package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
  366. package/examples/react/WorkerApp/tsconfig.json +22 -0
  367. package/examples/react/WorkerApp/vite.config.ts +18 -0
  368. package/package.json +11 -9
  369. package/src/Pending.test.ts +1 -2
  370. package/src/Sorting.test.ts +1 -1
  371. package/src/produceDraft.test.ts +3 -3
  372. package/src/react/components/CardList.test.tsx +1 -1
  373. package/src/react/components/DataTable.test.tsx +1 -1
  374. package/src/react/components/InfiniteScroll.test.tsx +5 -5
  375. package/dist/mvc-kit.cjs.map +0 -1
  376. package/dist/mvc-kit.js.map +0 -1
  377. package/dist/react-native.cjs.map +0 -1
  378. package/dist/react-native.js.map +0 -1
  379. package/dist/react.cjs.map +0 -1
  380. package/dist/react.js.map +0 -1
  381. package/dist/web.cjs.map +0 -1
  382. package/dist/web.js.map +0 -1
@@ -1,171 +1,144 @@
1
- import { isAbortError, classifyError } from "./errors.js";
2
1
  import { walkPrototypeChain } from "./walkPrototypeChain.js";
3
- const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
4
- const LOADING_TASK_STATE = Object.freeze({ loading: true, error: null, errorCode: null });
2
+ import { classifyError, isAbortError } from "./errors.js";
3
+ //#region src/wrapAsyncMethods.ts
4
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
5
+ var LOADING_TASK_STATE = Object.freeze({
6
+ loading: true,
7
+ error: null,
8
+ errorCode: null
9
+ });
10
+ /**
11
+ * Shared async method wrapping logic used by both ViewModel and Resource.
12
+ * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.
13
+ * Returns the list of wrapped method keys.
14
+ */
5
15
  function wrapAsyncMethods(ctx) {
6
- const {
7
- instance,
8
- stopPrototype,
9
- reservedKeys,
10
- lifecycleHooks,
11
- isDisposed,
12
- isInitialized,
13
- asyncStates,
14
- asyncSnapshots,
15
- asyncListeners,
16
- notifyAsync,
17
- addCleanup,
18
- ghostTimeout,
19
- className,
20
- activeOps
21
- } = ctx;
22
- if (__DEV__) {
23
- for (const key of reservedKeys) {
24
- if (Object.getOwnPropertyDescriptor(instance, key)?.value !== void 0) {
25
- let fromPrototype = false;
26
- let proto = Object.getPrototypeOf(instance);
27
- while (proto && proto !== Object.prototype) {
28
- const desc = Object.getOwnPropertyDescriptor(proto, key);
29
- if (desc && typeof desc.value === "function") {
30
- fromPrototype = true;
31
- break;
32
- }
33
- proto = Object.getPrototypeOf(proto);
34
- }
35
- if (!fromPrototype) {
36
- throw new Error(
37
- `[mvc-kit] "${key}" is a reserved property on ${className} and cannot be overridden.`
38
- );
39
- }
40
- }
41
- }
42
- }
43
- const methodEntries = ctx.methods ?? (() => {
44
- const result = [];
45
- const processed = /* @__PURE__ */ new Set();
46
- walkPrototypeChain(instance, stopPrototype, (key, desc) => {
47
- if (desc.get || desc.set) return;
48
- if (typeof desc.value !== "function") return;
49
- if (key.startsWith("_")) return;
50
- if (lifecycleHooks.has(key)) return;
51
- if (processed.has(key)) return;
52
- processed.add(key);
53
- result.push({ key, fn: desc.value });
54
- });
55
- return result;
56
- })();
57
- const wrappedKeys = [];
58
- for (const { key, fn: original } of methodEntries) {
59
- let pruned = false;
60
- const wrapper = function(...args) {
61
- if (isDisposed()) {
62
- if (__DEV__) {
63
- console.warn(`[mvc-kit] "${key}" called after dispose — ignored.`);
64
- }
65
- return void 0;
66
- }
67
- if (__DEV__ && !isInitialized()) {
68
- console.warn(
69
- `[mvc-kit] "${key}" called before init(). Async tracking is active only after init().`
70
- );
71
- }
72
- let result;
73
- try {
74
- result = original.apply(instance, args);
75
- } catch (e) {
76
- throw e;
77
- }
78
- if (!result || typeof result.then !== "function") {
79
- if (!pruned) {
80
- pruned = true;
81
- asyncStates.delete(key);
82
- asyncSnapshots.delete(key);
83
- instance[key] = original.bind(instance);
84
- }
85
- return result;
86
- }
87
- let internal = asyncStates.get(key);
88
- if (!internal) {
89
- internal = { loading: false, error: null, errorCode: null, count: 0 };
90
- asyncStates.set(key, internal);
91
- }
92
- internal.count++;
93
- internal.loading = true;
94
- internal.error = null;
95
- internal.errorCode = null;
96
- asyncSnapshots.set(key, LOADING_TASK_STATE);
97
- notifyAsync();
98
- if (__DEV__ && activeOps) {
99
- activeOps.set(key, (activeOps.get(key) ?? 0) + 1);
100
- }
101
- const finalizeOp = (errorMsg, errorCode) => {
102
- internal.count--;
103
- internal.loading = internal.count > 0;
104
- if (errorMsg !== void 0) {
105
- internal.error = errorMsg;
106
- internal.errorCode = errorCode ?? null;
107
- }
108
- asyncSnapshots.set(
109
- key,
110
- Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
111
- );
112
- notifyAsync();
113
- if (__DEV__ && activeOps) {
114
- const c = (activeOps.get(key) ?? 1) - 1;
115
- if (c <= 0) activeOps.delete(key);
116
- else activeOps.set(key, c);
117
- }
118
- };
119
- return result.then(
120
- (value) => {
121
- if (!isDisposed()) finalizeOp();
122
- return value;
123
- },
124
- (error) => {
125
- if (isAbortError(error)) {
126
- if (!isDisposed()) finalizeOp();
127
- return void 0;
128
- }
129
- if (isDisposed()) return void 0;
130
- const classified = classifyError(error);
131
- finalizeOp(classified.message, classified.code);
132
- throw error;
133
- }
134
- );
135
- };
136
- wrappedKeys.push(key);
137
- instance[key] = wrapper;
138
- }
139
- if (wrappedKeys.length > 0) {
140
- addCleanup(() => {
141
- const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;
142
- for (const k of wrappedKeys) {
143
- if (__DEV__) {
144
- instance[k] = () => {
145
- console.warn(`[mvc-kit] "${k}" called after dispose — ignored.`);
146
- return void 0;
147
- };
148
- } else {
149
- instance[k] = () => void 0;
150
- }
151
- }
152
- asyncListeners.clear();
153
- asyncStates.clear();
154
- asyncSnapshots.clear();
155
- if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {
156
- setTimeout(() => {
157
- for (const [key, count] of opsSnapshot) {
158
- console.warn(
159
- `[mvc-kit] Ghost async operation detected: "${key}" had ${count} pending call(s) when the ${className} was disposed. Consider using disposeSignal to cancel in-flight work.`
160
- );
161
- }
162
- }, ghostTimeout);
163
- }
164
- });
165
- }
166
- return wrappedKeys;
16
+ const { instance, stopPrototype, reservedKeys, lifecycleHooks, isDisposed, isInitialized, asyncStates, asyncSnapshots, asyncListeners, notifyAsync, addCleanup, ghostTimeout, className, activeOps } = ctx;
17
+ if (__DEV__) {
18
+ for (const key of reservedKeys) if (Object.getOwnPropertyDescriptor(instance, key)?.value !== void 0) {
19
+ let fromPrototype = false;
20
+ let proto = Object.getPrototypeOf(instance);
21
+ while (proto && proto !== Object.prototype) {
22
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
23
+ if (desc && typeof desc.value === "function") {
24
+ fromPrototype = true;
25
+ break;
26
+ }
27
+ proto = Object.getPrototypeOf(proto);
28
+ }
29
+ if (!fromPrototype) throw new Error(`[mvc-kit] "${key}" is a reserved property on ${className} and cannot be overridden.`);
30
+ }
31
+ }
32
+ const methodEntries = ctx.methods ?? (() => {
33
+ const result = [];
34
+ const processed = /* @__PURE__ */ new Set();
35
+ walkPrototypeChain(instance, stopPrototype, (key, desc) => {
36
+ if (desc.get || desc.set) return;
37
+ if (typeof desc.value !== "function") return;
38
+ if (key.startsWith("_")) return;
39
+ if (lifecycleHooks.has(key)) return;
40
+ if (processed.has(key)) return;
41
+ processed.add(key);
42
+ result.push({
43
+ key,
44
+ fn: desc.value
45
+ });
46
+ });
47
+ return result;
48
+ })();
49
+ const wrappedKeys = [];
50
+ for (const { key, fn: original } of methodEntries) {
51
+ let pruned = false;
52
+ const wrapper = function(...args) {
53
+ if (isDisposed()) {
54
+ if (__DEV__) console.warn(`[mvc-kit] "${key}" called after dispose — ignored.`);
55
+ return;
56
+ }
57
+ if (__DEV__ && !isInitialized()) console.warn(`[mvc-kit] "${key}" called before init(). Async tracking is active only after init().`);
58
+ let result;
59
+ try {
60
+ result = original.apply(instance, args);
61
+ } catch (e) {
62
+ throw e;
63
+ }
64
+ if (!result || typeof result.then !== "function") {
65
+ if (!pruned) {
66
+ pruned = true;
67
+ asyncStates.delete(key);
68
+ asyncSnapshots.delete(key);
69
+ instance[key] = original.bind(instance);
70
+ }
71
+ return result;
72
+ }
73
+ let internal = asyncStates.get(key);
74
+ if (!internal) {
75
+ internal = {
76
+ loading: false,
77
+ error: null,
78
+ errorCode: null,
79
+ count: 0
80
+ };
81
+ asyncStates.set(key, internal);
82
+ }
83
+ internal.count++;
84
+ internal.loading = true;
85
+ internal.error = null;
86
+ internal.errorCode = null;
87
+ asyncSnapshots.set(key, LOADING_TASK_STATE);
88
+ notifyAsync();
89
+ if (__DEV__ && activeOps) activeOps.set(key, (activeOps.get(key) ?? 0) + 1);
90
+ const finalizeOp = (errorMsg, errorCode) => {
91
+ internal.count--;
92
+ internal.loading = internal.count > 0;
93
+ if (errorMsg !== void 0) {
94
+ internal.error = errorMsg;
95
+ internal.errorCode = errorCode ?? null;
96
+ }
97
+ asyncSnapshots.set(key, Object.freeze({
98
+ loading: internal.loading,
99
+ error: internal.error,
100
+ errorCode: internal.errorCode
101
+ }));
102
+ notifyAsync();
103
+ if (__DEV__ && activeOps) {
104
+ const c = (activeOps.get(key) ?? 1) - 1;
105
+ if (c <= 0) activeOps.delete(key);
106
+ else activeOps.set(key, c);
107
+ }
108
+ };
109
+ return result.then((value) => {
110
+ if (!isDisposed()) finalizeOp();
111
+ return value;
112
+ }, (error) => {
113
+ if (isAbortError(error)) {
114
+ if (!isDisposed()) finalizeOp();
115
+ return;
116
+ }
117
+ if (isDisposed()) return void 0;
118
+ const classified = classifyError(error);
119
+ finalizeOp(classified.message, classified.code);
120
+ throw error;
121
+ });
122
+ };
123
+ wrappedKeys.push(key);
124
+ instance[key] = wrapper;
125
+ }
126
+ if (wrappedKeys.length > 0) addCleanup(() => {
127
+ const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;
128
+ for (const k of wrappedKeys) if (__DEV__) instance[k] = () => {
129
+ console.warn(`[mvc-kit] "${k}" called after dispose — ignored.`);
130
+ };
131
+ else instance[k] = () => void 0;
132
+ asyncListeners.clear();
133
+ asyncStates.clear();
134
+ asyncSnapshots.clear();
135
+ if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) setTimeout(() => {
136
+ for (const [key, count] of opsSnapshot) console.warn(`[mvc-kit] Ghost async operation detected: "${key}" had ${count} pending call(s) when the ${className} was disposed. Consider using disposeSignal to cancel in-flight work.`);
137
+ }, ghostTimeout);
138
+ });
139
+ return wrappedKeys;
167
140
  }
168
- export {
169
- wrapAsyncMethods
170
- };
171
- //# sourceMappingURL=wrapAsyncMethods.js.map
141
+ //#endregion
142
+ export { wrapAsyncMethods };
143
+
144
+ //# sourceMappingURL=wrapAsyncMethods.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrapAsyncMethods.js","sources":["../src/wrapAsyncMethods.ts"],"sourcesContent":["import { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nconst LOADING_TASK_STATE: TaskState = Object.freeze({ loading: true, error: null, errorCode: null });\n\n/** @internal Mutable internal async tracking state per method. */\nexport interface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\n/** @internal Configuration for the shared async method wrapping logic. */\nexport interface AsyncTrackingContext {\n instance: object;\n stopPrototype: object;\n reservedKeys: readonly string[];\n lifecycleHooks: Set<string>;\n isDisposed: () => boolean;\n isInitialized: () => boolean;\n asyncStates: Map<string, InternalTaskState>;\n asyncSnapshots: Map<string, TaskState>;\n asyncListeners: Set<() => void>;\n notifyAsync: () => void;\n addCleanup: (fn: () => void) => void;\n ghostTimeout: number;\n className: string;\n activeOps: Map<string, number> | null;\n /** Pre-scanned methods from class metadata cache. When provided, skips prototype walk. */\n methods?: Array<{ key: string; fn: Function }>;\n}\n\n/**\n * Shared async method wrapping logic used by both ViewModel and Resource.\n * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.\n * Returns the list of wrapped method keys.\n */\nexport function wrapAsyncMethods(ctx: AsyncTrackingContext): string[] {\n const {\n instance,\n stopPrototype,\n reservedKeys,\n lifecycleHooks,\n isDisposed,\n isInitialized,\n asyncStates,\n asyncSnapshots,\n asyncListeners,\n notifyAsync,\n addCleanup,\n ghostTimeout,\n className,\n activeOps,\n } = ctx;\n\n // Instance property reserved key check (DEV-only — prototype check in constructor catches most cases)\n // Skip own properties that are bound methods from bindPublicMethods (the key\n // also exists as a method on the prototype chain). Only reject class-field overrides.\n if (__DEV__) {\n for (const key of reservedKeys) {\n if (Object.getOwnPropertyDescriptor(instance, key)?.value !== undefined) {\n // Check if the prototype chain has a method with this name —\n // if so, the own property is from bindPublicMethods, not a user override\n let fromPrototype = false;\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc && typeof desc.value === 'function') { fromPrototype = true; break; }\n proto = Object.getPrototypeOf(proto);\n }\n if (!fromPrototype) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ${className} and cannot be overridden.`\n );\n }\n }\n }\n }\n\n // Use pre-scanned methods from class metadata cache, or walk prototype chain\n const methodEntries: Array<{ key: string; fn: Function }> = ctx.methods ?? (() => {\n const result: Array<{ key: string; fn: Function }> = [];\n const processed = new Set<string>();\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n if (desc.get || desc.set) return;\n if (typeof desc.value !== 'function') return;\n if (key.startsWith('_')) return;\n if (lifecycleHooks.has(key)) return;\n if (processed.has(key)) return;\n processed.add(key);\n result.push({ key, fn: desc.value });\n });\n return result;\n })();\n\n const wrappedKeys: string[] = [];\n\n for (const { key, fn: original } of methodEntries) {\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (isDisposed()) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !isInitialized()) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(instance, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n asyncStates.delete(key);\n asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (instance as any)[key] = original.bind(instance);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n asyncSnapshots.set(key, LOADING_TASK_STATE);\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n activeOps.set(key, (activeOps.get(key) ?? 0) + 1);\n }\n\n // Shared bookkeeping: decrement count, snapshot state, update DEV active ops\n const finalizeOp = (errorMsg?: string | null, errorCode?: TaskState['errorCode']) => {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n if (errorMsg !== undefined) {\n internal!.error = errorMsg;\n internal!.errorCode = errorCode ?? null;\n }\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n };\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (!isDisposed()) finalizeOp();\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!isDisposed()) finalizeOp();\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (isDisposed()) return undefined;\n\n const classified = classifyError(error);\n finalizeOp(classified.message, classified.code);\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (instance as any)[key] = wrapper;\n }\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (instance as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (instance as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n asyncListeners.clear();\n asyncStates.clear();\n asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ${className} was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, ghostTimeout);\n }\n });\n }\n\n return wrappedKeys;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wrapAsyncMethods.js","names":[],"sources":["../src/wrapAsyncMethods.ts"],"sourcesContent":["import { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nconst LOADING_TASK_STATE: TaskState = Object.freeze({ loading: true, error: null, errorCode: null });\n\n/** @internal Mutable internal async tracking state per method. */\nexport interface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\n/** @internal Configuration for the shared async method wrapping logic. */\nexport interface AsyncTrackingContext {\n instance: object;\n stopPrototype: object;\n reservedKeys: readonly string[];\n lifecycleHooks: Set<string>;\n isDisposed: () => boolean;\n isInitialized: () => boolean;\n asyncStates: Map<string, InternalTaskState>;\n asyncSnapshots: Map<string, TaskState>;\n asyncListeners: Set<() => void>;\n notifyAsync: () => void;\n addCleanup: (fn: () => void) => void;\n ghostTimeout: number;\n className: string;\n activeOps: Map<string, number> | null;\n /** Pre-scanned methods from class metadata cache. When provided, skips prototype walk. */\n methods?: Array<{ key: string; fn: Function }>;\n}\n\n/**\n * Shared async method wrapping logic used by both ViewModel and Resource.\n * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.\n * Returns the list of wrapped method keys.\n */\nexport function wrapAsyncMethods(ctx: AsyncTrackingContext): string[] {\n const {\n instance,\n stopPrototype,\n reservedKeys,\n lifecycleHooks,\n isDisposed,\n isInitialized,\n asyncStates,\n asyncSnapshots,\n asyncListeners,\n notifyAsync,\n addCleanup,\n ghostTimeout,\n className,\n activeOps,\n } = ctx;\n\n // Instance property reserved key check (DEV-only — prototype check in constructor catches most cases)\n // Skip own properties that are bound methods from bindPublicMethods (the key\n // also exists as a method on the prototype chain). Only reject class-field overrides.\n if (__DEV__) {\n for (const key of reservedKeys) {\n if (Object.getOwnPropertyDescriptor(instance, key)?.value !== undefined) {\n // Check if the prototype chain has a method with this name —\n // if so, the own property is from bindPublicMethods, not a user override\n let fromPrototype = false;\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc && typeof desc.value === 'function') { fromPrototype = true; break; }\n proto = Object.getPrototypeOf(proto);\n }\n if (!fromPrototype) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ${className} and cannot be overridden.`\n );\n }\n }\n }\n }\n\n // Use pre-scanned methods from class metadata cache, or walk prototype chain\n const methodEntries: Array<{ key: string; fn: Function }> = ctx.methods ?? (() => {\n const result: Array<{ key: string; fn: Function }> = [];\n const processed = new Set<string>();\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n if (desc.get || desc.set) return;\n if (typeof desc.value !== 'function') return;\n if (key.startsWith('_')) return;\n if (lifecycleHooks.has(key)) return;\n if (processed.has(key)) return;\n processed.add(key);\n result.push({ key, fn: desc.value });\n });\n return result;\n })();\n\n const wrappedKeys: string[] = [];\n\n for (const { key, fn: original } of methodEntries) {\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (isDisposed()) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !isInitialized()) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(instance, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n asyncStates.delete(key);\n asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (instance as any)[key] = original.bind(instance);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n asyncSnapshots.set(key, LOADING_TASK_STATE);\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n activeOps.set(key, (activeOps.get(key) ?? 0) + 1);\n }\n\n // Shared bookkeeping: decrement count, snapshot state, update DEV active ops\n const finalizeOp = (errorMsg?: string | null, errorCode?: TaskState['errorCode']) => {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n if (errorMsg !== undefined) {\n internal!.error = errorMsg;\n internal!.errorCode = errorCode ?? null;\n }\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n };\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (!isDisposed()) finalizeOp();\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!isDisposed()) finalizeOp();\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (isDisposed()) return undefined;\n\n const classified = classifyError(error);\n finalizeOp(classified.message, classified.code);\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (instance as any)[key] = wrapper;\n }\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (instance as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (instance as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n asyncListeners.clear();\n asyncStates.clear();\n asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ${className} was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, ghostTimeout);\n }\n });\n }\n\n return wrappedKeys;\n}\n"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ import { Channel, singleton, teardownAll } from 'mvc-kit';
2
+ import type { ChannelStatus } from 'mvc-kit';
3
+
4
+ // Channel: Persistent connection with auto-reconnect and typed messages
5
+ //
6
+ // Extend Channel<MessageMap> and implement two abstract methods:
7
+ // open(signal) — establish the connection (WebSocket, SSE, etc.)
8
+ // close() — tear down the transport
9
+ //
10
+ // The framework handles connection status, reconnect with exponential
11
+ // backoff, message routing, and lifecycle management.
12
+
13
+ // --- Message type map ---
14
+
15
+ interface ChatMessages {
16
+ message: { userId: string; text: string };
17
+ typing: { userId: string };
18
+ presence: { online: string[] };
19
+ }
20
+
21
+ // --- Channel subclass ---
22
+
23
+ class ChatChannel extends Channel<ChatMessages> {
24
+ // Tune reconnect behavior via static overrides
25
+ static override RECONNECT_BASE = 1000; // initial backoff (ms)
26
+ static override RECONNECT_MAX = 30000; // max backoff cap (ms)
27
+ static override MAX_ATTEMPTS = 5; // give up after 5 attempts
28
+
29
+ private ws: { close(): void } | null = null;
30
+
31
+ // open() is called by the framework when connect() is invoked.
32
+ // The signal aborts on disconnect() or dispose().
33
+ protected open(signal: AbortSignal): void {
34
+ // In production, this would be:
35
+ // this.ws = new WebSocket('wss://chat.example.com');
36
+ // this.ws.onmessage = (e) => {
37
+ // const { type, payload } = JSON.parse(e.data);
38
+ // this.receive(type, payload);
39
+ // };
40
+
41
+ // Simulated connection
42
+ console.log('Connecting...');
43
+ const connectTimer = setTimeout(() => {
44
+ if (!signal.aborted) {
45
+ console.log('Connected!');
46
+ // Simulate incoming messages
47
+ this.receive('presence', { online: ['alice', 'bob'] });
48
+ this.receive('message', { userId: 'alice', text: 'Hello!' });
49
+ }
50
+ }, 100);
51
+
52
+ this.ws = {
53
+ close: () => clearTimeout(connectTimer),
54
+ };
55
+
56
+ // Always clean up when the signal aborts (disconnect or dispose)
57
+ signal.addEventListener('abort', () => this.ws?.close());
58
+ }
59
+
60
+ // close() tears down the transport. Must not throw.
61
+ protected close(): void {
62
+ this.ws?.close();
63
+ this.ws = null;
64
+ }
65
+ }
66
+
67
+ // --- Usage ---
68
+
69
+ const chat = singleton(ChatChannel);
70
+ chat.init();
71
+
72
+ // Subscribe to connection status changes
73
+ chat.subscribe((next: ChannelStatus, prev: ChannelStatus) => {
74
+ console.log(`Status: connected=${next.connected}, reconnecting=${next.reconnecting}`);
75
+ if (next.error) console.log(`Error: ${next.error}`);
76
+ });
77
+
78
+ // Subscribe to typed messages
79
+ const unsubMessage = chat.on('message', ({ userId, text }) => {
80
+ console.log(`${userId}: ${text}`);
81
+ });
82
+
83
+ // One-time subscription — auto-unsubscribes after first event
84
+ chat.once('presence', ({ online }) => {
85
+ console.log('Initial presence:', online);
86
+ });
87
+
88
+ // Initiate connection
89
+ chat.connect();
90
+
91
+ // Read current status at any time
92
+ console.log('Connected:', chat.state.connected);
93
+ console.log('Reconnecting:', chat.state.reconnecting);
94
+ console.log('Attempt:', chat.state.attempt);
95
+
96
+ // Manually disconnect (cancels pending reconnect, resets status)
97
+ // chat.disconnect();
98
+
99
+ // Reconnect after disconnect
100
+ // chat.connect();
101
+
102
+ // Unsubscribe from a message type
103
+ unsubMessage();
104
+
105
+ // Cleanup — cancels timers, aborts signals, calls close()
106
+ setTimeout(() => {
107
+ chat.dispose();
108
+ teardownAll();
109
+ }, 500);
@@ -0,0 +1,118 @@
1
+ import { Collection } from 'mvc-kit';
2
+
3
+ // Collection: Reactive typed array with CRUD and query methods
4
+
5
+ interface Todo {
6
+ id: string;
7
+ text: string;
8
+ done: boolean;
9
+ priority: 'low' | 'medium' | 'high';
10
+ }
11
+
12
+ // --- Basic usage ---
13
+
14
+ const todos = new Collection<Todo>();
15
+
16
+ // Subscribe to changes
17
+ todos.subscribe((items, prev) => {
18
+ console.log(`Collection changed: ${prev.length} → ${items.length} items`);
19
+ });
20
+
21
+ // CRUD operations (trigger notifications)
22
+
23
+ // Add items
24
+ todos.add(
25
+ { id: '1', text: 'Learn mvc-kit', done: false, priority: 'high' },
26
+ { id: '2', text: 'Build an app', done: false, priority: 'medium' },
27
+ { id: '3', text: 'Write tests', done: true, priority: 'low' }
28
+ );
29
+
30
+ console.log('Length:', todos.length); // 3
31
+ console.log('Items:', todos.items);
32
+
33
+ // Update an item
34
+ todos.update('1', { done: true });
35
+ console.log('After update:', todos.get('1')?.done); // true
36
+
37
+ // Remove an item
38
+ todos.remove('3');
39
+ console.log('After remove length:', todos.length); // 2
40
+
41
+ // Query operations (pure, no notifications)
42
+
43
+ // Get by id (O(1) lookup)
44
+ const todo = todos.get('1');
45
+ console.log('Get by id:', todo?.text);
46
+
47
+ // Check existence
48
+ console.log('Has id 1:', todos.has('1')); // true
49
+ console.log('Has id 99:', todos.has('99')); // false
50
+
51
+ // Find first match
52
+ const firstIncomplete = todos.find(t => !t.done);
53
+ console.log('First incomplete:', firstIncomplete?.text);
54
+
55
+ // Filter
56
+ const highPriority = todos.filter(t => t.priority === 'high');
57
+ console.log('High priority count:', highPriority.length);
58
+
59
+ // Sort (returns new array, doesn't mutate)
60
+ const sorted = todos.sorted((a, b) => a.text.localeCompare(b.text));
61
+ console.log('Sorted:', sorted.map(t => t.text));
62
+
63
+ // Map
64
+ const texts = todos.map(t => t.text);
65
+ console.log('Texts:', texts);
66
+
67
+ // Reset - replace all items
68
+ todos.reset([
69
+ { id: 'a', text: 'New todo', done: false, priority: 'medium' }
70
+ ]);
71
+ console.log('After reset length:', todos.length); // 1
72
+
73
+ // Clear all items
74
+ todos.clear();
75
+ console.log('After clear length:', todos.length); // 0
76
+
77
+ // --- Upsert: add-or-replace by ID ---
78
+
79
+ todos.reset([
80
+ { id: '1', text: 'Learn mvc-kit', done: false, priority: 'high' },
81
+ { id: '2', text: 'Build an app', done: false, priority: 'medium' },
82
+ ]);
83
+
84
+ // Upsert replaces existing items in-place and appends new ones
85
+ todos.upsert(
86
+ { id: '2', text: 'Build an app', done: true, priority: 'medium' }, // replaces in position
87
+ { id: '3', text: 'Ship it', done: false, priority: 'high' }, // appended
88
+ );
89
+ console.log('After upsert length:', todos.length); // 3
90
+ console.log('Item 2 done:', todos.get('2')?.done); // true (replaced)
91
+ console.log('Item 3 text:', todos.get('3')?.text); // 'Ship it' (new)
92
+
93
+ // --- Optimistic updates ---
94
+
95
+ // Reset with some data for the optimistic demo
96
+ todos.reset([
97
+ { id: '1', text: 'Learn mvc-kit', done: false, priority: 'high' },
98
+ { id: '2', text: 'Build an app', done: false, priority: 'medium' },
99
+ ]);
100
+
101
+ // Snapshot current state, apply mutations, get a rollback function
102
+ const rollback = todos.optimistic(() => {
103
+ todos.update('1', { done: true });
104
+ todos.remove('2');
105
+ });
106
+
107
+ console.log('After optimistic:', todos.length); // 1
108
+ console.log('Item 1 done:', todos.get('1')?.done); // true
109
+
110
+ // If the server call fails, rollback restores pre-optimistic state
111
+ rollback();
112
+
113
+ console.log('After rollback:', todos.length); // 2
114
+ console.log('Item 1 done:', todos.get('1')?.done); // false
115
+ console.log('Item 2 exists:', todos.has('2')); // true
116
+
117
+ // Cleanup
118
+ todos.dispose();