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