mvc-kit 2.13.0 → 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 (200) hide show
  1. package/BEST_PRACTICES.md +1390 -0
  2. package/agent-config/claude-code/agents/mvc-kit-architect.md +6 -1
  3. package/agent-config/claude-code/skills/guide/SKILL.md +9 -0
  4. package/agent-config/lib/install-claude.mjs +8 -2
  5. package/examples/primitive/channel.ts +109 -0
  6. package/examples/primitive/collection.ts +118 -0
  7. package/examples/primitive/controller.ts +118 -0
  8. package/examples/primitive/counter.ts +108 -0
  9. package/examples/primitive/env.d.ts +1 -0
  10. package/examples/primitive/eventbus.ts +77 -0
  11. package/examples/primitive/feed.ts +162 -0
  12. package/examples/primitive/model.ts +82 -0
  13. package/examples/primitive/pagination.ts +91 -0
  14. package/examples/primitive/pending.ts +189 -0
  15. package/examples/primitive/persistent-collection.ts +116 -0
  16. package/examples/primitive/resource.ts +114 -0
  17. package/examples/primitive/selection.ts +96 -0
  18. package/examples/primitive/sorting.ts +112 -0
  19. package/examples/primitive/timer.ts +58 -0
  20. package/examples/primitive/trackable.ts +225 -0
  21. package/examples/primitive/tsconfig.json +20 -0
  22. package/examples/primitive/viewmodel-service.ts +161 -0
  23. package/examples/react/AuthExample/index.html +12 -0
  24. package/examples/react/AuthExample/src/App.tsx +29 -0
  25. package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
  26. package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
  27. package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
  28. package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
  29. package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
  30. package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
  31. package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
  32. package/examples/react/AuthExample/src/env.d.ts +10 -0
  33. package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
  34. package/examples/react/AuthExample/src/main.tsx +10 -0
  35. package/examples/react/AuthExample/src/mock/api.ts +78 -0
  36. package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
  37. package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
  38. package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
  39. package/examples/react/AuthExample/src/styles.css +445 -0
  40. package/examples/react/AuthExample/src/types/auth.ts +12 -0
  41. package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
  42. package/examples/react/AuthExample/tsconfig.json +22 -0
  43. package/examples/react/AuthExample/vite.config.ts +18 -0
  44. package/examples/react/ComplexApp/index.html +12 -0
  45. package/examples/react/ComplexApp/src/App.tsx +17 -0
  46. package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
  47. package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
  48. package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
  49. package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
  50. package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
  51. package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
  52. package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
  53. package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
  54. package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
  55. package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
  56. package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
  57. package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
  58. package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
  59. package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
  60. package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
  61. package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
  62. package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
  63. package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
  64. package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
  65. package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
  66. package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
  67. package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
  68. package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
  69. package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
  70. package/examples/react/ComplexApp/src/env.d.ts +10 -0
  71. package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
  72. package/examples/react/ComplexApp/src/main.tsx +10 -0
  73. package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
  74. package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
  75. package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
  76. package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
  77. package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
  78. package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
  79. package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
  80. package/examples/react/ComplexApp/src/styles.css +463 -0
  81. package/examples/react/ComplexApp/src/types/activity.ts +8 -0
  82. package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
  83. package/examples/react/ComplexApp/src/types/social.ts +8 -0
  84. package/examples/react/ComplexApp/src/types/users.ts +6 -0
  85. package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
  86. package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
  87. package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
  88. package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
  89. package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
  90. package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
  91. package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
  92. package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
  93. package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
  94. package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
  95. package/examples/react/ComplexApp/tsconfig.json +22 -0
  96. package/examples/react/ComplexApp/vite.config.ts +18 -0
  97. package/examples/react/FullApp/index.html +12 -0
  98. package/examples/react/FullApp/src/App.tsx +28 -0
  99. package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
  100. package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
  101. package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
  102. package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
  103. package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
  104. package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
  105. package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
  106. package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
  107. package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
  108. package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
  109. package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
  110. package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
  111. package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
  112. package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
  113. package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
  114. package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
  115. package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
  116. package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
  117. package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
  118. package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
  119. package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
  120. package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
  121. package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
  122. package/examples/react/FullApp/src/env.d.ts +10 -0
  123. package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
  124. package/examples/react/FullApp/src/main.tsx +10 -0
  125. package/examples/react/FullApp/src/mock/delay.ts +21 -0
  126. package/examples/react/FullApp/src/mock/locations.ts +76 -0
  127. package/examples/react/FullApp/src/mock/messages.ts +237 -0
  128. package/examples/react/FullApp/src/mock/users.ts +84 -0
  129. package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
  130. package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
  131. package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
  132. package/examples/react/FullApp/src/services/AuthService.ts +18 -0
  133. package/examples/react/FullApp/src/services/LocationService.ts +23 -0
  134. package/examples/react/FullApp/src/services/MessageService.ts +65 -0
  135. package/examples/react/FullApp/src/services/UserService.ts +23 -0
  136. package/examples/react/FullApp/src/styles.css +767 -0
  137. package/examples/react/FullApp/src/types/conversation.ts +7 -0
  138. package/examples/react/FullApp/src/types/location.ts +12 -0
  139. package/examples/react/FullApp/src/types/message.ts +7 -0
  140. package/examples/react/FullApp/src/types/user.ts +10 -0
  141. package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
  142. package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
  143. package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
  144. package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
  145. package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
  146. package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
  147. package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
  148. package/examples/react/FullApp/tsconfig.json +22 -0
  149. package/examples/react/FullApp/vite.config.ts +18 -0
  150. package/examples/react/WorkerApp/index.html +12 -0
  151. package/examples/react/WorkerApp/src/App.tsx +24 -0
  152. package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
  153. package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
  154. package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
  155. package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
  156. package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
  157. package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
  158. package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
  159. package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
  160. package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
  161. package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
  162. package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
  163. package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
  164. package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
  165. package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
  166. package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
  167. package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
  168. package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
  169. package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
  170. package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
  171. package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
  172. package/examples/react/WorkerApp/src/env.d.ts +10 -0
  173. package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
  174. package/examples/react/WorkerApp/src/main.tsx +10 -0
  175. package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
  176. package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
  177. package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
  178. package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
  179. package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
  180. package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
  181. package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
  182. package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
  183. package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
  184. package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
  185. package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
  186. package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
  187. package/examples/react/WorkerApp/src/styles.css +756 -0
  188. package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
  189. package/examples/react/WorkerApp/src/types/message.ts +7 -0
  190. package/examples/react/WorkerApp/src/types/shift.ts +13 -0
  191. package/examples/react/WorkerApp/src/types/site.ts +8 -0
  192. package/examples/react/WorkerApp/src/types/worker.ts +8 -0
  193. package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
  194. package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
  195. package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
  196. package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
  197. package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
  198. package/examples/react/WorkerApp/tsconfig.json +22 -0
  199. package/examples/react/WorkerApp/vite.config.ts +18 -0
  200. package/package.json +4 -2
@@ -21,7 +21,12 @@ The mvc-kit framework reference skill is preloaded into this agent's context. Fo
21
21
  - `recipes.md` — Composition recipes for real-world features
22
22
  - `testing.md` — Testing patterns
23
23
 
24
- For detailed per-class documentation, search the `.md` files colocated with source in `node_modules/mvc-kit/src/`.
24
+ For deep dives on any specific class or hook, search for the `.md` file by name in `node_modules/mvc-kit/src/`:
25
+
26
+ - **Core:** `ViewModel.md`, `Model.md`, `Collection.md`, `PersistentCollection.md`, `Resource.md`, `Service.md`, `EventBus.md`, `Channel.md`, `Controller.md`, `Trackable.md`, `singleton.md`
27
+ - **Helpers:** `Sorting.md`, `Pagination.md`, `Selection.md`, `Feed.md`, `Pending.md`, `produceDraft.md`
28
+ - **React hooks:** `react/use-local.md`, `react/use-instance.md`, `react/use-singleton.md`, `react/use-model.md`, `react/use-event-bus.md`, `react/use-teardown.md`
29
+ - **Components:** `react/components/DataTable.md`, `react/components/CardList.md`, `react/components/InfiniteScroll.md`
25
30
 
26
31
  ## Core Classes
27
32
 
@@ -97,3 +97,12 @@ For full composition recipes with code, see [recipes.md](recipes.md).
97
97
  - [anti-patterns.md](anti-patterns.md) — Anti-patterns to reject with fixes
98
98
  - [recipes.md](recipes.md) — Composition recipes for real-world features
99
99
  - [testing.md](testing.md) — Testing patterns (teardownAll, async assertions, memoization verification)
100
+
101
+ ### Per-Class Documentation (in `node_modules/mvc-kit/src/`)
102
+
103
+ For deep dives on any specific class or hook, search for the `.md` file by name:
104
+
105
+ - **Core:** `ViewModel.md`, `Model.md`, `Collection.md`, `PersistentCollection.md`, `Resource.md`, `Service.md`, `EventBus.md`, `Channel.md`, `Controller.md`, `Trackable.md`, `singleton.md`
106
+ - **Helpers:** `Sorting.md`, `Pagination.md`, `Selection.md`, `Feed.md`, `Pending.md`, `produceDraft.md`
107
+ - **React hooks:** `react/use-local.md`, `react/use-instance.md`, `react/use-singleton.md`, `react/use-model.md`, `react/use-event-bus.md`, `react/use-teardown.md`
108
+ - **Components:** `react/components/DataTable.md`, `react/components/CardList.md`, `react/components/InfiniteScroll.md`
@@ -80,8 +80,14 @@ user-invocable: false
80
80
  - [recipes.md](recipes.md) — Composition recipes for real-world features
81
81
  - [testing.md](testing.md) — Testing patterns (teardownAll, async assertions, memoization verification)
82
82
 
83
- For detailed per-class documentation, read the \`.md\` files colocated with source in:
84
- \`node_modules/mvc-kit/src/\`
83
+ ### Per-Class Documentation (in \`node_modules/mvc-kit/src/\`)
84
+
85
+ For deep dives on any specific class or hook, search for the \`.md\` file by name:
86
+
87
+ - **Core:** \`ViewModel.md\`, \`Model.md\`, \`Collection.md\`, \`PersistentCollection.md\`, \`Resource.md\`, \`Service.md\`, \`EventBus.md\`, \`Channel.md\`, \`Controller.md\`, \`Trackable.md\`, \`singleton.md\`
88
+ - **Helpers:** \`Sorting.md\`, \`Pagination.md\`, \`Selection.md\`, \`Feed.md\`, \`Pending.md\`, \`produceDraft.md\`
89
+ - **React hooks:** \`react/use-local.md\`, \`react/use-instance.md\`, \`react/use-singleton.md\`, \`react/use-model.md\`, \`react/use-event-bus.md\`, \`react/use-teardown.md\`
90
+ - **Components:** \`react/components/DataTable.md\`, \`react/components/CardList.md\`, \`react/components/InfiniteScroll.md\`
85
91
  `;
86
92
 
87
93
  writeFileSync(join(guideDir, 'SKILL.md'), skillContent, 'utf-8');
@@ -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();
@@ -0,0 +1,118 @@
1
+ import { Controller, ViewModel, Collection, singleton, teardownAll } from 'mvc-kit';
2
+
3
+ // Controller: Stateless multi-ViewModel orchestrator
4
+ //
5
+ // Controllers coordinate between ViewModels, Models, and Services when
6
+ // a single ViewModel can't own the workflow. This is rare — most
7
+ // orchestration fits in a single ViewModel.
8
+ //
9
+ // Use Controller only for pure cross-cutting coordination with no state
10
+ // of its own: multi-step checkout, drag-and-drop between lists, etc.
11
+ //
12
+ // What Controller provides:
13
+ // - init() / dispose() lifecycle
14
+ // - subscribeTo() / listenTo() with auto-cleanup
15
+ // - disposeSignal for cancelling async operations
16
+ // - addCleanup() for custom teardown
17
+ //
18
+ // What Controller does NOT provide:
19
+ // - State (no set())
20
+ // - Computed getters
21
+ // - Async tracking
22
+ // - Events (no emit())
23
+
24
+ // --- Supporting types ---
25
+
26
+ interface Task {
27
+ id: string;
28
+ title: string;
29
+ status: 'todo' | 'done';
30
+ }
31
+
32
+ interface TaskListState {
33
+ items: Task[];
34
+ }
35
+
36
+ // --- Two ViewModels that the Controller coordinates ---
37
+
38
+ class TodoListViewModel extends ViewModel<TaskListState> {
39
+ removeItem(id: string) {
40
+ this.set({ items: this.state.items.filter(t => t.id !== id) });
41
+ }
42
+
43
+ addItem(task: Task) {
44
+ this.set({ items: [...this.state.items, task] });
45
+ }
46
+ }
47
+
48
+ class DoneListViewModel extends ViewModel<TaskListState> {
49
+ addItem(task: Task) {
50
+ this.set({ items: [...this.state.items, { ...task, status: 'done' as const }] });
51
+ }
52
+ }
53
+
54
+ // --- Controller definition ---
55
+
56
+ class TaskTransferController extends Controller {
57
+ constructor(
58
+ private todoVM: TodoListViewModel,
59
+ private doneVM: DoneListViewModel,
60
+ ) {
61
+ super();
62
+ }
63
+
64
+ protected onInit() {
65
+ // subscribeTo auto-cleans up on dispose
66
+ this.subscribeTo(this.todoVM, (state) => {
67
+ console.log(`Todo list: ${state.items.length} items`);
68
+ });
69
+
70
+ this.subscribeTo(this.doneVM, (state) => {
71
+ console.log(`Done list: ${state.items.length} items`);
72
+ });
73
+ }
74
+
75
+ // Pure coordination — moves an item from todo to done
76
+ completeTask(taskId: string) {
77
+ const task = this.todoVM.state.items.find(t => t.id === taskId);
78
+ if (!task) return;
79
+ this.todoVM.removeItem(taskId);
80
+ this.doneVM.addItem(task);
81
+ }
82
+
83
+ protected onDispose() {
84
+ console.log('TaskTransferController disposed');
85
+ }
86
+ }
87
+
88
+ // --- Usage ---
89
+
90
+ const todoVM = new TodoListViewModel({
91
+ items: [
92
+ { id: '1', title: 'Learn mvc-kit', status: 'todo' },
93
+ { id: '2', title: 'Build an app', status: 'todo' },
94
+ { id: '3', title: 'Write tests', status: 'todo' },
95
+ ],
96
+ });
97
+ todoVM.init();
98
+
99
+ const doneVM = new DoneListViewModel({ items: [] });
100
+ doneVM.init();
101
+
102
+ const controller = new TaskTransferController(todoVM, doneVM);
103
+ controller.init();
104
+
105
+ console.log('Todo:', todoVM.state.items.length); // 3
106
+ console.log('Done:', doneVM.state.items.length); // 0
107
+
108
+ // Controller coordinates the move
109
+ controller.completeTask('1');
110
+
111
+ console.log('Todo:', todoVM.state.items.length); // 2
112
+ console.log('Done:', doneVM.state.items.length); // 1
113
+ console.log('Done item:', doneVM.state.items[0]?.title); // 'Learn mvc-kit'
114
+
115
+ // Cleanup
116
+ controller.dispose();
117
+ todoVM.dispose();
118
+ doneVM.dispose();
@@ -0,0 +1,108 @@
1
+ import { ViewModel, singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
2
+
3
+ // ViewModel: Reactive state + computed getters + async tracking + typed events
4
+ //
5
+ // The core building block. Holds state, derives computed values via getters,
6
+ // provides actions to update state, and tracks async method loading/error
7
+ // automatically. After init(), getters are auto-memoized and only recompute
8
+ // when their dependencies change.
9
+
10
+ // Define state interface
11
+ interface CounterState {
12
+ count: number;
13
+ }
14
+
15
+ // Create a ViewModel by extending the base class
16
+ class CounterViewModel extends ViewModel<CounterState> {
17
+ // --- Computed getters (auto-memoized after init) ---
18
+ get doubled(): number {
19
+ return this.state.count * 2;
20
+ }
21
+
22
+ get isPositive(): boolean {
23
+ return this.state.count > 0;
24
+ }
25
+
26
+ get parity(): 'even' | 'odd' {
27
+ return this.state.count % 2 === 0 ? 'even' : 'odd';
28
+ }
29
+
30
+ // --- Actions ---
31
+ increment() {
32
+ this.set({ count: this.state.count + 1 });
33
+ }
34
+
35
+ decrement() {
36
+ this.set({ count: this.state.count - 1 });
37
+ }
38
+
39
+ reset() {
40
+ this.set({ count: 0 });
41
+ }
42
+
43
+ // Updater function pattern for derived updates
44
+ multiplyBy(factor: number) {
45
+ this.set(prev => ({ count: prev.count * factor }));
46
+ }
47
+
48
+ // Lifecycle hook: called after each state update
49
+ protected onSet(prev: Readonly<CounterState>, next: Readonly<CounterState>) {
50
+ console.log(`Count changed: ${prev.count} → ${next.count}`);
51
+ }
52
+
53
+ // Lifecycle hook: called when disposed
54
+ protected onDispose() {
55
+ console.log('CounterViewModel disposed');
56
+ }
57
+ }
58
+
59
+ // --- Basic usage ---
60
+
61
+ const counter = new CounterViewModel({ count: 0 });
62
+ counter.init(); // activates getter memoization and async tracking
63
+
64
+ // Subscribe to state changes
65
+ const unsubscribe = counter.subscribe((state, prev) => {
66
+ console.log(`Subscriber notified: ${prev.count} → ${state.count}`);
67
+ });
68
+
69
+ counter.increment(); // Count changed: 0 → 1
70
+ counter.increment(); // Count changed: 1 → 2
71
+ counter.multiplyBy(5); // Count changed: 2 → 10
72
+
73
+ // Access current state
74
+ console.log('Current count:', counter.state.count); // 10
75
+
76
+ // --- Computed getters ---
77
+
78
+ console.log('Doubled:', counter.doubled); // 20
79
+ console.log('Is positive:', counter.isPositive); // true
80
+ console.log('Parity:', counter.parity); // even
81
+
82
+ counter.decrement(); // Count changed: 10 → 9
83
+ console.log('Doubled:', counter.doubled); // 18
84
+ console.log('Parity:', counter.parity); // odd
85
+
86
+ // Cleanup
87
+ unsubscribe();
88
+ counter.dispose();
89
+
90
+ // --- Singleton pattern ---
91
+
92
+ // Get or create a singleton instance
93
+ const shared1 = singleton(CounterViewModel, { count: 100 });
94
+ shared1.init(); // activate getter memoization on the singleton too
95
+ const shared2 = singleton(CounterViewModel, { count: 999 }); // args ignored, same instance returned
96
+
97
+ console.log('Same instance:', shared1 === shared2); // true
98
+ console.log('Singleton count:', shared1.state.count); // 100
99
+
100
+ // Check if singleton exists
101
+ console.log('Has singleton:', hasSingleton(CounterViewModel)); // true
102
+
103
+ // Cleanup singleton
104
+ teardown(CounterViewModel);
105
+ console.log('Has singleton after teardown:', hasSingleton(CounterViewModel)); // false
106
+
107
+ // Cleanup all singletons (useful in tests)
108
+ teardownAll();
@@ -0,0 +1 @@
1
+ declare const __MVC_KIT_DEV__: boolean;
@@ -0,0 +1,77 @@
1
+ import { EventBus } from 'mvc-kit';
2
+
3
+ // EventBus: Typed pub/sub for decoupled communication
4
+
5
+ // Define event types
6
+ interface AppEvents {
7
+ 'user:login': { userId: string; timestamp: number };
8
+ 'user:logout': { userId: string };
9
+ 'notification': { message: string; type: 'info' | 'warning' | 'error' };
10
+ 'cart:updated': { itemCount: number };
11
+ }
12
+
13
+ // --- Basic usage ---
14
+
15
+ const bus = new EventBus<AppEvents>();
16
+
17
+ // Subscribe to events
18
+ const unsubLogin = bus.on('user:login', ({ userId, timestamp }) => {
19
+ console.log(`User ${userId} logged in at ${new Date(timestamp).toISOString()}`);
20
+ });
21
+
22
+ const unsubNotification = bus.on('notification', ({ message, type }) => {
23
+ console.log(`[${type.toUpperCase()}] ${message}`);
24
+ });
25
+
26
+ // Emit events
27
+ bus.emit('user:login', { userId: '123', timestamp: Date.now() });
28
+ bus.emit('notification', { message: 'Welcome back!', type: 'info' });
29
+
30
+ // One-time subscription - auto-unsubscribes after first event
31
+ bus.once('user:logout', ({ userId }) => {
32
+ console.log(`User ${userId} logged out (one-time handler)`);
33
+ });
34
+
35
+ bus.emit('user:logout', { userId: '123' }); // Handler called
36
+ bus.emit('user:logout', { userId: '123' }); // Handler NOT called (already unsubscribed)
37
+
38
+ // Unsubscribe manually
39
+ unsubLogin();
40
+ unsubNotification();
41
+
42
+ // After unsubscribe, handlers are not called
43
+ bus.emit('user:login', { userId: '456', timestamp: Date.now() }); // No output
44
+
45
+ // --- Practical pattern: Cross-component communication ---
46
+
47
+ class AuthService {
48
+ constructor(private bus: EventBus<AppEvents>) {}
49
+
50
+ login(userId: string) {
51
+ // ... authentication logic ...
52
+ this.bus.emit('user:login', { userId, timestamp: Date.now() });
53
+ }
54
+
55
+ logout(userId: string) {
56
+ // ... logout logic ...
57
+ this.bus.emit('user:logout', { userId });
58
+ }
59
+ }
60
+
61
+ class NotificationManager {
62
+ constructor(private bus: EventBus<AppEvents>) {
63
+ // React to login events
64
+ this.bus.on('user:login', () => {
65
+ this.bus.emit('notification', { message: 'Welcome!', type: 'info' });
66
+ });
67
+ }
68
+ }
69
+
70
+ // Components can communicate without direct references
71
+ const auth = new AuthService(bus);
72
+ new NotificationManager(bus);
73
+
74
+ auth.login('789'); // Triggers login event, which triggers notification
75
+
76
+ // Cleanup
77
+ bus.dispose();