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 +1 -1
- package/template/.agent/skills/miolo-caching/SKILL.md +88 -0
- package/template/.agent/skills/miolo-model/SKILL.md +143 -0
- package/template/.agent/skills/miolo-react-patterns/SKILL.md +30 -33
- package/template/.agent/skills/miolo-ssr/SKILL.md +1 -0
- package/template/jsconfig.json +1 -0
- package/template/package.json +3 -3
package/package.json
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
package/template/jsconfig.json
CHANGED
package/template/package.json
CHANGED
|
@@ -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.
|
|
49
|
+
"miolo-cli": "^3.0.0-beta.229",
|
|
50
50
|
"miolo-model": "file:../miolo-model",
|
|
51
|
-
"miolo-react": "^3.0.0-beta.
|
|
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.
|
|
66
|
+
"miolo": "^3.0.0-beta.229",
|
|
67
67
|
"sass-embedded": "^1.100.0"
|
|
68
68
|
},
|
|
69
69
|
"overrides": {
|