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,507 +1,427 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const bindPublicMethods = require("./bindPublicMethods.cjs");
4
- const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
5
- const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup"]);
1
+ const require_bindPublicMethods = require("./bindPublicMethods.cjs");
2
+ //#region src/Collection.ts
3
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
4
+ var PROTECTED_KEYS = new Set(["addCleanup"]);
6
5
  function freeze(obj) {
7
- return __DEV__ ? Object.freeze(obj) : obj;
8
- }
9
- class Collection {
10
- /** Maximum number of items before FIFO eviction. 0 = unlimited. */
11
- static MAX_SIZE = 0;
12
- /** Time-to-live in milliseconds. 0 = no expiry. */
13
- static TTL = 0;
14
- _items = [];
15
- _disposed = false;
16
- _listeners = /* @__PURE__ */ new Set();
17
- _index = /* @__PURE__ */ new Map();
18
- _abortController = null;
19
- _cleanups = null;
20
- _timestamps = null;
21
- _evictionTimer = null;
22
- constructor(initialItems = []) {
23
- let result = [...initialItems];
24
- if (this._ttl > 0) {
25
- this._timestamps = /* @__PURE__ */ new Map();
26
- const now = Date.now();
27
- for (const item of result) {
28
- this._timestamps.set(item.id, now);
29
- }
30
- }
31
- if (this._maxSize > 0 && result.length > this._maxSize) {
32
- const excess = result.length - this._maxSize;
33
- const evicted = result.slice(0, excess);
34
- result = result.slice(excess);
35
- for (const item of evicted) {
36
- this._timestamps?.delete(item.id);
37
- }
38
- }
39
- this._items = freeze(result);
40
- this._rebuildIndex();
41
- this._scheduleEvictionTimer();
42
- bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
43
- }
44
- /**
45
- * Alias for Subscribable compatibility.
46
- */
47
- get state() {
48
- return this._items;
49
- }
50
- /** The raw array of items. */
51
- get items() {
52
- return this._items;
53
- }
54
- /** Number of items in the collection. */
55
- get length() {
56
- return this._items.length;
57
- }
58
- /** Whether this instance has been disposed. */
59
- get disposed() {
60
- return this._disposed;
61
- }
62
- /** AbortSignal that fires when this instance is disposed. Lazily created. */
63
- get disposeSignal() {
64
- if (!this._abortController) {
65
- this._abortController = new AbortController();
66
- }
67
- return this._abortController.signal;
68
- }
69
- // ── Config Accessors ──
70
- get _maxSize() {
71
- return this.constructor.MAX_SIZE;
72
- }
73
- get _ttl() {
74
- return this.constructor.TTL;
75
- }
76
- // ── CRUD Methods (notify listeners) ──
77
- /**
78
- * Add one or more items. Items with existing IDs are silently skipped.
79
- */
80
- add(...items) {
81
- if (this._disposed) {
82
- throw new Error("Cannot add to disposed Collection");
83
- }
84
- if (items.length === 0) {
85
- return;
86
- }
87
- if (items.length === 1) {
88
- const item = items[0];
89
- if (this._index.has(item.id)) return;
90
- const prev2 = this._items;
91
- let result2 = [...prev2, item];
92
- this._index.set(item.id, item);
93
- if (this._timestamps) this._timestamps.set(item.id, Date.now());
94
- if (this._maxSize > 0 && result2.length > this._maxSize) {
95
- result2 = this._evictForCapacity(result2);
96
- }
97
- this._items = freeze(result2);
98
- this._notify(prev2);
99
- this._scheduleEvictionTimer();
100
- return;
101
- }
102
- const seen = /* @__PURE__ */ new Set();
103
- const newItems = [];
104
- for (const item of items) {
105
- if (!this._index.has(item.id) && !seen.has(item.id)) {
106
- newItems.push(item);
107
- seen.add(item.id);
108
- }
109
- }
110
- if (newItems.length === 0) return;
111
- const prev = this._items;
112
- let result = [...prev, ...newItems];
113
- for (const item of newItems) {
114
- this._index.set(item.id, item);
115
- }
116
- if (this._timestamps) {
117
- const now = Date.now();
118
- for (const item of newItems) {
119
- this._timestamps.set(item.id, now);
120
- }
121
- }
122
- if (this._maxSize > 0 && result.length > this._maxSize) {
123
- result = this._evictForCapacity(result);
124
- }
125
- this._items = freeze(result);
126
- this._notify(prev);
127
- this._scheduleEvictionTimer();
128
- }
129
- /**
130
- * Add or replace items by ID. Existing items are replaced in-place
131
- * (preserving array position); new items are appended. Deduplicates
132
- * input — last occurrence wins. No-op if nothing changed (reference
133
- * comparison).
134
- */
135
- upsert(...items) {
136
- if (this._disposed) {
137
- throw new Error("Cannot upsert on disposed Collection");
138
- }
139
- if (items.length === 0) return;
140
- if (items.length === 1) {
141
- const item = items[0];
142
- const existing = this._index.get(item.id);
143
- if (existing) {
144
- if (existing === item) return;
145
- const prev2 = this._items;
146
- const idx = this._items.indexOf(existing);
147
- const newItems = [...prev2];
148
- newItems[idx] = item;
149
- this._index.set(item.id, item);
150
- if (this._timestamps) this._timestamps.set(item.id, Date.now());
151
- this._items = freeze(newItems);
152
- this._notify(prev2);
153
- } else {
154
- const prev2 = this._items;
155
- let result2 = [...prev2, item];
156
- this._index.set(item.id, item);
157
- if (this._timestamps) this._timestamps.set(item.id, Date.now());
158
- if (this._maxSize > 0 && result2.length > this._maxSize) {
159
- result2 = this._evictForCapacity(result2);
160
- }
161
- this._items = freeze(result2);
162
- this._notify(prev2);
163
- this._scheduleEvictionTimer();
164
- }
165
- return;
166
- }
167
- const incoming = /* @__PURE__ */ new Map();
168
- for (const item of items) {
169
- incoming.set(item.id, item);
170
- }
171
- const prev = this._items;
172
- let changed = false;
173
- const replaced = /* @__PURE__ */ new Set();
174
- const newArray = [];
175
- for (const existing of prev) {
176
- if (incoming.has(existing.id)) {
177
- const replacement = incoming.get(existing.id);
178
- if (replacement !== existing) changed = true;
179
- newArray.push(replacement);
180
- replaced.add(existing.id);
181
- } else {
182
- newArray.push(existing);
183
- }
184
- }
185
- for (const [id, item] of incoming) {
186
- if (!replaced.has(id)) {
187
- newArray.push(item);
188
- changed = true;
189
- }
190
- }
191
- if (!changed) return;
192
- if (this._timestamps) {
193
- const now = Date.now();
194
- for (const [id] of incoming) {
195
- this._timestamps.set(id, now);
196
- }
197
- }
198
- for (const [id, item] of incoming) {
199
- this._index.set(id, item);
200
- }
201
- let result = newArray;
202
- if (this._maxSize > 0 && result.length > this._maxSize) {
203
- result = this._evictForCapacity(result);
204
- }
205
- this._items = freeze(result);
206
- this._notify(prev);
207
- this._scheduleEvictionTimer();
208
- }
209
- /**
210
- * Remove items by id(s).
211
- */
212
- remove(...ids) {
213
- if (this._disposed) {
214
- throw new Error("Cannot remove from disposed Collection");
215
- }
216
- if (ids.length === 0) {
217
- return;
218
- }
219
- if (ids.length === 1) {
220
- const id = ids[0];
221
- if (!this._index.has(id)) return;
222
- const prev2 = this._items;
223
- this._items = freeze(prev2.filter((item) => item.id !== id));
224
- this._index.delete(id);
225
- this._timestamps?.delete(id);
226
- this._notify(prev2);
227
- this._scheduleEvictionTimer();
228
- return;
229
- }
230
- const idSet = new Set(ids);
231
- const filtered = this._items.filter((item) => !idSet.has(item.id));
232
- if (filtered.length === this._items.length) {
233
- return;
234
- }
235
- const prev = this._items;
236
- this._items = freeze(filtered);
237
- for (const id of ids) {
238
- this._index.delete(id);
239
- this._timestamps?.delete(id);
240
- }
241
- this._notify(prev);
242
- this._scheduleEvictionTimer();
243
- }
244
- /**
245
- * Update an item by id with partial changes.
246
- */
247
- update(id, changes) {
248
- if (this._disposed) {
249
- throw new Error("Cannot update disposed Collection");
250
- }
251
- const existing = this._index.get(id);
252
- if (!existing) return;
253
- const keys = Object.keys(changes);
254
- const hasChanges = keys.some((key) => changes[key] !== existing[key]);
255
- if (!hasChanges) return;
256
- const updated = { ...existing, ...changes, id };
257
- const prev = this._items;
258
- const idx = this._items.indexOf(existing);
259
- const newItems = [...prev];
260
- newItems[idx] = updated;
261
- this._items = freeze(newItems);
262
- this._index.set(id, updated);
263
- this._notify(prev);
264
- }
265
- /**
266
- * Replace all items.
267
- */
268
- reset(items) {
269
- if (this._disposed) {
270
- throw new Error("Cannot reset disposed Collection");
271
- }
272
- const prev = this._items;
273
- if (this._timestamps) {
274
- this._timestamps.clear();
275
- const now = Date.now();
276
- for (const item of items) {
277
- this._timestamps.set(item.id, now);
278
- }
279
- }
280
- let result = [...items];
281
- if (this._maxSize > 0 && result.length > this._maxSize) {
282
- result = this._evictForCapacity(result);
283
- }
284
- this._items = freeze(result);
285
- this._rebuildIndex();
286
- this._notify(prev);
287
- this._scheduleEvictionTimer();
288
- }
289
- /**
290
- * Remove all items.
291
- */
292
- clear() {
293
- if (this._disposed) {
294
- throw new Error("Cannot clear disposed Collection");
295
- }
296
- if (this._items.length === 0) {
297
- return;
298
- }
299
- const prev = this._items;
300
- this._items = freeze([]);
301
- this._index.clear();
302
- this._timestamps?.clear();
303
- this._clearEvictionTimer();
304
- this._notify(prev);
305
- }
306
- /**
307
- * Snapshot current state, apply callback mutations, and return a rollback function.
308
- * Rollback restores items to pre-callback state regardless of later mutations.
309
- */
310
- optimistic(callback) {
311
- if (this._disposed) {
312
- throw new Error("Cannot perform optimistic update on disposed Collection");
313
- }
314
- const snapshot = this._items;
315
- const timestampSnapshot = this._timestamps ? new Map(this._timestamps) : null;
316
- callback();
317
- let rolledBack = false;
318
- return () => {
319
- if (rolledBack || this._disposed) return;
320
- rolledBack = true;
321
- const prev = this._items;
322
- this._items = snapshot;
323
- if (timestampSnapshot) {
324
- this._timestamps = timestampSnapshot;
325
- }
326
- this._rebuildIndex();
327
- this._notify(prev);
328
- this._scheduleEvictionTimer();
329
- };
330
- }
331
- // ── Query Methods (pure, no notification) ──
332
- /**
333
- * Get item by id.
334
- */
335
- get(id) {
336
- return this._index.get(id);
337
- }
338
- /**
339
- * Check if item exists by id.
340
- */
341
- has(id) {
342
- return this._index.has(id);
343
- }
344
- /**
345
- * Find first item matching predicate.
346
- */
347
- find(predicate) {
348
- return this._items.find(predicate);
349
- }
350
- /**
351
- * Filter items matching predicate.
352
- */
353
- filter(predicate) {
354
- return this._items.filter(predicate);
355
- }
356
- /**
357
- * Return sorted copy.
358
- */
359
- sorted(compareFn) {
360
- return [...this._items].sort(compareFn);
361
- }
362
- /**
363
- * Map items to new array.
364
- */
365
- map(fn) {
366
- return this._items.map(fn);
367
- }
368
- // ── Subscribable interface ──
369
- /** Subscribes to state changes. Returns an unsubscribe function. */
370
- subscribe(listener) {
371
- if (this._disposed) {
372
- return () => {
373
- };
374
- }
375
- this._listeners.add(listener);
376
- return () => {
377
- this._listeners.delete(listener);
378
- };
379
- }
380
- /** Tears down the instance, releasing all subscriptions and resources. */
381
- dispose() {
382
- if (this._disposed) {
383
- return;
384
- }
385
- this._disposed = true;
386
- this._clearEvictionTimer();
387
- this._abortController?.abort();
388
- if (this._cleanups) {
389
- for (const fn of this._cleanups) fn();
390
- this._cleanups = null;
391
- }
392
- this.onDispose?.();
393
- this._listeners.clear();
394
- this._index.clear();
395
- this._timestamps?.clear();
396
- }
397
- /** Registers a cleanup function to be called on dispose. @protected */
398
- addCleanup(fn) {
399
- if (!this._cleanups) {
400
- this._cleanups = [];
401
- }
402
- this._cleanups.push(fn);
403
- }
404
- _notify(prev) {
405
- for (const listener of this._listeners) {
406
- listener(this._items, prev);
407
- }
408
- }
409
- _rebuildIndex() {
410
- this._index.clear();
411
- for (const item of this._items) {
412
- this._index.set(item.id, item);
413
- }
414
- }
415
- // ── Eviction Internals ──
416
- _evictForCapacity(items) {
417
- const excess = items.length - this._maxSize;
418
- if (excess <= 0) return items;
419
- const candidates = items.slice(0, excess);
420
- const toEvict = this._applyOnEvict(candidates, "capacity");
421
- if (toEvict === false) return items;
422
- if (toEvict.length === 0) return items;
423
- const evictIds = new Set(toEvict.map((item) => item.id));
424
- const result = items.filter((item) => !evictIds.has(item.id));
425
- for (const item of toEvict) {
426
- this._index.delete(item.id);
427
- this._timestamps?.delete(item.id);
428
- }
429
- return result;
430
- }
431
- _applyOnEvict(candidates, reason) {
432
- if (!this.onEvict) return candidates;
433
- const result = this.onEvict(candidates, reason);
434
- if (result === false) {
435
- if (__DEV__ && reason === "capacity" && this._maxSize > 0) {
436
- const currentSize = this._items.length + candidates.length;
437
- if (currentSize > this._maxSize * 2) {
438
- console.warn(
439
- `[mvc-kit] Collection exceeded 2x MAX_SIZE (${currentSize}/${this._maxSize}). onEvict is vetoing eviction — this may cause unbounded growth.`
440
- );
441
- }
442
- }
443
- return false;
444
- }
445
- if (Array.isArray(result)) {
446
- const candidateIds = new Set(candidates.map((c) => c.id));
447
- return result.filter((item) => candidateIds.has(item.id));
448
- }
449
- return candidates;
450
- }
451
- _sweepExpired() {
452
- if (this._disposed || !this._timestamps || this._ttl <= 0) return;
453
- const now = Date.now();
454
- const ttl = this._ttl;
455
- const expired = [];
456
- for (const item of this._items) {
457
- const ts = this._timestamps.get(item.id);
458
- if (ts !== void 0 && now - ts >= ttl) {
459
- expired.push(item);
460
- }
461
- }
462
- if (expired.length === 0) {
463
- this._scheduleEvictionTimer();
464
- return;
465
- }
466
- const toEvict = this._applyOnEvict(expired, "ttl");
467
- if (toEvict === false) {
468
- this._scheduleEvictionTimer();
469
- return;
470
- }
471
- if (toEvict.length === 0) {
472
- this._scheduleEvictionTimer();
473
- return;
474
- }
475
- const evictIds = new Set(toEvict.map((item) => item.id));
476
- const prev = this._items;
477
- this._items = freeze(
478
- prev.filter((item) => !evictIds.has(item.id))
479
- );
480
- for (const item of toEvict) {
481
- this._index.delete(item.id);
482
- this._timestamps.delete(item.id);
483
- }
484
- this._notify(prev);
485
- this._scheduleEvictionTimer();
486
- }
487
- _scheduleEvictionTimer() {
488
- this._clearEvictionTimer();
489
- if (this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;
490
- const now = Date.now();
491
- const ttl = this._ttl;
492
- let earliest = Infinity;
493
- for (const ts of this._timestamps.values()) {
494
- if (ts < earliest) earliest = ts;
495
- }
496
- const delay = Math.max(0, earliest + ttl - now);
497
- this._evictionTimer = setTimeout(() => this._sweepExpired(), delay);
498
- }
499
- _clearEvictionTimer() {
500
- if (this._evictionTimer !== null) {
501
- clearTimeout(this._evictionTimer);
502
- this._evictionTimer = null;
503
- }
504
- }
6
+ return __DEV__ ? Object.freeze(obj) : obj;
505
7
  }
8
+ /**
9
+ * Reactive typed array with CRUD and query methods.
10
+ */
11
+ var Collection = class {
12
+ /** Maximum number of items before FIFO eviction. 0 = unlimited. */
13
+ static MAX_SIZE = 0;
14
+ /** Time-to-live in milliseconds. 0 = no expiry. */
15
+ static TTL = 0;
16
+ _items = [];
17
+ _disposed = false;
18
+ _listeners = /* @__PURE__ */ new Set();
19
+ _index = /* @__PURE__ */ new Map();
20
+ _abortController = null;
21
+ _cleanups = null;
22
+ _timestamps = null;
23
+ _evictionTimer = null;
24
+ constructor(initialItems = []) {
25
+ let result = [...initialItems];
26
+ if (this._ttl > 0) {
27
+ this._timestamps = /* @__PURE__ */ new Map();
28
+ const now = Date.now();
29
+ for (const item of result) this._timestamps.set(item.id, now);
30
+ }
31
+ if (this._maxSize > 0 && result.length > this._maxSize) {
32
+ const excess = result.length - this._maxSize;
33
+ const evicted = result.slice(0, excess);
34
+ result = result.slice(excess);
35
+ for (const item of evicted) this._timestamps?.delete(item.id);
36
+ }
37
+ this._items = freeze(result);
38
+ this._rebuildIndex();
39
+ this._scheduleEvictionTimer();
40
+ require_bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
41
+ }
42
+ /**
43
+ * Alias for Subscribable compatibility.
44
+ */
45
+ get state() {
46
+ return this._items;
47
+ }
48
+ /** The raw array of items. */
49
+ get items() {
50
+ return this._items;
51
+ }
52
+ /** Number of items in the collection. */
53
+ get length() {
54
+ return this._items.length;
55
+ }
56
+ /** Whether this instance has been disposed. */
57
+ get disposed() {
58
+ return this._disposed;
59
+ }
60
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
61
+ get disposeSignal() {
62
+ if (!this._abortController) this._abortController = new AbortController();
63
+ return this._abortController.signal;
64
+ }
65
+ get _maxSize() {
66
+ return this.constructor.MAX_SIZE;
67
+ }
68
+ get _ttl() {
69
+ return this.constructor.TTL;
70
+ }
71
+ /**
72
+ * Add one or more items. Items with existing IDs are silently skipped.
73
+ */
74
+ add(...items) {
75
+ if (this._disposed) throw new Error("Cannot add to disposed Collection");
76
+ if (items.length === 0) return;
77
+ if (items.length === 1) {
78
+ const item = items[0];
79
+ if (this._index.has(item.id)) return;
80
+ const prev = this._items;
81
+ let result = [...prev, item];
82
+ this._index.set(item.id, item);
83
+ if (this._timestamps) this._timestamps.set(item.id, Date.now());
84
+ if (this._maxSize > 0 && result.length > this._maxSize) result = this._evictForCapacity(result);
85
+ this._items = freeze(result);
86
+ this._notify(prev);
87
+ this._scheduleEvictionTimer();
88
+ return;
89
+ }
90
+ const seen = /* @__PURE__ */ new Set();
91
+ const newItems = [];
92
+ for (const item of items) if (!this._index.has(item.id) && !seen.has(item.id)) {
93
+ newItems.push(item);
94
+ seen.add(item.id);
95
+ }
96
+ if (newItems.length === 0) return;
97
+ const prev = this._items;
98
+ let result = [...prev, ...newItems];
99
+ for (const item of newItems) this._index.set(item.id, item);
100
+ if (this._timestamps) {
101
+ const now = Date.now();
102
+ for (const item of newItems) this._timestamps.set(item.id, now);
103
+ }
104
+ if (this._maxSize > 0 && result.length > this._maxSize) result = this._evictForCapacity(result);
105
+ this._items = freeze(result);
106
+ this._notify(prev);
107
+ this._scheduleEvictionTimer();
108
+ }
109
+ /**
110
+ * Add or replace items by ID. Existing items are replaced in-place
111
+ * (preserving array position); new items are appended. Deduplicates
112
+ * input — last occurrence wins. No-op if nothing changed (reference
113
+ * comparison).
114
+ */
115
+ upsert(...items) {
116
+ if (this._disposed) throw new Error("Cannot upsert on disposed Collection");
117
+ if (items.length === 0) return;
118
+ if (items.length === 1) {
119
+ const item = items[0];
120
+ const existing = this._index.get(item.id);
121
+ if (existing) {
122
+ if (existing === item) return;
123
+ const prev = this._items;
124
+ const idx = this._items.indexOf(existing);
125
+ const newItems = [...prev];
126
+ newItems[idx] = item;
127
+ this._index.set(item.id, item);
128
+ if (this._timestamps) this._timestamps.set(item.id, Date.now());
129
+ this._items = freeze(newItems);
130
+ this._notify(prev);
131
+ } else {
132
+ const prev = this._items;
133
+ let result = [...prev, item];
134
+ this._index.set(item.id, item);
135
+ if (this._timestamps) this._timestamps.set(item.id, Date.now());
136
+ if (this._maxSize > 0 && result.length > this._maxSize) result = this._evictForCapacity(result);
137
+ this._items = freeze(result);
138
+ this._notify(prev);
139
+ this._scheduleEvictionTimer();
140
+ }
141
+ return;
142
+ }
143
+ const incoming = /* @__PURE__ */ new Map();
144
+ for (const item of items) incoming.set(item.id, item);
145
+ const prev = this._items;
146
+ let changed = false;
147
+ const replaced = /* @__PURE__ */ new Set();
148
+ const newArray = [];
149
+ for (const existing of prev) if (incoming.has(existing.id)) {
150
+ const replacement = incoming.get(existing.id);
151
+ if (replacement !== existing) changed = true;
152
+ newArray.push(replacement);
153
+ replaced.add(existing.id);
154
+ } else newArray.push(existing);
155
+ for (const [id, item] of incoming) if (!replaced.has(id)) {
156
+ newArray.push(item);
157
+ changed = true;
158
+ }
159
+ if (!changed) return;
160
+ if (this._timestamps) {
161
+ const now = Date.now();
162
+ for (const [id] of incoming) this._timestamps.set(id, now);
163
+ }
164
+ for (const [id, item] of incoming) this._index.set(id, item);
165
+ let result = newArray;
166
+ if (this._maxSize > 0 && result.length > this._maxSize) result = this._evictForCapacity(result);
167
+ this._items = freeze(result);
168
+ this._notify(prev);
169
+ this._scheduleEvictionTimer();
170
+ }
171
+ /**
172
+ * Remove items by id(s).
173
+ */
174
+ remove(...ids) {
175
+ if (this._disposed) throw new Error("Cannot remove from disposed Collection");
176
+ if (ids.length === 0) return;
177
+ if (ids.length === 1) {
178
+ const id = ids[0];
179
+ if (!this._index.has(id)) return;
180
+ const prev = this._items;
181
+ this._items = freeze(prev.filter((item) => item.id !== id));
182
+ this._index.delete(id);
183
+ this._timestamps?.delete(id);
184
+ this._notify(prev);
185
+ this._scheduleEvictionTimer();
186
+ return;
187
+ }
188
+ const idSet = new Set(ids);
189
+ const filtered = this._items.filter((item) => !idSet.has(item.id));
190
+ if (filtered.length === this._items.length) return;
191
+ const prev = this._items;
192
+ this._items = freeze(filtered);
193
+ for (const id of ids) {
194
+ this._index.delete(id);
195
+ this._timestamps?.delete(id);
196
+ }
197
+ this._notify(prev);
198
+ this._scheduleEvictionTimer();
199
+ }
200
+ /**
201
+ * Update an item by id with partial changes.
202
+ */
203
+ update(id, changes) {
204
+ if (this._disposed) throw new Error("Cannot update disposed Collection");
205
+ const existing = this._index.get(id);
206
+ if (!existing) return;
207
+ if (!Object.keys(changes).some((key) => changes[key] !== existing[key])) return;
208
+ const updated = {
209
+ ...existing,
210
+ ...changes,
211
+ id
212
+ };
213
+ const prev = this._items;
214
+ const idx = this._items.indexOf(existing);
215
+ const newItems = [...prev];
216
+ newItems[idx] = updated;
217
+ this._items = freeze(newItems);
218
+ this._index.set(id, updated);
219
+ this._notify(prev);
220
+ }
221
+ /**
222
+ * Replace all items.
223
+ */
224
+ reset(items) {
225
+ if (this._disposed) throw new Error("Cannot reset disposed Collection");
226
+ const prev = this._items;
227
+ if (this._timestamps) {
228
+ this._timestamps.clear();
229
+ const now = Date.now();
230
+ for (const item of items) this._timestamps.set(item.id, now);
231
+ }
232
+ let result = [...items];
233
+ if (this._maxSize > 0 && result.length > this._maxSize) result = this._evictForCapacity(result);
234
+ this._items = freeze(result);
235
+ this._rebuildIndex();
236
+ this._notify(prev);
237
+ this._scheduleEvictionTimer();
238
+ }
239
+ /**
240
+ * Remove all items.
241
+ */
242
+ clear() {
243
+ if (this._disposed) throw new Error("Cannot clear disposed Collection");
244
+ if (this._items.length === 0) return;
245
+ const prev = this._items;
246
+ this._items = freeze([]);
247
+ this._index.clear();
248
+ this._timestamps?.clear();
249
+ this._clearEvictionTimer();
250
+ this._notify(prev);
251
+ }
252
+ /**
253
+ * Snapshot current state, apply callback mutations, and return a rollback function.
254
+ * Rollback restores items to pre-callback state regardless of later mutations.
255
+ */
256
+ optimistic(callback) {
257
+ if (this._disposed) throw new Error("Cannot perform optimistic update on disposed Collection");
258
+ const snapshot = this._items;
259
+ const timestampSnapshot = this._timestamps ? new Map(this._timestamps) : null;
260
+ callback();
261
+ let rolledBack = false;
262
+ return () => {
263
+ if (rolledBack || this._disposed) return;
264
+ rolledBack = true;
265
+ const prev = this._items;
266
+ this._items = snapshot;
267
+ if (timestampSnapshot) this._timestamps = timestampSnapshot;
268
+ this._rebuildIndex();
269
+ this._notify(prev);
270
+ this._scheduleEvictionTimer();
271
+ };
272
+ }
273
+ /**
274
+ * Get item by id.
275
+ */
276
+ get(id) {
277
+ return this._index.get(id);
278
+ }
279
+ /**
280
+ * Check if item exists by id.
281
+ */
282
+ has(id) {
283
+ return this._index.has(id);
284
+ }
285
+ /**
286
+ * Find first item matching predicate.
287
+ */
288
+ find(predicate) {
289
+ return this._items.find(predicate);
290
+ }
291
+ /**
292
+ * Filter items matching predicate.
293
+ */
294
+ filter(predicate) {
295
+ return this._items.filter(predicate);
296
+ }
297
+ /**
298
+ * Return sorted copy.
299
+ */
300
+ sorted(compareFn) {
301
+ return [...this._items].sort(compareFn);
302
+ }
303
+ /**
304
+ * Map items to new array.
305
+ */
306
+ map(fn) {
307
+ return this._items.map(fn);
308
+ }
309
+ /** Subscribes to state changes. Returns an unsubscribe function. */
310
+ subscribe(listener) {
311
+ if (this._disposed) return () => {};
312
+ this._listeners.add(listener);
313
+ return () => {
314
+ this._listeners.delete(listener);
315
+ };
316
+ }
317
+ /** Tears down the instance, releasing all subscriptions and resources. */
318
+ dispose() {
319
+ if (this._disposed) return;
320
+ this._disposed = true;
321
+ this._clearEvictionTimer();
322
+ this._abortController?.abort();
323
+ if (this._cleanups) {
324
+ for (const fn of this._cleanups) fn();
325
+ this._cleanups = null;
326
+ }
327
+ this.onDispose?.();
328
+ this._listeners.clear();
329
+ this._index.clear();
330
+ this._timestamps?.clear();
331
+ }
332
+ /** Registers a cleanup function to be called on dispose. @protected */
333
+ addCleanup(fn) {
334
+ if (!this._cleanups) this._cleanups = [];
335
+ this._cleanups.push(fn);
336
+ }
337
+ _notify(prev) {
338
+ for (const listener of this._listeners) listener(this._items, prev);
339
+ }
340
+ _rebuildIndex() {
341
+ this._index.clear();
342
+ for (const item of this._items) this._index.set(item.id, item);
343
+ }
344
+ _evictForCapacity(items) {
345
+ const excess = items.length - this._maxSize;
346
+ if (excess <= 0) return items;
347
+ const candidates = items.slice(0, excess);
348
+ const toEvict = this._applyOnEvict(candidates, "capacity");
349
+ if (toEvict === false) return items;
350
+ if (toEvict.length === 0) return items;
351
+ const evictIds = new Set(toEvict.map((item) => item.id));
352
+ const result = items.filter((item) => !evictIds.has(item.id));
353
+ for (const item of toEvict) {
354
+ this._index.delete(item.id);
355
+ this._timestamps?.delete(item.id);
356
+ }
357
+ return result;
358
+ }
359
+ _applyOnEvict(candidates, reason) {
360
+ if (!this.onEvict) return candidates;
361
+ const result = this.onEvict(candidates, reason);
362
+ if (result === false) {
363
+ if (__DEV__ && reason === "capacity" && this._maxSize > 0) {
364
+ const currentSize = this._items.length + candidates.length;
365
+ if (currentSize > this._maxSize * 2) console.warn(`[mvc-kit] Collection exceeded 2x MAX_SIZE (${currentSize}/${this._maxSize}). onEvict is vetoing eviction — this may cause unbounded growth.`);
366
+ }
367
+ return false;
368
+ }
369
+ if (Array.isArray(result)) {
370
+ const candidateIds = new Set(candidates.map((c) => c.id));
371
+ return result.filter((item) => candidateIds.has(item.id));
372
+ }
373
+ return candidates;
374
+ }
375
+ _sweepExpired() {
376
+ if (this._disposed || !this._timestamps || this._ttl <= 0) return;
377
+ const now = Date.now();
378
+ const ttl = this._ttl;
379
+ const expired = [];
380
+ for (const item of this._items) {
381
+ const ts = this._timestamps.get(item.id);
382
+ if (ts !== void 0 && now - ts >= ttl) expired.push(item);
383
+ }
384
+ if (expired.length === 0) {
385
+ this._scheduleEvictionTimer();
386
+ return;
387
+ }
388
+ const toEvict = this._applyOnEvict(expired, "ttl");
389
+ if (toEvict === false) {
390
+ this._scheduleEvictionTimer();
391
+ return;
392
+ }
393
+ if (toEvict.length === 0) {
394
+ this._scheduleEvictionTimer();
395
+ return;
396
+ }
397
+ const evictIds = new Set(toEvict.map((item) => item.id));
398
+ const prev = this._items;
399
+ this._items = freeze(prev.filter((item) => !evictIds.has(item.id)));
400
+ for (const item of toEvict) {
401
+ this._index.delete(item.id);
402
+ this._timestamps.delete(item.id);
403
+ }
404
+ this._notify(prev);
405
+ this._scheduleEvictionTimer();
406
+ }
407
+ _scheduleEvictionTimer() {
408
+ this._clearEvictionTimer();
409
+ if (this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;
410
+ const now = Date.now();
411
+ const ttl = this._ttl;
412
+ let earliest = Infinity;
413
+ for (const ts of this._timestamps.values()) if (ts < earliest) earliest = ts;
414
+ const delay = Math.max(0, earliest + ttl - now);
415
+ this._evictionTimer = setTimeout(() => this._sweepExpired(), delay);
416
+ }
417
+ _clearEvictionTimer() {
418
+ if (this._evictionTimer !== null) {
419
+ clearTimeout(this._evictionTimer);
420
+ this._evictionTimer = null;
421
+ }
422
+ }
423
+ };
424
+ //#endregion
506
425
  exports.Collection = Collection;
507
- //# sourceMappingURL=Collection.cjs.map
426
+
427
+ //# sourceMappingURL=Collection.cjs.map