miolo 3.0.0-beta.227 → 3.0.0-beta.229

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miolo",
3
- "version": "3.0.0-beta.227",
3
+ "version": "3.0.0-beta.229",
4
4
  "description": "all-in-one koa-based server",
5
5
  "author": "Donato Lorenzo <donato@afialapis.com>",
6
6
  "contributors": [
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: miolo-caching
3
+ description: Advanced data caching and synchronization patterns for miolo applications. Use when handling useSsrData(), real-time data invalidation via WebSockets, or preventing abrupt UI re-renders using lazy refresh patterns.
4
+ ---
5
+
6
+ # Miolo Caching & Sockets
7
+
8
+ This skill covers how to efficiently cache data on the client using `useSsrData()`, keep it in sync across multiple clients via WebSockets, and ensure smooth UI experiences.
9
+
10
+ ## The `useSsrData()` Cache
11
+
12
+ `useSsrData` handles both Server-Side Rendering (SSR) preloading and client-side data fetching. However, its true power comes from its caching capabilities and how it integrates with global application events.
13
+
14
+ ### Basic Invalidation
15
+
16
+ When data changes on the server (e.g., a user adds a new Todo), you typically need to refresh the client's cache.
17
+
18
+ ```javascript
19
+ const [todos, setTodos, refreshTodos] = useSsrData('todos', [], loadTodos)
20
+
21
+ // After a mutation:
22
+ const handleAdd = async (newTodo) => {
23
+ await fetcher.post('/api/todos', newTodo)
24
+ // Manually refresh local data
25
+ refreshTodos()
26
+ }
27
+ ```
28
+
29
+ ## WebSocket Synchronization
30
+
31
+ In multi-user apps, when User A updates a Todo, User B needs to see the change. Miolo uses WebSockets to broadcast invalidation messages.
32
+
33
+ ### The `exclude_socket_id` Pattern
34
+
35
+ When User A makes a mutation, they generally update their UI optimistically or refresh their own data immediately. If the server broadcasts a blanket `refresh` to *everyone*, User A will suffer a redundant data fetch.
36
+
37
+ **Server-side:**
38
+ ```javascript
39
+ // Example: src/server/io/cache/todos/invalidate.mjs
40
+
41
+ export async function ch_todos_invalidate(ctx) {
42
+ // A mutation happened. Inform the socket layer.
43
+ // The cache key "todos" matches the key used in the client's useSsrData("todos", ...)
44
+ await ctx.miolo.io.emitSsrRefresh(ctx, "todos", {
45
+ excludeCurrent: true // Ignore the user who caused the change!
46
+ })
47
+ }
48
+ ```
49
+
50
+ **Client-side:**
51
+ When the socket receives the invalidation, Miolo's core triggers a re-fetch for any `useSsrData` listening to the updated keys, keeping User B in sync seamlessly.
52
+
53
+ ### The `only_socket_id` Pattern
54
+
55
+ Sometimes, an operation is localized or private. The server can push data updates specifically to a single connected client.
56
+
57
+ **Server-side:**
58
+ ```javascript
59
+ // Example: src/server/io/cache/stats/invalidate.mjs
60
+
61
+ export async function ch_personal_stats_invalidate(ctx) {
62
+ await ctx.miolo.io.emitSsrRefresh(ctx, "personal_stats", {
63
+ onlyCurrent: true // Push update specifically to the originating client
64
+ })
65
+ }
66
+ ```
67
+
68
+ ## Lazy SSR Data Refresh (`ssr-lazy-refresh`)
69
+
70
+ A common UX problem with real-time sockets is "Abrupt UI Re-renders". If User B is actively reading a list and User A edits an item, a forced websocket refresh might cause User B's screen to jump or lose scroll state unexpectedly.
71
+
72
+ To solve this, use the **lazy refresh** pattern:
73
+
74
+ 1. **Trigger `ssr-lazy-refresh`:** Instead of an immediate forced refresh, the server passes `lazy: true`.
75
+ ```javascript
76
+ await ctx.miolo.io.emitSsrRefresh(ctx, "todos", {
77
+ excludeCurrent: true,
78
+ lazy: true
79
+ })
80
+ ```
81
+ 2. **Wait for Interaction:** The data refresh is delayed until the user navigates, focuses the window, or explicitly triggers an action (handled by `miolo-navigation` and window focus listeners).
82
+ 3. **Implementation:** Miolo marks the internal `pendingLazyRefreshRef` flag. When the UX is safe (e.g., natural navigation boundary), it refreshes without causing jarring layout shifts.
83
+
84
+ ### Best Practices
85
+
86
+ - **Avoid Blanket Broadcasts:** Always provide `excludeCurrent: true` in your server mutations when the originating client already handled its local state optimistically.
87
+ - **Respect User Focus:** Use lazy refreshing for heavy or layout-shifting data updates.
88
+ - **Graceful Degradation:** The cache should always fall back to `remoteLoader` if the socket disconnects.
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: miolo-model
3
+ description: Patterns and conventions for data models in miolo apps. Use when creating or modifying data models, establishing JSDoc strict typing, extending MioloModel or MioloArray, or working in the src/ns/model directory.
4
+ ---
5
+
6
+ # Miolo Data Models
7
+
8
+ This skill details how to properly use and type data models in miolo applications using `miolo-model`. Models should encapsulate business logic and data structure securely, working across both client and server boundaries.
9
+
10
+ ## Architecture & Location
11
+
12
+ Models reside in `src/ns/model/` (or `src/ns/models/`) because they are part of the shared **namespace (ns)** layer, meaning they execute seamlessly on both the server (Node.js) and the client (React).
13
+
14
+ ```
15
+ src/ns/model/
16
+ ├── base/ # Base models mirroring DB structures or API responses
17
+ │ ├── Category.mjs
18
+ │ ├── Todo.mjs
19
+ │ └── TodoList.mjs
20
+ └── aggregate_models... # Domain-specific aggregated models
21
+ ```
22
+
23
+ ## Extending `MioloModel`
24
+
25
+ All single-entity models should extend `MioloModel`.
26
+
27
+ **File:** `src/ns/model/base/Todo.mjs`
28
+
29
+ ```javascript
30
+ import { MioloModel } from "miolo-model"
31
+ import Category from "./Category.mjs"
32
+
33
+ /**
34
+ * @typedef {import("#ns/names/status.mjs").TodoStatus} TodoStatus
35
+ */
36
+
37
+ export default class Todo extends MioloModel {
38
+ constructor(data) {
39
+ super(data)
40
+
41
+ /**
42
+ * Initializing nested models
43
+ * @type {Category}
44
+ */
45
+ this.category = new Category()
46
+ }
47
+
48
+ /**
49
+ * Safely setting a nested model using Miolo patterns
50
+ * @param {Category | object} [category]
51
+ */
52
+ set_category(category) {
53
+ this.category = category instanceof Category ? category : new Category(category || {})
54
+ }
55
+
56
+ // --- Getters to access internal data map safely ---
57
+
58
+ /** @returns {number | undefined} */
59
+ get id_todo() {
60
+ return this.get_value("id_todo")
61
+ }
62
+
63
+ /** @returns {TodoStatus} */
64
+ get status() {
65
+ // Provide a sensible default if property is not found
66
+ return this.get_value("status", "PENDING")
67
+ }
68
+
69
+ /** @returns {boolean} */
70
+ get is_urgent() {
71
+ return this.get_value("is_urgent", false)
72
+ }
73
+
74
+ // --- Encapsulated Business Logic ---
75
+
76
+ /**
77
+ * @returns {boolean}
78
+ */
79
+ is_completed() {
80
+ return this.status === "COMPLETED"
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Key Principles for Models:
86
+ 1. **Never read internal keys directly:** Always use `this.get_value("key", defaultValue)`.
87
+ 2. **Encapsulate Nested Objects:** Provide initialization logic in `constructor` and use setters like `set_category` to ensure the nested data is instantiated as a proper model object.
88
+ 3. **Keep Logic Here:** Don't put business logic like "is this todo completed?" in a React component. Put it in `is_completed()` inside the model.
89
+
90
+ ## Extending `MioloArray`
91
+
92
+ For lists of models, extend `MioloArray`. This allows mapping a raw array of data directly into an array of proper model instances.
93
+
94
+ **File:** `src/ns/model/base/TodoList.mjs`
95
+
96
+ ```javascript
97
+ import { MioloArray } from "miolo-model"
98
+ import Todo from "./Todo.mjs"
99
+
100
+ /**
101
+ * @extends {MioloArray<Todo>}
102
+ */
103
+ export default class TodoList extends MioloArray {
104
+ /**
105
+ * Return the Model class to map the raw items into
106
+ */
107
+ get model() {
108
+ return Todo
109
+ }
110
+
111
+ // --- List-specific Business Logic ---
112
+
113
+ /**
114
+ * @returns {Array<Todo>}
115
+ */
116
+ get_urgent_todos() {
117
+ return this.filter(t => t.is_urgent && !t.is_completed())
118
+ }
119
+ }
120
+ ```
121
+
122
+ > [!WARNING]
123
+ > **TypeScript limitations with MioloArray:** Because `MioloArray` alters how native array methods (`filter`, `slice`, etc.) return data (returning a `MioloArray` instead of a standard `Array`), strict TypeScript checking might complain about `push()` signature incompatibilities. This is a known TS limitation with extending `Array`.
124
+ > *Solution:* Use `"skipLibCheck": true` in `jsconfig.json` or explicitly ignore the TS errors in `node_modules` via `exclude`.
125
+
126
+ ## Strict JSDoc Typing (`checkJs: true`)
127
+
128
+ Miolo enforces robust type safety without needing `.ts` files by utilizing standard JSDoc.
129
+
130
+ 1. **Enable Type Checking:** Add `"checkJs": true` in your `jsconfig.json`.
131
+ 2. **Type Every Getter:** `/** @returns {number | undefined} */ get id()`
132
+ 3. **Type Every Param:** `/** @param {boolean} also_doubt @returns {boolean} */ is_callable(also_doubt = false)`
133
+ 4. **Use `@typedef`:** Import and declare shared enums/types from namespace files.
134
+
135
+ ```javascript
136
+ /**
137
+ * @typedef {import("#ns/names/role.mjs").Role} Role
138
+ */
139
+ ```
140
+
141
+ ### Why Typed Models?
142
+
143
+ By strictly typing the `src/ns/model` layer, you guarantee that API responses mapped into `MioloModel` objects will provide intelligent autocompletion across your entire client (React components) and server (routing layers), drastically reducing runtime errors.
@@ -32,13 +32,13 @@ src/cli/
32
32
 
33
33
  ## Context Pattern
34
34
 
35
- Every context follows a three-file pattern:
35
+ Every context follows a strictly-typed three-file pattern to ensure type safety across the application:
36
36
 
37
37
  ```
38
38
  context/feature/
39
- ├── FeatureContext.jsx # Context definition
40
- ├── FeatureProvider.jsx # Provider component
41
- └── useFeatureContext.mjs # Hook for consuming
39
+ ├── FeatureContext.mjs # Context definition
40
+ ├── FeatureProvider.jsx # Provider component (exports JSDoc types)
41
+ └── useFeatureContext.mjs # Hook for consuming (returns JSDoc types)
42
42
  ```
43
43
 
44
44
  ### Context Definition
@@ -53,46 +53,33 @@ const SessionContext = createContext()
53
53
  export default SessionContext
54
54
  ```
55
55
 
56
- ### Provider Component
56
+ ### Provider Component with JSDoc Types
57
57
 
58
58
  **File:** `context/session/SessionProvider.jsx`
59
59
 
60
60
  ```javascript
61
61
  import { useState, useEffect } from 'react'
62
62
  import SessionContext from './SessionContext.mjs'
63
+ import User from '#ns/model/User.mjs'
64
+
65
+ /**
66
+ * Define the structure of your context data so consumers get autocomplete.
67
+ *
68
+ * @typedef {Object} SessionContextData
69
+ * @property {User} user
70
+ * @property {boolean} loading
71
+ * @property {boolean} isAuthenticated
72
+ * @property {Function} login
73
+ * @property {Function} logout
74
+ */
63
75
 
64
76
  export default function SessionProvider({ children }) {
65
77
  const [user, setUser] = useState(null)
66
78
  const [loading, setLoading] = useState(true)
67
79
 
68
- useEffect(() => {
69
- // Fetch current user
70
- fetch('/api/user/current')
71
- .then(res => res.json())
72
- .then(data => {
73
- setUser(data.user)
74
- setLoading(false)
75
- })
76
- }, [])
77
-
78
- const login = async (credentials) => {
79
- const res = await fetch('/api/user/login', {
80
- method: 'POST',
81
- headers: { 'Content-Type': 'application/json' },
82
- body: JSON.stringify(credentials)
83
- })
84
- const data = await res.json()
85
- if (data.ok) {
86
- setUser(data.user)
87
- }
88
- return data
89
- }
90
-
91
- const logout = async () => {
92
- await fetch('/api/user/logout', { method: 'POST' })
93
- setUser(null)
94
- }
80
+ // ... implementation ...
95
81
 
82
+ /** @type {SessionContextData} */
96
83
  const value = {
97
84
  user,
98
85
  loading,
@@ -109,7 +96,7 @@ export default function SessionProvider({ children }) {
109
96
  }
110
97
  ```
111
98
 
112
- ### Consumer Hook
99
+ ### Strictly Typed Consumer Hook
113
100
 
114
101
  **File:** `context/session/useSessionContext.mjs`
115
102
 
@@ -117,6 +104,15 @@ export default function SessionProvider({ children }) {
117
104
  import { useContext } from 'react'
118
105
  import SessionContext from './SessionContext.mjs'
119
106
 
107
+ /**
108
+ * Import the typedef from the Provider
109
+ * @typedef {import("./SessionProvider.jsx").SessionContextData} SessionContextData
110
+ */
111
+
112
+ /**
113
+ * The hook returns the typed context data
114
+ * @returns {SessionContextData}
115
+ */
120
116
  export default function useSessionContext() {
121
117
  const context = useContext(SessionContext)
122
118
 
@@ -134,6 +130,7 @@ export default function useSessionContext() {
134
130
  import useSessionContext from '#cli/context/session/useSessionContext.mjs'
135
131
 
136
132
  export default function Profile() {
133
+ // Full IDE autocomplete for `user`, `loading`, `logout`!
137
134
  const { user, loading, logout } = useSessionContext()
138
135
 
139
136
  if (loading) return <div>Loading...</div>
@@ -423,6 +423,7 @@ const loader = async (ctx) => {
423
423
  ## Related Skills
424
424
 
425
425
  - **miolo-session-context** - Access `useSsrData` from session context
426
+ - **miolo-caching** - Advanced caching, socket invalidation (`exclude_socket_id`), and lazy refreshing.
426
427
  - **miolo-fetcher** - Fetcher usage in remote loaders (future skill)
427
428
  - **miolo-react-patterns** - Context provider patterns
428
429
 
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "jsx": "react-jsx",
4
4
  "checkJs": false,
5
+ "lib": ["ES2022", "DOM"],
5
6
  "target": "ES6",
6
7
  "module": "ES6",
7
8
  "moduleResolution": "bundler",
@@ -46,9 +46,9 @@
46
46
  "intre": "^3.0.0-beta.4",
47
47
  "joi": "^18.2.3",
48
48
  "lucide-react": "^1.24.0",
49
- "miolo-cli": "^3.0.0-beta.227",
49
+ "miolo-cli": "^3.0.0-beta.229",
50
50
  "miolo-model": "file:../miolo-model",
51
- "miolo-react": "^3.0.0-beta.227",
51
+ "miolo-react": "^3.0.0-beta.229",
52
52
  "next-themes": "^0.4.6",
53
53
  "radix-ui": "^1.6.2",
54
54
  "react": "^19.2.7",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@biomejs/biome": "2.5.3",
66
- "miolo": "^3.0.0-beta.227",
66
+ "miolo": "^3.0.0-beta.229",
67
67
  "sass-embedded": "^1.100.0"
68
68
  },
69
69
  "overrides": {