logic-runtime-react-z 2.0.2 → 3.0.0
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/README.md +265 -242
- package/build/core/applyMiddleware.d.ts +3 -0
- package/build/core/effect.d.ts +30 -6
- package/build/core/effectMiddleware.d.ts +2 -0
- package/build/core/intentBus.d.ts +13 -0
- package/build/core/middleware.d.ts +3 -2
- package/build/core/runtime.d.ts +32 -0
- package/build/core/types.d.ts +6 -35
- package/build/devtools/devtools.d.ts +8 -6
- package/build/devtools/index.d.ts +1 -1
- package/build/index.cjs.js +1 -1
- package/build/index.d.ts +11 -15
- package/build/index.esm.js +1 -1
- package/build/logic/composeLogic.d.ts +16 -0
- package/build/logic/createBackendRuntime.d.ts +17 -0
- package/build/logic/createLogic.d.ts +28 -0
- package/build/react/useActions.d.ts +5 -0
- package/build/react/useRuntime.d.ts +4 -0
- package/build/react/useSelector.d.ts +2 -0
- package/build/react/withLogic.d.ts +6 -10
- package/package.json +31 -25
- package/build/core/index.d.ts +0 -3
- package/build/core/intent.d.ts +0 -11
- package/build/plugins/index.d.ts +0 -5
- package/build/runtime/backend.d.ts +0 -29
- package/build/runtime/compose.d.ts +0 -21
- package/build/runtime/index.d.ts +0 -3
- package/build/runtime/logic.d.ts +0 -21
- package/build/state/computed.d.ts +0 -10
- package/build/state/index.d.ts +0 -4
- package/build/state/signal.d.ts +0 -6
- package/build/state/store.d.ts +0 -6
- /package/build/{state → react}/selector.d.ts +0 -0
package/README.md
CHANGED
|
@@ -2,22 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/logic-runtime-react-z) 
|
|
4
4
|
|
|
5
|
-
<a href="https://codesandbox.io/p/sandbox/
|
|
5
|
+
<a href="https://codesandbox.io/p/sandbox/jnd992" target="_blank">LIVE EXAMPLE</a>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
React is a view
|
|
7
|
+
|
|
8
|
+
**Intent-first business logic runtime**: React is a view - Logic lives elsewhere.
|
|
9
|
+
|
|
10
|
+
> A headless, deterministic, intent-driven runtime for frontend & backend logic.
|
|
11
|
+
> React components stay pure. Business logic is fully testable and replayable.
|
|
9
12
|
|
|
10
13
|
---
|
|
11
14
|
|
|
12
|
-
## ✨
|
|
15
|
+
## ✨ logic-runtime-react-z?
|
|
13
16
|
|
|
14
|
-
> **
|
|
17
|
+
> **Intent is the only entry point.**
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
- No React hooks in views
|
|
20
|
+
- Intent is the *only* entry point
|
|
21
|
+
- Predictable async flows
|
|
22
|
+
- Computed graph with caching
|
|
23
|
+
- Headless & backend-friendly
|
|
24
|
+
- Deterministic testing & devtools replay
|
|
21
25
|
|
|
22
26
|
---
|
|
23
27
|
|
|
@@ -26,17 +30,19 @@ React is a view. Logic lives elsewhere.
|
|
|
26
30
|
```
|
|
27
31
|
UI / HTTP / Queue / Cron
|
|
28
32
|
↓
|
|
29
|
-
emit(intent)
|
|
33
|
+
emit(intent)
|
|
30
34
|
↓
|
|
31
|
-
|
|
35
|
+
effects / middleware
|
|
32
36
|
↓
|
|
33
|
-
|
|
37
|
+
intent handlers
|
|
34
38
|
↓
|
|
35
|
-
|
|
39
|
+
mutate state
|
|
36
40
|
↓
|
|
37
|
-
computed / subscribers
|
|
41
|
+
computed / subscribers
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
Think **events → behavior → state → derived state**.
|
|
45
|
+
|
|
40
46
|
---
|
|
41
47
|
|
|
42
48
|
## 📦 Installation
|
|
@@ -54,288 +60,324 @@ import { createLogic } from "logic-runtime-react-z"
|
|
|
54
60
|
|
|
55
61
|
const counterLogic = createLogic({
|
|
56
62
|
state: { count: 0 },
|
|
63
|
+
|
|
57
64
|
intents: bus => {
|
|
58
|
-
bus.on("inc", ({ setState }) =>
|
|
59
|
-
|
|
65
|
+
bus.on("inc", ({ setState }) => {
|
|
66
|
+
setState(s => {
|
|
67
|
+
s.count++
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
bus.on<number>("add", ({ payload, setState }) => {
|
|
72
|
+
setState(s => {
|
|
73
|
+
s.count += payload
|
|
74
|
+
})
|
|
75
|
+
})
|
|
60
76
|
},
|
|
61
77
|
})
|
|
62
78
|
|
|
63
79
|
const runtime = counterLogic.create()
|
|
80
|
+
|
|
64
81
|
await runtime.emit("inc")
|
|
65
82
|
await runtime.emit("add", 5)
|
|
66
|
-
console.log(runtime.state.count) // 6
|
|
67
83
|
|
|
84
|
+
console.log(runtime.state.count) // 6
|
|
68
85
|
```
|
|
69
86
|
|
|
87
|
+
✔ No UI
|
|
88
|
+
✔ Fully testable
|
|
89
|
+
✔ Deterministic
|
|
90
|
+
|
|
70
91
|
---
|
|
71
92
|
|
|
72
|
-
## ⚛️ React Integration (No Hooks
|
|
93
|
+
## ⚛️ React Integration (Type Inference, No Hooks)
|
|
94
|
+
|
|
95
|
+
### Define Logic
|
|
73
96
|
|
|
74
97
|
```ts
|
|
75
|
-
|
|
98
|
+
// counter.logic.ts
|
|
99
|
+
import { createLogic, effect } from "logic-runtime-react-z"
|
|
76
100
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
loading: boolean;
|
|
80
|
-
double: number;
|
|
81
|
-
}
|
|
101
|
+
export const counterLogic = createLogic({
|
|
102
|
+
name: "counter",
|
|
82
103
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
104
|
+
state: {
|
|
105
|
+
count: 1,
|
|
106
|
+
loading: false,
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
computed: {
|
|
110
|
+
double: ({ state }) => state.count * 2,
|
|
111
|
+
tripple: ({ state }) => state.count * 3,
|
|
112
|
+
},
|
|
87
113
|
|
|
88
|
-
const counterLogic = createLogic({
|
|
89
|
-
name: "counter",
|
|
90
|
-
state: { count: 1, loading: false },
|
|
91
|
-
computed: { double: ({ state }) => state.count * 2 },
|
|
92
114
|
intents: bus => {
|
|
93
|
-
bus.on("inc", ({ setState }) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
bus.on("inc", ({ setState }) => {
|
|
116
|
+
setState(s => {
|
|
117
|
+
s.count++
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
bus.on<number>("add", ({ payload, setState }) => {
|
|
122
|
+
setState(s => {
|
|
123
|
+
s.count += payload
|
|
124
|
+
})
|
|
98
125
|
})
|
|
99
|
-
|
|
126
|
+
|
|
127
|
+
bus.on<number>("inc-async", async ({ payload, setState }) => {
|
|
128
|
+
setState(s => {
|
|
129
|
+
s.loading = true
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
133
|
+
|
|
134
|
+
setState(s => {
|
|
135
|
+
s.count += payload
|
|
136
|
+
s.loading = false
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
bus.effect(
|
|
141
|
+
"inc-async",
|
|
142
|
+
effect(async ({ payload }) => {
|
|
143
|
+
console.log("effect run, payload =", payload)
|
|
144
|
+
}).takeLatest()
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
actions: {
|
|
149
|
+
inc({ emit }) {
|
|
150
|
+
return () => emit("inc")
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
add({ emit }) {
|
|
154
|
+
return (n: number) => emit("add", n)
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
incAsync({ emit }) {
|
|
158
|
+
return (n: number) => emit("inc-async", n)
|
|
159
|
+
},
|
|
100
160
|
},
|
|
101
161
|
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### Pure React View (No Types Needed)
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import React from "react"
|
|
170
|
+
import { withLogic } from "logic-runtime-react-z"
|
|
171
|
+
import { counterLogic } from "./counter.logic"
|
|
172
|
+
|
|
173
|
+
function CounterView(props: any) {
|
|
174
|
+
const { state, actions, emit } = props
|
|
102
175
|
|
|
103
|
-
// React view (pure, no hooks)
|
|
104
|
-
function CounterView({ state, emit }: { state: State; emit: (intent: string, payload?: any) => void | Promise<void> }) {
|
|
105
176
|
return (
|
|
106
|
-
<div>
|
|
107
|
-
<div>Count: {state.
|
|
108
|
-
|
|
109
|
-
<button
|
|
110
|
-
<
|
|
177
|
+
<div style={{ padding: 12 }}>
|
|
178
|
+
<div>Count: {state.tripple}</div>
|
|
179
|
+
|
|
180
|
+
<button onClick={actions.inc}>+1 (action)</button>
|
|
181
|
+
<button onClick={() => actions.add(10)}>+10 (action)</button>
|
|
182
|
+
|
|
183
|
+
<button
|
|
184
|
+
disabled={state.loading}
|
|
185
|
+
onClick={() => actions.incAsync(5)}
|
|
186
|
+
>
|
|
187
|
+
Async +5
|
|
188
|
+
</button>
|
|
189
|
+
|
|
190
|
+
<hr />
|
|
191
|
+
|
|
192
|
+
<button onClick={() => emit("inc")}>
|
|
193
|
+
+1 (emit directly)
|
|
194
|
+
</button>
|
|
111
195
|
</div>
|
|
112
196
|
)
|
|
113
197
|
}
|
|
114
198
|
|
|
115
|
-
export const
|
|
116
|
-
|
|
199
|
+
export const CounterPage =
|
|
200
|
+
withLogic(counterLogic, CounterView)
|
|
117
201
|
```
|
|
118
202
|
|
|
203
|
+
✔ Props inferred automatically
|
|
204
|
+
✔ No generics
|
|
205
|
+
✔ No interfaces
|
|
206
|
+
✔ View stays dumb
|
|
207
|
+
|
|
119
208
|
---
|
|
120
209
|
|
|
121
|
-
## 🧪
|
|
210
|
+
## 🧪 Backend Runtime Example
|
|
122
211
|
|
|
123
212
|
```ts
|
|
124
213
|
import { createBackendRuntime } from "logic-runtime-react-z"
|
|
125
214
|
|
|
126
|
-
// Create runtime with initial state
|
|
127
215
|
const runtime = createBackendRuntime({
|
|
128
216
|
user: null,
|
|
129
217
|
loading: false,
|
|
130
218
|
})
|
|
131
219
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
s.user = user
|
|
142
|
-
s.loading = false
|
|
143
|
-
})
|
|
144
|
-
})
|
|
220
|
+
runtime.registerIntents({
|
|
221
|
+
async login({ set }) {
|
|
222
|
+
set({ loading: true })
|
|
223
|
+
await new Promise(r => setTimeout(r, 500))
|
|
224
|
+
set({
|
|
225
|
+
user: { name: "Alice" },
|
|
226
|
+
loading: false,
|
|
227
|
+
})
|
|
228
|
+
},
|
|
145
229
|
|
|
146
|
-
|
|
147
|
-
|
|
230
|
+
logout({ set }) {
|
|
231
|
+
set({ user: null })
|
|
232
|
+
},
|
|
148
233
|
})
|
|
149
234
|
|
|
150
|
-
|
|
151
|
-
await runtime.emit("login", { username: "alice", password: "123" })
|
|
235
|
+
await runtime.emit("login")
|
|
152
236
|
await runtime.emit("logout")
|
|
153
237
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
console.log("Timeline records:", devtools.timeline.records)
|
|
158
|
-
|
|
159
|
-
// 2️⃣ Replay intents
|
|
160
|
-
await devtools.timeline.replay(runtime.emit, { scope: "backend" })
|
|
161
|
-
|
|
162
|
-
// 3️⃣ Clear timeline
|
|
163
|
-
devtools.timeline.clear()
|
|
164
|
-
console.log("Timeline cleared:", devtools.timeline.records)
|
|
238
|
+
// 👇 backend devtools
|
|
239
|
+
const devtools = runtime.devtools
|
|
240
|
+
console.log(devtools.timeline.records)
|
|
165
241
|
|
|
242
|
+
// relay
|
|
243
|
+
// await devtools.timeline.replay(runtime.emit, {
|
|
244
|
+
// scope: "backend"
|
|
245
|
+
})
|
|
166
246
|
```
|
|
167
247
|
|
|
248
|
+
✔ Same intent model
|
|
249
|
+
✔ No React
|
|
250
|
+
✔ Replayable
|
|
251
|
+
✔ Devtools is backend-first.
|
|
252
|
+
|
|
168
253
|
---
|
|
169
254
|
|
|
170
|
-
##
|
|
255
|
+
## 🪝 Hooks API (Optional)
|
|
171
256
|
|
|
172
257
|
```ts
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
intents: bus => {
|
|
177
|
-
bus.on("set", ({ payload, setState }) => setState(s => { s.value = payload }))
|
|
178
|
-
}
|
|
179
|
-
})
|
|
258
|
+
// useActions
|
|
259
|
+
import { useActions } from "logic-runtime-react-z"
|
|
260
|
+
import { counterLogic } from "./counter.logic"
|
|
180
261
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
expect(runtime.state.squared).toBe(16)
|
|
262
|
+
function Buttons() {
|
|
263
|
+
const actions = useActions(counterLogic)
|
|
184
264
|
|
|
185
|
-
|
|
265
|
+
return (
|
|
266
|
+
<>
|
|
267
|
+
<button onClick={actions.inc}>+1</button>
|
|
268
|
+
<button onClick={() => actions.add(5)}>+5</button>
|
|
269
|
+
</>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
186
272
|
|
|
187
|
-
|
|
273
|
+
// useSelector
|
|
274
|
+
import { useSelector } from "logic-runtime-react-z"
|
|
275
|
+
import { counterLogic } from "./counter.logic"
|
|
188
276
|
|
|
189
|
-
|
|
277
|
+
function DoubleValue() {
|
|
278
|
+
const double = useSelector(
|
|
279
|
+
counterLogic,
|
|
280
|
+
s => s.double
|
|
281
|
+
)
|
|
190
282
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
| Intent-first | ✅ | ❌ | ❌ | ❌ |
|
|
194
|
-
| Headless / backend-friendly | ✅ | ⚠️ | ⚠️ | ❌ |
|
|
195
|
-
| Async orchestration | ✅ (takeLatest, debounce) | ⚠️ (middleware add ) | ⚠️ | ⚠️ |
|
|
196
|
-
| Computed graph | ✅ | ❌ | ❌ | ✅ (atom deps) |
|
|
197
|
-
| Devtools replay async | ✅ | ⚠️ | ❌ | ⚠️ |
|
|
198
|
-
| UI-agnostic | ✅ | ⚠️ | ⚠️ | ❌ |
|
|
199
|
-
| Deterministic testability | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
|
283
|
+
return <div>Double: {double}</div>
|
|
284
|
+
}
|
|
200
285
|
|
|
286
|
+
// useRuntime
|
|
287
|
+
import { useRuntime } from "logic-runtime-react-z"
|
|
288
|
+
import { counterLogic } from "./counter.logic"
|
|
201
289
|
|
|
202
|
-
|
|
290
|
+
function DebugPanel() {
|
|
291
|
+
const runtime = useRuntime(counterLogic)
|
|
203
292
|
|
|
204
|
-
|
|
293
|
+
return (
|
|
294
|
+
<button onClick={() => runtime.emit("inc")}>
|
|
295
|
+
Emit directly
|
|
296
|
+
</button>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
205
299
|
|
|
206
|
-
|
|
300
|
+
```
|
|
207
301
|
|
|
208
|
-
|
|
209
|
-
|---------------------------|----------------------- |---------------------------------------------------- |
|
|
210
|
-
| Reactive base state | ✅ proxy | ✅ store + computed tracking. |
|
|
211
|
-
| Computed | ✅ | ✅ dependency tracking + invalidation. |
|
|
212
|
-
| Intent-driven flow | ❌ | ✅ all actions go through `emit(intent)`. |
|
|
213
|
-
| Async orchestration | ❌ | ✅ effects + middleware (takeLatest, debounce, etc.) |
|
|
214
|
-
| Headless / backend-ready | ❌ | ✅ can run without React/UI |
|
|
215
|
-
| Deterministic testing | ❌ | ✅ full headless tests possible |
|
|
216
|
-
| Devtools replay | ❌ | ✅ timeline tracking & replay |
|
|
302
|
+
---
|
|
217
303
|
|
|
218
|
-
|
|
304
|
+
## 🧱 Composing Multiple Logic Modules
|
|
219
305
|
|
|
220
|
-
|
|
306
|
+
```ts
|
|
307
|
+
import { composeLogic } from "logic-runtime-react-z"
|
|
308
|
+
import { userLogic } from "./user.logic"
|
|
309
|
+
import { cartLogic } from "./cart.logic"
|
|
221
310
|
|
|
222
|
-
|
|
311
|
+
export const appLogic = composeLogic({
|
|
312
|
+
user: userLogic,
|
|
313
|
+
cart: cartLogic,
|
|
314
|
+
})
|
|
223
315
|
|
|
224
|
-
This library enforces a **clear separation between intent, behavior, and view**.
|
|
225
|
-
If you find yourself doing the following, you are probably fighting the architecture.
|
|
226
316
|
|
|
317
|
+
// usage
|
|
318
|
+
const runtime = appLogic.create()
|
|
227
319
|
|
|
228
|
-
|
|
320
|
+
await runtime.emit("user:login", credentials)
|
|
321
|
+
|
|
322
|
+
const snapshot = runtime.getSnapshot()
|
|
323
|
+
snapshot.user // user state
|
|
324
|
+
snapshot.cart // cart state
|
|
229
325
|
|
|
230
|
-
```tsx
|
|
231
|
-
// ❌ Don't do this
|
|
232
|
-
function Login() {
|
|
233
|
-
const [loading, setLoading] = useState(false)
|
|
234
|
-
|
|
235
|
-
async function handleLogin() {
|
|
236
|
-
setLoading(true)
|
|
237
|
-
const user = await api.login()
|
|
238
|
-
setLoading(false)
|
|
239
|
-
navigate("/home")
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
326
|
```
|
|
243
|
-
Why this is wrong
|
|
244
|
-
- Logic tied to React lifecycle
|
|
245
|
-
- Hard to test without rendering
|
|
246
|
-
- Side-effects scattered in UI
|
|
247
327
|
|
|
248
|
-
|
|
328
|
+
---
|
|
249
329
|
|
|
250
|
-
|
|
251
|
-
runtime.emit("login")
|
|
252
|
-
```
|
|
330
|
+
## 🧪 Unit Test Example
|
|
253
331
|
|
|
254
332
|
```ts
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const user = await api.login()
|
|
258
|
-
setState(s => { s.loading = false })
|
|
259
|
-
emit("login:success", user)
|
|
260
|
-
})
|
|
261
|
-
```
|
|
333
|
+
const logic = createLogic({
|
|
334
|
+
state: { value: 0 },
|
|
262
335
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
loginHandler(payload)
|
|
267
|
-
```
|
|
268
|
-
Why this is wrong
|
|
336
|
+
computed: {
|
|
337
|
+
squared: ({ state }) => state.value * state.value,
|
|
338
|
+
},
|
|
269
339
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
340
|
+
intents: bus => {
|
|
341
|
+
bus.on("set", ({ payload, setState }) => {
|
|
342
|
+
setState(s => {
|
|
343
|
+
s.value = payload
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
},
|
|
347
|
+
})
|
|
273
348
|
|
|
274
|
-
|
|
349
|
+
const runtime = logic.create()
|
|
275
350
|
|
|
276
|
-
|
|
277
|
-
runtime.emit("login", payload)
|
|
278
|
-
```
|
|
279
|
-
Intent is the only entry point. Always.
|
|
351
|
+
await runtime.emit("set", 4)
|
|
280
352
|
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
// ❌ Effect mutating state
|
|
284
|
-
bus.effect("save", next => async ctx => {
|
|
285
|
-
ctx.setState(s => { s.saving = true })
|
|
286
|
-
await next(ctx)
|
|
287
|
-
})
|
|
353
|
+
expect(runtime.state.squared).toBe(16)
|
|
288
354
|
```
|
|
289
355
|
|
|
290
|
-
|
|
356
|
+
---
|
|
291
357
|
|
|
292
|
-
|
|
293
|
-
- Hard to reason about ordering
|
|
294
|
-
- Blurs responsibility
|
|
358
|
+
## 🚫 Anti-patterns (What NOT to do)
|
|
295
359
|
|
|
296
|
-
|
|
360
|
+
### ❌ Business logic in React
|
|
297
361
|
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
})
|
|
302
|
-
```
|
|
303
|
-
Effects should only:
|
|
304
|
-
- debounce
|
|
305
|
-
- retry
|
|
306
|
-
- cancel
|
|
307
|
-
- log
|
|
308
|
-
- trace
|
|
309
|
-
|
|
310
|
-
#### ❌ 4. Treating intent like Redux actions
|
|
311
|
-
```ts
|
|
312
|
-
// ❌ Generic, meaningless intent
|
|
313
|
-
emit("SET_STATE", { loading: true })
|
|
362
|
+
```tsx
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
fetchData()
|
|
365
|
+
}, [])
|
|
314
366
|
```
|
|
315
367
|
|
|
316
|
-
Why this is wrong
|
|
317
|
-
|
|
318
|
-
- Intent should describe user or system intention
|
|
319
|
-
- Not raw state mutation
|
|
320
|
-
|
|
321
368
|
✅ Correct
|
|
322
369
|
|
|
323
370
|
```ts
|
|
324
|
-
emit("
|
|
325
|
-
emit("login:success", user)
|
|
326
|
-
emit("login:failed", error)
|
|
371
|
+
emit("data:fetch")
|
|
327
372
|
```
|
|
328
|
-
Intents are verbs, not patches.
|
|
329
373
|
|
|
330
|
-
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### ❌ Mutating state directly
|
|
377
|
+
|
|
331
378
|
```ts
|
|
332
|
-
// ❌ External mutation
|
|
333
379
|
runtime.state.user.name = "admin"
|
|
334
380
|
```
|
|
335
|
-
Why this is wrong
|
|
336
|
-
- Breaks computed cache
|
|
337
|
-
- Bypasses subscriptions
|
|
338
|
-
- Devtools become unreliable
|
|
339
381
|
|
|
340
382
|
✅ Correct
|
|
341
383
|
|
|
@@ -343,57 +385,38 @@ Why this is wrong
|
|
|
343
385
|
emit("update:user:name", "admin")
|
|
344
386
|
```
|
|
345
387
|
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
// ❌ useEffect as orchestration
|
|
349
|
-
useEffect(() => {
|
|
350
|
-
if (state.loggedIn) {
|
|
351
|
-
fetchProfile()
|
|
352
|
-
}
|
|
353
|
-
}, [state.loggedIn])
|
|
354
|
-
```
|
|
355
|
-
Why this is wrong
|
|
356
|
-
|
|
357
|
-
- Behavior split across layers
|
|
358
|
-
- Impossible to replay or test headlessly
|
|
388
|
+
---
|
|
359
389
|
|
|
360
|
-
|
|
390
|
+
### ❌ Generic Redux-style intents
|
|
361
391
|
|
|
362
392
|
```ts
|
|
363
|
-
|
|
364
|
-
await emit("profile:fetch")
|
|
365
|
-
})
|
|
366
|
-
```
|
|
367
|
-
#### ❌ 7. One logic runtime doing everything
|
|
368
|
-
```ts
|
|
369
|
-
// ❌ God runtime
|
|
370
|
-
createLogic({
|
|
371
|
-
state: {
|
|
372
|
-
user: {},
|
|
373
|
-
cart: {},
|
|
374
|
-
products: {},
|
|
375
|
-
settings: {},
|
|
376
|
-
ui: {},
|
|
377
|
-
}
|
|
378
|
-
})
|
|
393
|
+
emit("SET_STATE", { loading: true })
|
|
379
394
|
```
|
|
380
|
-
Why this is wrong
|
|
381
|
-
- No ownership boundaries
|
|
382
|
-
- Hard to compose
|
|
383
|
-
- Does not scale
|
|
384
395
|
|
|
385
396
|
✅ Correct
|
|
386
397
|
|
|
387
398
|
```ts
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
productLogic
|
|
392
|
-
)
|
|
399
|
+
emit("login:start")
|
|
400
|
+
emit("login:success", user)
|
|
401
|
+
emit("login:failed", error)
|
|
393
402
|
```
|
|
394
403
|
|
|
395
404
|
---
|
|
396
405
|
|
|
406
|
+
## 🧩 When to Use This
|
|
407
|
+
|
|
408
|
+
- Complex async flows
|
|
409
|
+
- Shared logic across UI / backend
|
|
410
|
+
- Need deterministic tests
|
|
411
|
+
- Want to remove logic from React
|
|
412
|
+
|
|
413
|
+
## 🚫 When NOT to Use
|
|
414
|
+
|
|
415
|
+
- Simple local UI state
|
|
416
|
+
- Throwaway components
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
397
420
|
## 📜 License
|
|
398
421
|
|
|
399
422
|
MIT / Delpi
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { IntentMiddleware } from "./middleware";
|
|
2
|
+
import { RuntimeIntentContext } from "./types";
|
|
3
|
+
export declare function applyMiddleware<S>(middlewares: IntentMiddleware<S>[], final: (context: RuntimeIntentContext<S>) => Promise<void>): (context: RuntimeIntentContext<S>) => Promise<void>;
|
package/build/core/effect.d.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export type EffectStrategy = "default" | "takeLatest" | "debounce";
|
|
2
|
+
export type EffectHandler<S = any> = (context: S) => void | Promise<void>;
|
|
3
|
+
export type EffectDef<S = any> = {
|
|
4
|
+
_kind: "effect";
|
|
5
|
+
handler: EffectHandler<S>;
|
|
6
|
+
strategy: EffectStrategy;
|
|
7
|
+
wait: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function effect<S = any>(fn: EffectHandler<S>): {
|
|
10
|
+
takeLatest(): {
|
|
11
|
+
takeLatest(): any;
|
|
12
|
+
debounce(ms: number): any;
|
|
13
|
+
_kind: "effect";
|
|
14
|
+
handler: EffectHandler<S>;
|
|
15
|
+
strategy: EffectStrategy;
|
|
16
|
+
wait: number;
|
|
17
|
+
};
|
|
18
|
+
debounce(ms: number): {
|
|
19
|
+
takeLatest(): any;
|
|
20
|
+
debounce(ms: number): any;
|
|
21
|
+
_kind: "effect";
|
|
22
|
+
handler: EffectHandler<S>;
|
|
23
|
+
strategy: EffectStrategy;
|
|
24
|
+
wait: number;
|
|
25
|
+
};
|
|
26
|
+
_kind: "effect";
|
|
27
|
+
handler: EffectHandler<S>;
|
|
28
|
+
strategy: EffectStrategy;
|
|
29
|
+
wait: number;
|
|
30
|
+
};
|