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 CHANGED
@@ -1,23 +1,26 @@
1
- # ⚙️ logic-runtime-react-z
1
+ # 🧩 logic-runtime-react-z
2
2
 
3
3
  [![NPM](https://img.shields.io/npm/v/logic-runtime-react-z.svg)](https://www.npmjs.com/package/logic-runtime-react-z) ![Downloads](https://img.shields.io/npm/dt/logic-runtime-react-z.svg)
4
4
 
5
- <a href="https://codesandbox.io/p/sandbox/x3jf32" target="_blank">LIVE EXAMPLE</a>
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
- ## ✨ Core Idea
14
+ ---
13
15
 
14
- > **Business logic lives outside React. React only renders state and emits intent.**
16
+ ## Why logic-runtime-react-z?
15
17
 
16
- * No React hooks in views
17
- * Intent is the only entry point
18
- * Predictable async flows
19
- * Headless & backend-friendly
20
- * Fully testable without rendering
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
- middleware / effects
34
+ effects / middleware
32
35
 
33
- intent handlers
36
+ intent handlers
34
37
 
35
- mutate state
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 }) => setState(s => { s.count++ }))
59
- bus.on("add", ({ payload, setState }) => setState(s => { s.count += payload }))
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 in View)
92
+ ## ⚛️ React Integration (No Hooks)
93
+
94
+ ### Define Logic
73
95
 
74
96
  ```ts
75
- import { createLogic, effect, withLogic } from "logic-runtime-react-z"
97
+ // counter.logic.ts
98
+ import { createLogic, effect } from "logic-runtime-react-z"
76
99
 
77
- interface State {
78
- count: number;
79
- loading: boolean;
80
- double: number;
81
- }
100
+ export const counterLogic = createLogic({
101
+ name: "counter",
82
102
 
83
- // Async effect for takeLatest behavior
84
- const asyncEffect = effect(async ({ payload, setState }) => {
85
- console.log("Effect fired for payload:", payload);
86
- }).takeLatest()
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 }) => setState(s => { s.count++ }))
94
- bus.on("inc-async", async ({ payload, setState }) => {
95
- setState(s => { s.loading = true })
96
- await new Promise(r => setTimeout(r, 5000))
97
- setState(s => { s.count += payload; s.loading = false })
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
- bus.effect("inc-async", asyncEffect)
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.count}</div>
108
- <button disabled={state.loading} onClick={() => emit("inc")}>Plus</button>
109
- <button disabled={state.loading} onClick={() => emit("inc-async", 100)}>Async +100</button>
110
- <div>Double: {state.double}</div>
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 Counter = withLogic(counterLogic, CounterView)
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
- ## 🧪 Middleware Example (Backend)
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
- // Optional: attach devtools in dev mode
133
- const devtools = runtime.devtools
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
- // Register some intents
136
- runtime.onIntent("login", async ({ payload, setState }) => {
137
- setState(s => { s.loading = true })
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.onIntent("logout", ({ setState }) => {
147
- setState(s => { s.user = null })
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
- // Emit some intents
151
- await runtime.emit("login", { username: "alice", password: "123" })
152
- await runtime.emit("logout")
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
- // ----------------- Using devtools -----------------
272
+ // useLogicSelector
273
+ import { useLogicSelector } from "logic-runtime-react-z"
274
+ import { counterLogic } from "./counter.logic"
155
275
 
156
- // 1️⃣ Access timeline records
157
- console.log("Timeline records:", devtools.timeline.records)
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
- // 2️⃣ Replay intents
160
- await devtools.timeline.replay(runtime.emit, { scope: "backend" })
285
+ // useRuntime
286
+ import { useRuntime } from "logic-runtime-react-z"
287
+ import { counterLogic } from "./counter.logic"
161
288
 
162
- // 3️⃣ Clear timeline
163
- devtools.timeline.clear()
164
- console.log("Timeline cleared:", devtools.timeline.records)
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
- ## 🧪 Unit Test Example (Headless)
303
+ ## 🧱 Composing Multiple Logic Modules
171
304
 
172
305
  ```ts
173
- const logic = createLogic({
174
- state: { value: 0 },
175
- computed: { squared: ({ state }) => state.value * state.value },
176
- intents: bus => {
177
- bus.on("set", ({ payload, setState }) => setState(s => { s.value = payload }))
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
- const runtime = logic.create()
182
- await runtime.emit("set", 4)
183
- expect(runtime.state.squared).toBe(16)
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
- ## 🔍 Comparison
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
- ## ⚖️ Comparison with Vue2
339
+ intents: bus => {
340
+ bus.on("set", ({ payload, setState }) => {
341
+ setState(s => {
342
+ s.value = payload
343
+ })
344
+ })
345
+ },
346
+ })
205
347
 
206
- While logic-runtime-react-z uses a **reactive + computed pattern** similar to Vue2, the behavior is quite different:
348
+ const runtime = logic.create()
207
349
 
208
- | Feature | Vue2 | logic-runtime-react-z |
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
- > **Takeaway:** It feels familiar if you know Vue2 reactivity, but under the hood it's **intent-first, headless, and fully testable**, unlike Vue2.
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
- 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
-
227
-
228
- #### ❌ 1. Putting business logic inside React components
359
+ ### Business logic in React
229
360
 
230
361
  ```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
- }
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
- runtime.emit("login")
370
+ emit("data:fetch")
252
371
  ```
253
372
 
254
- ```ts
255
- bus.on("login", async ({ setState, emit }) => {
256
- setState(s => { s.loading = true })
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
- // Don't call handlers manually
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
- runtime.emit("login", payload)
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
- Why this is wrong
291
-
292
- - Effects are orchestration, not business logic
293
- - Hard to reason about ordering
294
- - Blurs responsibility
387
+ ---
295
388
 
296
- Correct
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
- #### ❌ 5. Reading or mutating state outside the runtime
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
- Correct
405
+ ## 🧩 When to Use This
341
406
 
342
- ```ts
343
- emit("update:user:name", "admin")
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
- #### 6. Using React hooks to replace runtime behavior
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
- - Behavior split across layers
358
- - Impossible to replay or test headlessly
414
+ - Simple local UI state
415
+ - Throwaway components
359
416
 
360
- ✅ Correct
417
+ ---
361
418
 
362
- ```ts
363
- bus.on("login:success", async ({ emit }) => {
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
- })
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
- ✅ Correct
436
+ ```bash
437
+ Redux / Zustand:
438
+ UI → setState → store → re-render
386
439
 
387
- ```ts
388
- composeLogic(
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
- ## 📜 License
450
+ ## License
398
451
 
399
- MIT / Delpi
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>;