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 CHANGED
@@ -2,22 +2,26 @@
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
+
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
- ## ✨ Core Idea
15
+ ## ✨ logic-runtime-react-z?
13
16
 
14
- > **Business logic lives outside React. React only renders state and emits intent.**
17
+ > **Intent is the only entry point.**
15
18
 
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
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
- middleware / effects
35
+ effects / middleware
32
36
 
33
- intent handlers
37
+ intent handlers
34
38
 
35
- mutate state
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 }) => setState(s => { s.count++ }))
59
- bus.on("add", ({ payload, setState }) => setState(s => { s.count += payload }))
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 in View)
93
+ ## ⚛️ React Integration (Type Inference, No Hooks)
94
+
95
+ ### Define Logic
73
96
 
74
97
  ```ts
75
- import { createLogic, effect, withLogic } from "logic-runtime-react-z"
98
+ // counter.logic.ts
99
+ import { createLogic, effect } from "logic-runtime-react-z"
76
100
 
77
- interface State {
78
- count: number;
79
- loading: boolean;
80
- double: number;
81
- }
101
+ export const counterLogic = createLogic({
102
+ name: "counter",
82
103
 
83
- // Async effect for takeLatest behavior
84
- const asyncEffect = effect(async ({ payload, setState }) => {
85
- console.log("Effect fired for payload:", payload);
86
- }).takeLatest()
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 }) => 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 })
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
- bus.effect("inc-async", asyncEffect)
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.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>
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 Counter = withLogic(counterLogic, CounterView)
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
- ## 🧪 Middleware Example (Backend)
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
- // Optional: attach devtools in dev mode
133
- const devtools = runtime.devtools
134
-
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
- })
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
- runtime.onIntent("logout", ({ setState }) => {
147
- setState(s => { s.user = null })
230
+ logout({ set }) {
231
+ set({ user: null })
232
+ },
148
233
  })
149
234
 
150
- // Emit some intents
151
- await runtime.emit("login", { username: "alice", password: "123" })
235
+ await runtime.emit("login")
152
236
  await runtime.emit("logout")
153
237
 
154
- // ----------------- Using devtools -----------------
155
-
156
- // 1️⃣ Access timeline records
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
- ## 🧪 Unit Test Example (Headless)
255
+ ## 🪝 Hooks API (Optional)
171
256
 
172
257
  ```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
- }
179
- })
258
+ // useActions
259
+ import { useActions } from "logic-runtime-react-z"
260
+ import { counterLogic } from "./counter.logic"
180
261
 
181
- const runtime = logic.create()
182
- await runtime.emit("set", 4)
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
- ## 🔍 Comparison
277
+ function DoubleValue() {
278
+ const double = useSelector(
279
+ counterLogic,
280
+ s => s.double
281
+ )
190
282
 
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 | ✅ | ⚠️ | ⚠️ | ⚠️ |
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
- ## ⚖️ Comparison with Vue2
293
+ return (
294
+ <button onClick={() => runtime.emit("inc")}>
295
+ Emit directly
296
+ </button>
297
+ )
298
+ }
205
299
 
206
- While logic-runtime-react-z uses a **reactive + computed pattern** similar to Vue2, the behavior is quite different:
300
+ ```
207
301
 
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 |
302
+ ---
217
303
 
218
- > **Takeaway:** It feels familiar if you know Vue2 reactivity, but under the hood it's **intent-first, headless, and fully testable**, unlike Vue2.
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
- ## 🚫 Anti-patterns (What NOT to do)
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
- #### ❌ 1. Putting business logic inside React components
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
- ✅ Correct
328
+ ---
249
329
 
250
- ```ts
251
- runtime.emit("login")
252
- ```
330
+ ## 🧪 Unit Test Example
253
331
 
254
332
  ```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
- ```
333
+ const logic = createLogic({
334
+ state: { value: 0 },
262
335
 
263
- #### ❌ 2. Calling handlers directly instead of emitting intent
264
- ```ts
265
- // ❌ Don't call handlers manually
266
- loginHandler(payload)
267
- ```
268
- Why this is wrong
336
+ computed: {
337
+ squared: ({ state }) => state.value * state.value,
338
+ },
269
339
 
270
- - Skips middleware & effects
271
- - Breaks devtools timeline
272
- - Makes behavior non-deterministic
340
+ intents: bus => {
341
+ bus.on("set", ({ payload, setState }) => {
342
+ setState(s => {
343
+ s.value = payload
344
+ })
345
+ })
346
+ },
347
+ })
273
348
 
274
- Correct
349
+ const runtime = logic.create()
275
350
 
276
- ```ts
277
- runtime.emit("login", payload)
278
- ```
279
- Intent is the only entry point. Always.
351
+ await runtime.emit("set", 4)
280
352
 
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
- })
353
+ expect(runtime.state.squared).toBe(16)
288
354
  ```
289
355
 
290
- Why this is wrong
356
+ ---
291
357
 
292
- - Effects are orchestration, not business logic
293
- - Hard to reason about ordering
294
- - Blurs responsibility
358
+ ## 🚫 Anti-patterns (What NOT to do)
295
359
 
296
- Correct
360
+ ### ❌ Business logic in React
297
361
 
298
- ```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
- 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("login:start")
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
- #### ❌ 5. Reading or mutating state outside the runtime
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
- #### ❌ 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
356
-
357
- - Behavior split across layers
358
- - Impossible to replay or test headlessly
388
+ ---
359
389
 
360
- Correct
390
+ ### ❌ Generic Redux-style intents
361
391
 
362
392
  ```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
- })
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
- composeLogic(
389
- userLogic,
390
- cartLogic,
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>;
@@ -1,6 +1,30 @@
1
- import type { Effect, EffectCtx } from "./types";
2
- export declare function composeEffects<W, R>(effects: Effect<W, R>[]): Effect<W, R>;
3
- export declare function takeLatest<W, R>(): Effect<W, R>;
4
- export declare function debounce<W, R>(ms: number): Effect<W, R>;
5
- export declare function retry<W, R>(count?: number): Effect<W, R>;
6
- export declare function effect<W extends object, R extends object = W, P = any>(body: (ctx: EffectCtx<W, R, P>) => void | Promise<void>): any;
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
+ };