logic-runtime-react-z 1.0.0 → 2.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
@@ -1,215 +1,323 @@
1
- ## 🔀 logic-runtime-react-z
1
+
2
+ # ⚙️ logic-runtime-react-z
2
3
 
3
4
  [![NPM](https://img.shields.io/npm/v/logic-runtime-react-z.svg)](https://www.npmjs.com/package/logic-runtime-react-z)
4
5
  ![Downloads](https://img.shields.io/npm/dt/logic-runtime-react-z.svg)
5
6
 
6
- <a href="https://codesandbox.io/p/sandbox/x3jf32" target="_blank">LIVE EXAMPLE</a>
7
-
8
- ---
9
-
10
- **logic-runtime-react-z** is an intent-first external runtime,
11
- designed to run business logic **outside React**.
7
+ **logic-runtime-react-z** is an **intent-first business logic runtime**
8
+ designed to run **outside React**, with React acting purely as a view layer.
12
9
 
13
- React 18+ is required **only for the React bindings**.
14
- It provides a headless runtime layer that lives outside React, responsible for:
15
-
16
- > Think of it as:
17
- > **“Business logic runtime outside React — React is just the view.”**
10
+ > **Business logic lives outside React.
11
+ > React only renders state and emits intent.**
18
12
 
19
13
  ---
20
14
 
21
15
  ## ✨ Why / When to Use
22
- - You want zero hooks in UI components
23
- - Business logic should not live in React
24
- - UI should only emit intent, not orchestrate behavior
16
+
17
+ Use this library when:
18
+
19
+ - You want **zero hooks inside UI components**
20
+ - Business logic must not depend on React lifecycle
21
+ - UI should **emit intent**, not orchestrate behavior
25
22
  - Async flows are complex (login → fetch → redirect)
26
23
  - Side effects must be predictable & testable
27
- - You want headless tests without rendering
28
- - You prefer architecture-driven design over component-driven logic
24
+ - You want **headless testing** without rendering
25
+ - You prefer **architecture-driven** design
29
26
 
30
27
  ---
31
28
 
32
29
  ## 🧠 Mental Model
33
30
 
34
- ```txt
35
- UI (Pure View)
36
- └─ emits intent (no logic)
37
-
38
- External Runtime (logic-runtime-react-z)
39
- ├─ state store
40
- ├─ intent handlers
41
- ├─ effect pipeline
42
- ├─ computed graph
43
- ├─ selectors
44
- └─ devtools timeline
45
- ```
31
+ ![Architecture Diagram](https://github.com/delpikye-v/logic-runtime-react/raw/main/architect.png)
46
32
 
47
- - React is just an adapter.
48
- - The runtime owns behavior.
33
+ - React is only an adapter
34
+ - Runtime owns all behavior
49
35
 
50
36
  ---
51
37
 
52
38
  ## 📦 Installation
53
- ```ts
39
+
40
+ ```bash
54
41
  npm install logic-runtime-react-z
55
42
  ```
43
+
56
44
  ---
57
45
 
58
- ## Basic
46
+ ## Examples
47
+ #### 1. Headless (No React)
48
+
59
49
  ```ts
60
- const logic = createLogic({
50
+ import { createLogic } from "logic-runtime-react-z"
51
+
52
+ const counterLogic = createLogic({
61
53
  state: { count: 0 },
54
+
62
55
  intents: bus => {
63
56
  bus.on("inc", ({ setState }) => {
64
- setState(s => { s.count++ })
57
+ setState(s => {
58
+ s.count++
59
+ })
65
60
  })
66
- }
61
+
62
+ bus.on("add", ({ payload, setState }) => {
63
+ setState(s => {
64
+ s.count += payload
65
+ })
66
+ })
67
+ },
67
68
  })
68
69
 
69
- const runtime = logic.create()
70
- runtime.emit("inc")
70
+ const runtime = counterLogic.create()
71
+
72
+ await runtime.emit("inc")
73
+ await runtime.emit("add", 5)
74
+
75
+ console.log(runtime.state.count) // 6
76
+
71
77
  ```
72
78
 
73
79
  ---
74
80
 
75
- ## ⚛️ React Usage (No Hooks in View)
81
+ #### ⚛️ 2. With React (No Hooks in View)
76
82
 
77
83
  ```ts
78
- import { createLogic, withLogic, createSelector } from "logic-runtime-react-z"
84
+ import {
85
+ createLogic,
86
+ withLogic,
87
+ } from "logic-runtime-react-z"
79
88
 
80
89
  interface State {
81
- count: number;
82
-
83
- // computed
90
+ count: number
84
91
  double: number
85
92
  }
86
93
 
87
94
  const counterLogic = createLogic({
88
95
  name: "counter",
96
+
89
97
  state: { count: 1 },
90
98
 
91
99
  computed: {
92
- double({ state }): number {
100
+ double({ state }) {
93
101
  return state.count * 2
94
- }
102
+ },
95
103
  },
96
104
 
97
105
  intents: bus => {
98
- bus.on("inc", ({ state, setState }) => {
99
- // if (!selectIsAdult(state)) {
100
- // throw new Error("Not allowed")
101
- // }
102
-
106
+ bus.on("inc", ({ setState }) => {
103
107
  setState(s => {
104
108
  s.count++
105
109
  })
106
110
  })
111
+
112
+ bus.on("inc-payload", ({ payload, setState }) => {
113
+ setState(s => {
114
+ s.count += payload
115
+ })
116
+ })
107
117
  },
108
118
  })
109
119
 
110
- // Pure View (No Hooks)
120
+ // Pure View (no hooks)
111
121
  function CounterView({
112
122
  state,
113
123
  emit,
114
124
  }: {
115
125
  state: State
116
- emit: (intent: string) => void
126
+ emit: (intent: string, payload?: any) => void
117
127
  }) {
118
128
  return (
119
129
  <div>
120
130
  <div>Count: {state.count}</div>
121
- // <button
122
- // onClick={async () => {
123
- // try {
124
- // await emit("inc")
125
- // console.log("done")
126
- // } catch (e) {
127
- // console.error(e)
128
- // }
129
- // }}
130
- // >
131
- // +
132
- // </button>
133
131
  <div>Double: {state.double}</div>
132
+
134
133
  <button onClick={() => emit("inc")}>+</button>
134
+ <button onClick={() => emit("inc-payload", 5)}>+5</button>
135
135
  </div>
136
136
  )
137
137
  }
138
138
 
139
- // Bind Logic to React
139
+ // Bind logic to React
140
140
  export const Counter = withLogic(counterLogic, CounterView)
141
+
141
142
  ```
143
+ ---
142
144
 
143
- ## 🧪 No React
144
- ```ts
145
+ #### 🧪 3. Testing (No React)
145
146
 
146
- // import { createLogic } from "logic-runtime-react-z"
147
- const counterLogic = createLogic({
148
- state: {
149
- count: 0
150
- },
147
+ ```ts
148
+ // no render, no React
149
+ const logic = createLogic({
150
+ state: { value: 0 },
151
151
 
152
152
  computed: {
153
- double({ state }) {
154
- return state.count * 2
155
- }
153
+ squared({ state }) {
154
+ return state.value * state.value
155
+ },
156
156
  },
157
157
 
158
158
  intents: bus => {
159
- bus.on("inc", ({ state, setState }) => {
160
- // state is READONLY snapshot (read-only)
161
- // ❌ state.count++
159
+ bus.on("set", ({ payload, setState }) => {
162
160
  setState(s => {
163
- s.count += 1
164
- })
165
- })
166
-
167
- // Intent handlers can be sync or async
168
- bus.on("add", ({ payload, setState }) => {
169
- setState(s => {
170
- s.count += payload
171
- })
172
- })
173
-
174
- bus.on("reset", ({ setState }) => {
175
- setState(s => {
176
- s.count = 0
161
+ s.value = payload
177
162
  })
178
163
  })
179
164
  },
180
165
  })
181
166
 
182
- // initial
183
- // const runtime = counterLogic.create()
184
- const runtime = counterLogic.create("counter:main")
167
+ const runtime = logic.create()
168
+
169
+ await runtime.emit("set", 4)
185
170
 
186
- runtime.emit("inc")
187
- // await runtime.emit("inc") // if async
188
- runtime.emit("add", 5)
171
+ expect(runtime.state.squared).toBe(16)
189
172
 
190
- // console.log
191
- console.log(runtime.state.count) // 7
192
- console.log(runtime.state.double) // 14
193
173
  ```
194
174
 
195
175
  ---
196
176
 
197
- ## 🧩 Plugins – Runtime extensions (cross-cutting concerns)
177
+ ## Backend Runtime
178
+
179
+ ```perl
180
+ HTTP / Queue / Cron
181
+
182
+ emit(intent)
183
+
184
+ middleware / effects
185
+
186
+ intent handlers
187
+
188
+ mutate state
189
+
190
+ emit next intent
191
+ ```
198
192
 
199
- #### Plugins extend the runtime behavior **without touching business logic**.
200
193
 
194
+ #### 🚀 1. Backend Runtime
201
195
  ```ts
202
- export const authPlugin: LogicPlugin = {
203
- name: "auth",
196
+ 1️⃣ // create backend
197
+ import { createBackendRuntime } from "logic-runtime-react-z"
204
198
 
205
- setup(runtime) {
206
- runtime.onIntent("delete", ctx => {
207
- if (!ctx.state.user?.isAdmin) {
208
- throw new Error("Forbidden")
209
- }
210
- })
199
+ type BackendState = {
200
+ user: null | {
201
+ id: string
202
+ name: string
203
+ }
204
+ token: string | null
205
+ loading: boolean
206
+ }
207
+
208
+ const runtime = createBackendRuntime<BackendState>({
209
+ user: null,
210
+ token: null,
211
+ loading: false,
212
+ })
213
+
214
+ // middleware
215
+ runtime.use(async (ctx, next) => {
216
+ console.log("➡️ intent:", ctx.intent, ctx.payload)
217
+ console.log(" state before:", ctx.state)
218
+
219
+ const start = Date.now()
220
+ await next()
221
+
222
+ console.log("⬅️ intent:", ctx.intent)
223
+ console.log(" state after:", ctx.state)
224
+ console.log(" took:", Date.now() - start, "ms")
225
+ })
226
+
227
+ 2️⃣ // register handler
228
+ runtime.onIntent("login", async ({
229
+ payload,
230
+ setState,
231
+ emit,
232
+ }) => {
233
+ setState(s => {
234
+ s.loading = true
235
+ })
236
+
237
+ // fake API
238
+ const result = await fakeLoginApi(payload)
239
+
240
+ setState(s => {
241
+ s.user = result.user
242
+ s.token = result.token
243
+ s.loading = false
244
+ })
245
+
246
+ // chain intent
247
+ await emit("login:success", result.user)
248
+ })
249
+
250
+ runtime.onIntent("login:success", ({ payload }) => {
251
+ console.log("✅ login success:", payload.name)
252
+ })
253
+
254
+ // 3️⃣ Add side-effects (effects)
255
+ runtime.effect("login", next => async ctx => {
256
+ console.log("→ login start")
257
+ await next(ctx)
258
+ console.log("← login end")
259
+ })
260
+
261
+ // 4️⃣ Emit intent (entry point)
262
+ await runtime.emit("login", {
263
+ username: "admin",
264
+ password: "123456",
265
+ })
266
+
267
+ console.log(runtime.state().user)
268
+ /*
269
+ {
270
+ id: "u1",
271
+ name: "Admin"
272
+ }
273
+ */
274
+
275
+ // 5️⃣ Subscribe state changes (optional)
276
+ runtime.subscribe(() => {
277
+ console.log("🔄 state changed:", runtime.state())
278
+ })
279
+
280
+ // 6️⃣ Fake API (for demo)
281
+ async function fakeLoginApi(payload: {
282
+ username: string
283
+ password: string
284
+ }) {
285
+ await new Promise(r => setTimeout(r, 500))
286
+
287
+ return {
288
+ user: {
289
+ id: "u1",
290
+ name: payload.username,
291
+ },
292
+ token: "jwt-token",
211
293
  }
212
294
  }
295
+ ```
296
+
297
+ ---
298
+
299
+ #### 🧪 2. Testing Example
300
+ ```ts
301
+ await runtime.emit("login", {
302
+ username: "admin",
303
+ password: "123456",
304
+ })
305
+
306
+ expect(runtime.state().user?.name).toBe("admin")
307
+ expect(runtime.state().loading).toBe(false)
308
+
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Utility
314
+
315
+ #### 🧩 Plugins (Cross-cutting Concerns)
316
+
317
+ Plugins extend runtime behavior **without touching business logic**.
318
+
319
+ ```ts
320
+ import type { LogicPlugin } from "logic-runtime-react-z"
213
321
 
214
322
  export const persistPlugin: LogicPlugin = {
215
323
  name: "persist",
@@ -221,128 +329,281 @@ export const persistPlugin: LogicPlugin = {
221
329
  JSON.stringify(runtime.state)
222
330
  )
223
331
  })
224
- }
332
+ },
333
+ }
334
+
335
+ ```
336
+
337
+ ---
338
+
339
+ #### ⚡ Effects (Async / Side-effects)
340
+
341
+ Effects are **middleware-style intent interceptors**, not React effects.
342
+
343
+ ##### Built-in effects
344
+ - `takeLatest`
345
+ - `debounce`
346
+ - `retry`
347
+
348
+ ```ts
349
+ import { effect } from "logic-runtime-react-z"
350
+
351
+ const logEffect = effect(next => async ctx => {
352
+ console.log("→", ctx.intent)
353
+ await next(ctx)
354
+ console.log("←", ctx.intent)
355
+ })
356
+
357
+ intents: bus => {
358
+ bus.effect("save", logEffect)
359
+
360
+ bus.on("save", async ({ state }) => {
361
+ await api.save(state.form)
362
+ })
225
363
  }
364
+
365
+ ```
366
+ ##### Built-in helpers
367
+ - takeLatest()
368
+ - debounce(ms)
369
+ - retry(times)
370
+
371
+ ```ts
372
+ bus.effect(
373
+ "search",
374
+ logEffect.debounce(300).takeLatest()
375
+ )
226
376
  ```
227
377
 
228
378
  ---
229
379
 
230
- ## Async Effects + Selectors (Advanced Usage)
380
+ #### 🧮 Selectors
231
381
 
232
- #### createSelector
233
- Selectors are pure functions that derive data from state.
234
- They are read-only, memoized, and safe to reuse anywhere (runtime, effects, intents).
382
+ Selectors are pure, memoized, reusable functions.
235
383
 
236
384
  ```ts
237
385
  import { createSelector } from "logic-runtime-react-z"
238
386
 
239
387
  const selectIsAdult = createSelector(
240
- (state: { age: number }) => state.age,
241
- age => age >= 18
388
+ (state: { age: number }) => state.age >= 18
242
389
  )
390
+
243
391
  ```
244
392
 
245
- #### effect Async / side-effect layer
246
- Effects here are middleware-style intent interceptors, not React effects. They are used for:
247
- - logging
248
- - permission checks
249
- - retries / debounce
250
- - cancellation
251
- - async orchestration
393
+ #### 🧭 Devtools & Timeline
394
+
395
+ - Records every intent
396
+ - Replayable
397
+ - Deterministic async flow
252
398
 
253
399
  ```ts
254
- import { effect, debounce, retry, takeLatest } from "logic-runtime-react-z"
400
+ runtime.devtools?.timeline.replay(runtime.emit)
401
+ ```
402
+
403
+ ###### You can:
404
+
405
+ - time-travel intents
406
+ - replay async flows deterministically
407
+ - debug without UI
408
+
409
+ ---
255
410
 
256
- const withLogging = effect(next => {
257
- return async ctx => {
258
- console.log("saving...")
259
- await next(ctx)
411
+ ## 🔍 Comparison
412
+
413
+ | Feature | logic-runtime-react-z | Redux | Zustand |
414
+ | ------------------- | --------------------- | ----- | ------- |
415
+ | No-hook UI | ✅ | ❌ | ❌ |
416
+ | Intent-first | ✅ | ❌ | ❌ |
417
+ | Async orchestration | ✅ | ⚠️ | ⚠️ |
418
+ | Computed graph | ✅ | ❌ | ❌ |
419
+ | Headless testing | ✅ | ⚠️ | ⚠️ |
420
+ | Devtools replay | ✅ | ⚠️ | ❌ |
421
+
422
+ ---
423
+
424
+ ## 🚫 Anti-patterns (What NOT to do)
425
+
426
+ This library enforces a **clear separation between intent, behavior, and view**.
427
+ If you find yourself doing the following, you are probably fighting the architecture.
428
+
429
+ ---
430
+
431
+ #### ❌ 1. Putting business logic inside React components
432
+
433
+ ```tsx
434
+ // ❌ Don't do this
435
+ function Login() {
436
+ const [loading, setLoading] = useState(false)
437
+
438
+ async function handleLogin() {
439
+ setLoading(true)
440
+ const user = await api.login()
441
+ setLoading(false)
442
+ navigate("/home")
260
443
  }
444
+ }
445
+ ```
446
+ Why this is wrong
447
+ - Logic tied to React lifecycle
448
+ - Hard to test without rendering
449
+ - Side-effects scattered in UI
450
+
451
+ ✅ Correct
452
+
453
+ ```ts
454
+ runtime.emit("login")
455
+ ```
456
+
457
+ ```ts
458
+ bus.on("login", async ({ setState, emit }) => {
459
+ setState(s => { s.loading = true })
460
+ const user = await api.login()
461
+ setState(s => { s.loading = false })
462
+ emit("login:success", user)
261
463
  })
262
464
  ```
263
465
 
264
- #### Combining Effects + Selectors in Intents
466
+ #### 2. Calling handlers directly instead of emitting intent
265
467
  ```ts
266
- intents: bus => {
267
- // attach effect to intent
268
- bus.effect("save", withLogging)
468
+ // Don't call handlers manually
469
+ loginHandler(payload)
470
+ ```
471
+ Why this is wrong
269
472
 
270
- // intent handler (business rule)
271
- bus.on("save", ({ state }) => {
272
- if (!selectIsAdult(state)) {
273
- throw new Error("Not allowed")
274
- }
473
+ - Skips middleware & effects
474
+ - Breaks devtools timeline
475
+ - Makes behavior non-deterministic
275
476
 
276
- // perform save logic...
277
- })
477
+ Correct
278
478
 
279
- bus.on("save-user", async ({ state, setState }) => {
280
- await api.save(state.form)
281
- setState(s => {
282
- s.saved = true
283
- })
284
- })
479
+ ```ts
480
+ runtime.emit("login", payload)
481
+ ```
482
+ Intent is the only entry point. Always.
285
483
 
286
- // bus.effect("save", debounce(300))
287
- // bus.effect("save", retry(2))
288
- // bus.effect("save", takeLatest())
484
+ #### ❌ 3. Using effects to mutate state directly
485
+ ```ts
486
+ // Effect mutating state
487
+ bus.effect("save", next => async ctx => {
488
+ ctx.setState(s => { s.saving = true })
489
+ await next(ctx)
490
+ })
491
+ ```
289
492
 
290
- }
493
+ Why this is wrong
494
+
495
+ - Effects are orchestration, not business logic
496
+ - Hard to reason about ordering
497
+ - Blurs responsibility
498
+
499
+ ✅ Correct
500
+
501
+ ```ts
502
+ bus.on("save", ({ setState }) => {
503
+ setState(s => { s.saving = true })
504
+ })
505
+ ```
506
+ Effects should only:
507
+ - debounce
508
+ - retry
509
+ - cancel
510
+ - log
511
+ - trace
512
+
513
+ #### ❌ 4. Treating intent like Redux actions
514
+ ```ts
515
+ // ❌ Generic, meaningless intent
516
+ emit("SET_STATE", { loading: true })
291
517
  ```
292
518
 
293
- ---
519
+ Why this is wrong
294
520
 
295
- ## 🧭 Devtools & Timeline
296
- - Every intent is recorded
297
- - Replayable
298
- - Deterministic async flows
521
+ - Intent should describe user or system intention
522
+ - Not raw state mutation
523
+
524
+ Correct
299
525
 
300
526
  ```ts
301
- // devtool process.env.NODE_ENV !== "production"
302
- runtime.devtools?.timeline.replay(runtime.emit)
527
+ emit("login:start")
528
+ emit("login:success", user)
529
+ emit("login:failed", error)
303
530
  ```
531
+ Intents are verbs, not patches.
304
532
 
305
- ---
533
+ #### ❌ 5. Reading or mutating state outside the runtime
534
+ ```ts
535
+ // ❌ External mutation
536
+ runtime.state.user.name = "admin"
537
+ ```
538
+ Why this is wrong
539
+ - Breaks computed cache
540
+ - Bypasses subscriptions
541
+ - Devtools become unreliable
306
542
 
307
- ## Props
543
+ Correct
308
544
 
309
- | API field | Type | Description |
310
- | ---------- | ---------------------- | ---------------------------------------------------------------------------------- |
311
- | `name` | `string?` | Optional logic name. Used for debugging, devtools, and default runtime scope. |
312
- | `state` | `S` | **Base mutable state** (source of truth). Can only be changed via `setState`. |
313
- | `computed` | `ComputedDef<S, C>?` | **Derived read-only state**, automatically recomputed when base state changes. |
314
- | `intents` | `(bus) => void` | Defines **business actions** (intent handlers). Intents describe behavior, not UI. |
315
- | `plugins` | `LogicPlugin<S, C>[]?` | Runtime extensions (devtools, logging, persistence, analytics, etc.). |
545
+ ```ts
546
+ emit("update:user:name", "admin")
547
+ ```
316
548
 
549
+ #### ❌ 6. Using React hooks to replace runtime behavior
550
+ ```ts
551
+ // ❌ useEffect as orchestration
552
+ useEffect(() => {
553
+ if (state.loggedIn) {
554
+ fetchProfile()
555
+ }
556
+ }, [state.loggedIn])
557
+ ```
558
+ Why this is wrong
317
559
 
318
- ---
560
+ - Behavior split across layers
561
+ - Impossible to replay or test headlessly
319
562
 
320
- ## 🔍 Comparison
563
+ Correct
321
564
 
322
- | Feature | logic-runtime-react-z | Redux | Zustand |
323
- | --------------- | ---------------------- | ----- | -------- |
324
- | No-hook UI | ✅ | ❌ | ❌ |
325
- | Intent-first | ✅ | ❌ | ❌ |
326
- | Async built-in | ✅ | ⚠️ | ⚠️ |
327
- | Computed graph | ✅ | ❌ | ❌ |
328
- | Headless test | ✅ | ⚠️ | ⚠️ |
329
- | Devtools replay | ✅ | ⚠️ | ❌ |
565
+ ```ts
566
+ bus.on("login:success", async ({ emit }) => {
567
+ await emit("profile:fetch")
568
+ })
569
+ ```
570
+ #### 7. One logic runtime doing everything
571
+ ```ts
572
+ // God runtime
573
+ createLogic({
574
+ state: {
575
+ user: {},
576
+ cart: {},
577
+ products: {},
578
+ settings: {},
579
+ ui: {},
580
+ }
581
+ })
582
+ ```
583
+ Why this is wrong
584
+ - No ownership boundaries
585
+ - Hard to compose
586
+ - Does not scale
330
587
 
331
- > This comparison focuses on **architecture and mental model**, not ecosystem size.
588
+ Correct
332
589
 
333
- ---
590
+ ```ts
591
+ composeLogic(
592
+ userLogic,
593
+ cartLogic,
594
+ productLogic
595
+ )
596
+ ```
334
597
 
335
- ## 🚫 What this library is NOT
598
+ ---
336
599
 
337
- - Not a React state manager
338
- - ❌ Not a replacement for Redux Toolkit
339
- - ❌ Not a UI framework
340
- - ❌ Not tied to React (runtime is headless)
600
+ ## 🧠 Philosophy
341
601
 
342
- It is a **behavior runtime**.
602
+ - If it can’t be tested without React, it doesn’t belong in React.
603
+ - If it bypasses emit, it doesn’t belong in the system.
343
604
 
344
605
  ---
345
606
 
346
607
  ## 📜 License
347
608
 
348
- MIT
609
+ MIT / Delpi
@@ -0,0 +1,9 @@
1
+ export declare function createBackendRuntime<S extends object>(initial: S): {
2
+ state(): S;
3
+ setState(fn: (s: S) => void): void;
4
+ emit(intent: string, payload?: any): Promise<void>;
5
+ onIntent: (intent: string, handler: import("./types").IntentHandler<S, S, any>) => void;
6
+ effect: (intent: string, fx: import("./types").Effect<S, S, any>) => void;
7
+ use: (mw: import("./types").IntentMiddleware<S, S, any>) => void;
8
+ reset(): void;
9
+ };
@@ -1,5 +1,5 @@
1
- type LogicInstance = {
2
- state(): any;
1
+ type LogicInstance<S = any> = {
2
+ readonly state: S;
3
3
  emit?: (intent: string, payload?: any) => any | Promise<any>;
4
4
  subscribe(fn: () => void): () => void;
5
5
  };
@@ -13,17 +13,8 @@ type ComposedLogic = {
13
13
  };
14
14
  export declare function composeLogic(...entries: ComposedLogic[]): {
15
15
  create(scope?: string): {
16
- /**
17
- * merged state
18
- */
19
16
  readonly state: any;
20
- /**
21
- * ASYNC emit
22
- */
23
17
  emit(intent: string, payload?: any): Promise<any[]>;
24
- /**
25
- * subscription fan-out
26
- */
27
18
  subscribe(fn: () => void): () => void;
28
19
  };
29
20
  };
@@ -1,18 +1,12 @@
1
1
  import { Timeline } from "./timeline";
2
- /**
3
- * Devtools public API
4
- */
5
2
  export type Devtools = {
6
3
  timeline: Timeline;
7
4
  wrap(runtime: RuntimeLike): void;
8
5
  };
9
- /**
10
- * Minimal runtime surface for devtools
11
- */
12
6
  type RuntimeLike = {
13
7
  scope: string;
8
+ get state(): any;
14
9
  emit(intent: string, payload?: any): Promise<void>;
15
- state(): any;
16
10
  };
17
11
  export declare function attachDevtools(target: RuntimeLike): Devtools;
18
12
  export {};
@@ -1,16 +1,7 @@
1
1
  import type { Effect } from "./types";
2
2
  export declare function composeEffects<W, R>(effects: Effect<W, R>[]): Effect<W, R>;
3
- /**
4
- * takeLatest
5
- */
6
3
  export declare function takeLatest<W, R>(): Effect<W, R>;
7
- /**
8
- * debounce
9
- */
10
4
  export declare function debounce<W, R>(ms: number): Effect<W, R>;
11
- /**
12
- * retry
13
- */
14
5
  export declare function retry<W, R>(count?: number): Effect<W, R>;
15
6
  export type EffectBuilder<W, R> = Effect<W, R> & {
16
7
  takeLatest(): EffectBuilder<W, R>;
@@ -1,11 +1,11 @@
1
- import type { IntentHandler, Effect } from "./types";
2
- export declare function createIntentBus<W extends object, // Writable State
3
- R extends object = W, // Read State (snapshot / derived)
4
- P = any>(scope: string): {
1
+ import type { IntentHandler, Effect, IntentMiddleware } from "./types";
2
+ export declare function createIntentBus<W extends object, R extends object = W, P = any>(scope: string): {
5
3
  on: (intent: string, handler: IntentHandler<W, R, P>) => void;
6
4
  effect: (intent: string, fx: Effect<W, R, P>) => void;
7
5
  emit: (intent: string, payload: P, ctx: {
8
6
  getState: () => R;
9
7
  setState: (fn: (s: W) => void) => void;
8
+ emit: (intent: string, payload?: any) => Promise<void>;
10
9
  }) => Promise<void>;
10
+ use: (mw: IntentMiddleware<W, R, P>) => void;
11
11
  };
@@ -0,0 +1,2 @@
1
+ export type IntentMiddleware<Ctx = any> = (ctx: Ctx, next: () => Promise<void>) => Promise<void>;
2
+ export declare function compose(middlewares: IntentMiddleware[]): (ctx: any, next: () => Promise<void>) => Promise<void>;
@@ -8,25 +8,10 @@ export type IntentRecord<S = any> = {
8
8
  readonly state: Readonly<S>;
9
9
  readonly timestamp: number;
10
10
  };
11
- /**
12
- * Unified emit function:
13
- * - sync emit → void
14
- * - async emit → Promise<void>
15
- */
16
11
  export type EmitFn = (intent: string, payload?: any) => void | Promise<void>;
17
12
  export type Timeline<S = any> = {
18
- /** immutable snapshot */
19
13
  readonly records: readonly IntentRecord<S>[];
20
- /** record one event */
21
14
  record(entry: Omit<IntentRecord<S>, "id">): void;
22
- /**
23
- * Replay intent records.
24
- *
25
- * ⚠️ Notes:
26
- * - only replays `type === "emit"`
27
- * - replay is sequential
28
- * - replay does NOT record new timeline entries
29
- */
30
15
  replay(emit: EmitFn, options?: {
31
16
  from?: number;
32
17
  to?: number;
@@ -1,25 +1,24 @@
1
1
  import type { Timeline } from "./timeline";
2
2
  export type Listener = () => void;
3
3
  export type EffectCtx<W = any, R = W, P = any> = {
4
- /** full snapshot: base + computed */
5
4
  state: Readonly<R>;
6
5
  payload: P;
7
6
  signal: AbortSignal;
8
7
  scope: string;
9
- /** only base state is mutable */
8
+ emit(intent: string, payload?: any): Promise<void>;
10
9
  setState: (fn: (s: W) => void) => void;
11
10
  };
12
11
  export type IntentHandler<W = any, R = W, P = any> = (ctx: EffectCtx<W, R, P>) => void | Promise<void>;
13
12
  export type Effect<W = any, R = W, P = any> = (next: IntentHandler<W, R, P>) => IntentHandler<W, R, P>;
13
+ export type IntentMiddleware<W = any, R = W, P = any> = (ctx: EffectCtx<W, R, P>, next: () => Promise<void>) => Promise<void>;
14
14
  export type LogicRuntime<S extends object, C extends object = {}> = {
15
15
  scope: string;
16
- /** full snapshot */
17
- state(): Readonly<S & C>;
18
- /** base state only */
16
+ get state(): Readonly<S & C>;
19
17
  setState(mutator: (s: S) => void): void;
20
18
  reset(): void;
21
- /** await full intent pipeline */
19
+ batch(fn: () => void): void;
22
20
  emit(intent: string, payload?: any): Promise<void>;
21
+ use(mw: IntentMiddleware<S, S & C>): void;
23
22
  subscribe(fn: Listener): () => void;
24
23
  onIntent(intent: string, handler: IntentHandler<S, S & C>): void;
25
24
  devtools?: {
@@ -1 +1 @@
1
- "use strict";var t=require("react/jsx-runtime");function e(t){var e=Object.create(null);return t&&Object.keys(t).forEach(function(n){if("default"!==n){var r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:function(){return t[n]}})}}),e.default=t,Object.freeze(e)}var n,r,o=e(require("react"));function c(t){const e=new Map,n=new Map,r=new Map;return{on:function(t,n){const r=e.get(t)||[];r.push(n),e.set(t,r)},effect:function(t,e){const r=n.get(t)||[];r.push(e),n.set(t,r)},emit:async function(o,c,s){const a=(e=>`${t}:${e}`)(o),i=r.get(a);null==i||i.abort();const u=new AbortController;r.set(a,u);const l=e.get(o)||[],f=n.get(o)||[];for(const e of l){let n=e;for(let t=f.length-1;t>=0;t--)n=f[t](n);await n({state:s.getState(),payload:c,signal:u.signal,scope:t,setState:s.setState})}}}}function s(t){const e=function(){let t=0,e=[];return{get records(){return e.slice()},record:function(n){e.push({...n,id:++t,state:structuredClone(n.state)})},replay:async function(t,n){const{from:r=0,to:o=1/0,scope:c}=null!=n?n:{},s=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!c||t.scope===c));for(const e of s){const n=t(e.intent,e.payload);n instanceof Promise&&await n}},clear:function(){e=[],t=0}}}();return{timeline:e,wrap:function(){const n=t.emit.bind(t);t.emit=async(r,o)=>{e.record({type:"emit:start",intent:r,payload:o,scope:t.scope,state:t.state(),timestamp:Date.now()});try{await n(r,o)}finally{e.record({type:"emit:end",intent:r,payload:o,scope:t.scope,state:t.state(),timestamp:Date.now()})}}}}}let a=0;const i="production"!==(null===(r=null===(n=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===n?void 0:n.env)||void 0===r?void 0:r.NODE_ENV);const u=[],l=[];function f(){let t=null;return e=>async n=>(null==t||t.abort(),t=new AbortController,e({...n,signal:t.signal}))}function p(t){let e;return n=>r=>{clearTimeout(e),e=setTimeout(()=>n(r),t)}}function d(t=3){return e=>async n=>{let r;for(let o=0;o<t;o++)try{return await e(n)}catch(t){r=t}throw r}}exports.composeLogic=function(...t){return{create(e){const n=t.map(t=>{var n;if("logic"in t){const r=t.logic.create(null!==(n=t.namespace)&&void 0!==n?n:e);return{namespace:t.namespace,inst:r}}return{namespace:null,inst:t.create(e)}});return{get state(){const t={};for(const{namespace:e,inst:r}of n){const n=r.state();e?t[e]=n:Object.assign(t,n)}return t},async emit(t,e){const r=n.map(({inst:n})=>{var r;return null===(r=n.emit)||void 0===r?void 0:r.call(n,t,e)});return Promise.all(r.filter(Boolean))},subscribe(t){const e=n.map(e=>e.inst.subscribe(t));return()=>{e.forEach(t=>t())}}}}}},exports.createLogic=function(t){var e;const n=null!==(e=t.name)&&void 0!==e?e:"logic";return{create(e){var r,o;const f=null!=e?e:`${n}:${++a}`,p=structuredClone(t.state),d=function(t){let e=t;const n=new Set;return{getState:function(){return e},setState:function(t){const r=e,o=structuredClone(r);t(o),Object.is(r,o)||(e=o,n.forEach(t=>t()))},subscribe:function(t){return n.add(t),()=>n.delete(t)}}}(structuredClone(t.state)),m=function(){const t=new Map,e=new Map,n=new Map;return{compute:function(n,r,o){if(t.has(n))return t.get(n);const c=new Set,s=r({state:new Proxy(o,{get:(t,e,n)=>("string"==typeof e&&e in t&&c.add(e),Reflect.get(t,e,n))})});return t.set(n,s),e.set(n,c),s},invalidate:function(r){e.forEach((e,o)=>{var c;e.has(r)&&(t.delete(o),null===(c=n.get(o))||void 0===c||c.forEach(t=>t()))})},subscribe:function(t,e){var r;const o=null!==(r=n.get(t))&&void 0!==r?r:new Set;return o.add(e),n.set(t,o),()=>o.delete(e)},reset:function(){t.clear(),e.clear(),n.forEach(t=>{t.forEach(t=>t())})}}}(),g=c(f);null===(r=t.intents)||void 0===r||r.call(t,g);let b=null,v=null;function y(){const e=d.getState();if(e===b&&v)return v;const n={};var r;return t.computed&&(r=t.computed,Object.keys(r)).forEach(r=>{n[r]=m.compute(r,t.computed[r],e)}),b=e,v=Object.assign({},e,n),v}function h(t){const e=d.getState();d.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&m.invalidate(t)})}const w={scope:f,state:()=>y(),setState:h,reset(){!function(){var t;d.setState(()=>structuredClone(p)),null===(t=m.reset)||void 0===t||t.call(m),b=null,v=null}()},async emit(t,e){const n=y();u.forEach(r=>r({intent:t,payload:e,state:n,scope:f}));try{await g.emit(t,e,{getState:y,setState:h})}finally{l.forEach(n=>n({intent:t,payload:e,state:y(),scope:f}))}},subscribe:d.subscribe,onIntent(t,e){g.on(t,e)},__internal:{onEmitStart(t){u.push(t)},onEmitEnd(t){l.push(t)}}};let S;return null===(o=t.plugins)||void 0===o||o.forEach(t=>t.setup(w)),i&&(S=s(w),S.wrap(w)),{...w,devtools:S}}}},exports.createSelector=function(t,e=Object.is){let n,r=null;return o=>{if(null!==r){const c=t(o);return e(n,c)?n:(n=c,r=o,c)}return r=o,n=t(o),n}},exports.createSignal=function(t){let e=t;const n=new Set;return{get:()=>e,set(t){Object.is(e,t)||(e=t,n.forEach(t=>t()))},subscribe:t=>(n.add(t),()=>n.delete(t))}},exports.debounce=p,exports.effect=function(t){const e=[t],n=t=>{return(n=e,t=>n.reduceRight((t,e)=>e(t),t))(t);var n};return n.takeLatest=()=>(e.push(f()),n),n.debounce=t=>(e.push(p(t)),n),n.retry=(t=3)=>(e.push(d(t)),n),n},exports.retry=d,exports.takeLatest=f,exports.withLogic=function(e,n){var r,c;const s=r=>{const c=o.useRef(null);c.current||(c.current=e.create());const s=c.current,a=o.useSyncExternalStore(s.subscribe,s.state,s.state),i=o.useCallback((t,e)=>s.emit(t,e),[s]);return t.jsx(n,{...r,state:a,emit:i})};return s.displayName=`withLogic(${null!==(c=null!==(r=n.displayName)&&void 0!==r?r:n.name)&&void 0!==c?c:"View"})`,s};
1
+ "use strict";var t=require("react/jsx-runtime");function e(t){var e=Object.create(null);return t&&Object.keys(t).forEach(function(n){if("default"!==n){var r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:function(){return t[n]}})}}),e.default=t,Object.freeze(e)}var n,r,o=e(require("react"));function a(t){const e=new Map,n=new Map,r=[],o=new Map;return{on:function(t,n){const r=e.get(t)||[];r.push(n),e.set(t,r)},effect:function(t,e){const r=n.get(t)||[];r.push(e),n.set(t,r)},emit:async function(a,c,s){const i=(e=>`${t}:${e}`)(a),u=o.get(i);null==u||u.abort();const l=new AbortController;o.set(i,l);const f=(d=r,async(t,e)=>{let n=-1;await async function r(o){if(o<=n)throw new Error("next() called multiple times");n=o;const a=d[o];if(!a)return e();await a(t,()=>r(o+1))}(0)});var d;const p=Array.from(e.entries()).filter(([t])=>function(t,e){return t.endsWith("/*")?e.startsWith(t.slice(0,-2)):t===e}(t,a)).flatMap(([,t])=>t),m=n.get(a)||[];for(const e of p){let n=e;for(let t=m.length-1;t>=0;t--)n=m[t](n);const r={state:s.getState(),payload:c,signal:l.signal,scope:t,emit:s.emit,setState:s.setState};await f(r,async()=>{await n(r)})}},use:function(t){r.push(t)}}}function c(t){const e=function(){let t=0,e=[];return{get records(){return e.slice()},record:function(n){e.push({...n,id:++t,state:structuredClone(n.state)})},replay:async function(t,n){const{from:r=0,to:o=1/0,scope:a}=null!=n?n:{},c=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!a||t.scope===a));for(const e of c){const n=t(e.intent,e.payload);n instanceof Promise&&await n}},clear:function(){e=[],t=0}}}();return{timeline:e,wrap:function(){const n=t.emit.bind(t);t.emit=async(r,o)=>{e.record({type:"emit:start",intent:r,payload:o,scope:t.scope,state:t.state,timestamp:Date.now()});try{await n(r,o)}finally{e.record({type:"emit:end",intent:r,payload:o,scope:t.scope,state:t.state,timestamp:Date.now()})}}}}}let s=0;const i="production"!==(null===(r=null===(n=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===n?void 0:n.env)||void 0===r?void 0:r.NODE_ENV);function u(){let t=null;return e=>async n=>{null==t||t.abort(),t=new AbortController;const r=t.signal;if(!r.aborted)try{return await e({...n,signal:r})}catch(t){if(r.aborted)return;throw t}}}function l(t){let e;return n=>r=>new Promise((o,a)=>{clearTimeout(e),e=setTimeout(()=>{Promise.resolve(n(r)).then(o,a)},t)})}function f(t=3){return e=>async n=>{var r;let o;for(let a=0;a<t;a++)try{return await e(n)}catch(t){if(null===(r=n.signal)||void 0===r?void 0:r.aborted)return;o=t}throw o}}exports.attachDevtools=c,exports.composeLogic=function(...t){return{create(e){const n=t.map(t=>{var n;if("logic"in t){const r=t.logic.create(null!==(n=t.namespace)&&void 0!==n?n:e);return{namespace:t.namespace,inst:r}}return{namespace:null,inst:t.create(e)}});return{get state(){const t={};for(const{namespace:e,inst:r}of n){const n=r.state;e?t[e]=n:Object.assign(t,n)}return t},async emit(t,e){const r=n.map(({inst:n})=>{var r;return null===(r=n.emit)||void 0===r?void 0:r.call(n,t,e)});return Promise.all(r.filter(Boolean).map(t=>t.catch(t=>console.error(t))))},subscribe(t){const e=n.map(e=>e.inst.subscribe(t));return()=>{e.forEach(t=>t())}}}}}},exports.createBackendRuntime=function(t){let e=structuredClone(t);const n=a("backend"),r={state:()=>e,setState(t){t(e)},async emit(t,o){await n.emit(t,o,{getState:()=>e,setState:t=>t(e),emit:r.emit})},onIntent:n.on,effect:n.effect,use:n.use,reset(){e=structuredClone(t)}};return r},exports.createLogic=function(t){var e;const n=null!==(e=t.name)&&void 0!==e?e:"logic";return{create(e){var r,o;const u=null!=e?e:`${n}:${++s}`,l=structuredClone(t.state),f=function(t){let e=t;const n=new Set;return{getState:function(){return e},setState:function(t){const r=e,o=structuredClone(r);t(o),Object.is(r,o)||(e=o,n.forEach(t=>t()))},subscribe:function(t){return n.add(t),()=>n.delete(t)}}}(structuredClone(t.state)),d=function(){const t=new Map,e=new Map,n=new Map;return{compute:function(n,r,o){if(t.has(n))return t.get(n);const a=new Set,c=r({state:new Proxy(o,{get:(t,e,n)=>("string"==typeof e&&e in t&&a.add(e),Reflect.get(t,e,n))})});return t.set(n,c),e.set(n,a),c},invalidate:function(r){e.forEach((e,o)=>{var a;e.has(r)&&(t.delete(o),null===(a=n.get(o))||void 0===a||a.forEach(t=>t()))})},subscribe:function(t,e){var r;const o=null!==(r=n.get(t))&&void 0!==r?r:new Set;return o.add(e),n.set(t,o),()=>o.delete(e)},reset:function(){t.clear(),e.clear(),n.forEach(t=>{t.forEach(t=>t())})}}}(),p=a(u);null===(r=t.intents)||void 0===r||r.call(t,p);const m=[],b=[];let h=null,g=null,y=!1,w=new Set;function v(){const e=f.getState();if(e===h&&g)return g;const n={};var r;return t.computed&&(r=t.computed,Object.keys(r)).forEach(r=>{n[r]=d.compute(r,t.computed[r],e)}),h=e,g=Object.assign({},e,n),g}function E(t){const e=f.getState();f.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&(y?w.add(t):d.invalidate(t))})}const S={scope:u,get state(){return v()},setState:E,reset:function(){var t;f.setState(()=>structuredClone(l)),null===(t=d.reset)||void 0===t||t.call(d),h=null,g=null},batch:function(t){y=!0;try{t()}finally{y=!1,w.forEach(t=>d.invalidate(t)),w.clear()}},async emit(t,e){const n=v();m.forEach(r=>r({intent:t,payload:e,state:n,scope:u}));try{await p.emit(t,e,{getState:v,setState:E,emit:S.emit})}finally{b.forEach(n=>n({intent:t,payload:e,state:v(),scope:u}))}},subscribe:f.subscribe,onIntent(t,e){p.on(t,e)},use:p.use,__internal:{onEmitStart(t){m.push(t)},onEmitEnd(t){b.push(t)}}};let x;return null===(o=t.plugins)||void 0===o||o.forEach(t=>t.setup(S)),i&&(x=c(S),x.wrap(S)),{...S,devtools:x}}}},exports.createSelector=function(t,e=Object.is){let n,r=null;return o=>{if(null!==r){const a=t(o);return e(n,a)?n:(n=a,r=o,a)}return r=o,n=t(o),n}},exports.createSignal=function(t){let e=t;const n=new Set;return{get:()=>e,set(t){Object.is(e,t)||(e=t,n.forEach(t=>t()))},subscribe:t=>(n.add(t),()=>n.delete(t))}},exports.debounce=l,exports.effect=function(t){const e=[t];let n=!1;const r=t=>{return(n=!0,r=e,t=>r.reduceRight((t,e)=>e(t),t))(t);var r};return r.takeLatest=()=>{if(n)throw new Error("Effect already built");return e.push(u()),r},r.debounce=t=>{if(n)throw new Error("Effect already built");return e.push(l(t)),r},r.retry=(t=3)=>{if(n)throw new Error("Effect already built");return e.push(f(t)),r},r},exports.retry=f,exports.takeLatest=u,exports.withLogic=function(e,n){var r,a;const c=r=>{const a=o.useRef(null);a.current||(a.current=e.create());const c=a.current,s=o.useSyncExternalStore(c.subscribe,()=>c.state,()=>c.state),i=o.useCallback((t,e)=>c.emit(t,e),[c]);return t.jsx(n,{...r,state:s,emit:i})};return c.displayName=`withLogic(${null!==(a=null!==(r=n.displayName)&&void 0!==r?r:n.name)&&void 0!==a?a:"View"})`,c};
package/build/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { createSignal } from "./core/signal";
2
2
  export { createLogic } from "./core/logic";
3
3
  export { composeLogic } from "./core/compose";
4
- export { effect, takeLatest, debounce, retry } from "./core/effect";
4
+ export { effect, takeLatest, debounce, retry, } from "./core/effect";
5
5
  export { createSelector } from "./core/selector";
6
- export type { LogicPlugin } from "./core/plugin";
7
- export type { LogicRuntime } from "./core/types";
8
- export type { Timeline, IntentRecord } from "./core/timeline";
6
+ export { createBackendRuntime } from "./core/backend-runtime";
7
+ export type { LogicPlugin, } from "./core/plugin";
8
+ export type { LogicRuntime, IntentHandler, Effect, EffectCtx, IntentMiddleware, Listener, } from "./core/types";
9
+ export { attachDevtools } from "./core/devtools";
10
+ export type { Timeline, IntentRecord, } from "./core/timeline";
9
11
  export { withLogic } from "./react/withLogic";
@@ -1 +1 @@
1
- import{jsx as t}from"react/jsx-runtime";import*as e from"react";function n(t){let e=t;const n=new Set;return{get:()=>e,set(t){Object.is(e,t)||(e=t,n.forEach(t=>t()))},subscribe:t=>(n.add(t),()=>n.delete(t))}}function o(t){const e=new Map,n=new Map,o=new Map;return{on:function(t,n){const o=e.get(t)||[];o.push(n),e.set(t,o)},effect:function(t,e){const o=n.get(t)||[];o.push(e),n.set(t,o)},emit:async function(r,s,c){const a=(e=>`${t}:${e}`)(r),i=o.get(a);null==i||i.abort();const u=new AbortController;o.set(a,u);const l=e.get(r)||[],f=n.get(r)||[];for(const e of l){let n=e;for(let t=f.length-1;t>=0;t--)n=f[t](n);await n({state:c.getState(),payload:s,signal:u.signal,scope:t,setState:c.setState})}}}}function r(t){const e=function(){let t=0,e=[];return{get records(){return e.slice()},record:function(n){e.push({...n,id:++t,state:structuredClone(n.state)})},replay:async function(t,n){const{from:o=0,to:r=1/0,scope:s}=null!=n?n:{},c=e.filter(t=>"emit"===t.type&&t.id>=o&&t.id<=r&&(!s||t.scope===s));for(const e of c){const n=t(e.intent,e.payload);n instanceof Promise&&await n}},clear:function(){e=[],t=0}}}();return{timeline:e,wrap:function(){const n=t.emit.bind(t);t.emit=async(o,r)=>{e.record({type:"emit:start",intent:o,payload:r,scope:t.scope,state:t.state(),timestamp:Date.now()});try{await n(o,r)}finally{e.record({type:"emit:end",intent:o,payload:r,scope:t.scope,state:t.state(),timestamp:Date.now()})}}}}}var s,c;let a=0;const i="production"!==(null===(c=null===(s=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===s?void 0:s.env)||void 0===c?void 0:c.NODE_ENV);const u=[],l=[];function f(t){var e;const n=null!==(e=t.name)&&void 0!==e?e:"logic";return{create(e){var s,c;const f=null!=e?e:`${n}:${++a}`,d=structuredClone(t.state),p=function(t){let e=t;const n=new Set;return{getState:function(){return e},setState:function(t){const o=e,r=structuredClone(o);t(r),Object.is(o,r)||(e=r,n.forEach(t=>t()))},subscribe:function(t){return n.add(t),()=>n.delete(t)}}}(structuredClone(t.state)),m=function(){const t=new Map,e=new Map,n=new Map;return{compute:function(n,o,r){if(t.has(n))return t.get(n);const s=new Set,c=o({state:new Proxy(r,{get:(t,e,n)=>("string"==typeof e&&e in t&&s.add(e),Reflect.get(t,e,n))})});return t.set(n,c),e.set(n,s),c},invalidate:function(o){e.forEach((e,r)=>{var s;e.has(o)&&(t.delete(r),null===(s=n.get(r))||void 0===s||s.forEach(t=>t()))})},subscribe:function(t,e){var o;const r=null!==(o=n.get(t))&&void 0!==o?o:new Set;return r.add(e),n.set(t,r),()=>r.delete(e)},reset:function(){t.clear(),e.clear(),n.forEach(t=>{t.forEach(t=>t())})}}}(),g=o(f);null===(s=t.intents)||void 0===s||s.call(t,g);let b=null,v=null;function h(){const e=p.getState();if(e===b&&v)return v;const n={};var o;return t.computed&&(o=t.computed,Object.keys(o)).forEach(o=>{n[o]=m.compute(o,t.computed[o],e)}),b=e,v=Object.assign({},e,n),v}function y(t){const e=p.getState();p.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&m.invalidate(t)})}const w={scope:f,state:()=>h(),setState:y,reset(){!function(){var t;p.setState(()=>structuredClone(d)),null===(t=m.reset)||void 0===t||t.call(m),b=null,v=null}()},async emit(t,e){const n=h();u.forEach(o=>o({intent:t,payload:e,state:n,scope:f}));try{await g.emit(t,e,{getState:h,setState:y})}finally{l.forEach(n=>n({intent:t,payload:e,state:h(),scope:f}))}},subscribe:p.subscribe,onIntent(t,e){g.on(t,e)},__internal:{onEmitStart(t){u.push(t)},onEmitEnd(t){l.push(t)}}};let S;return null===(c=t.plugins)||void 0===c||c.forEach(t=>t.setup(w)),i&&(S=r(w),S.wrap(w)),{...w,devtools:S}}}}function d(...t){return{create(e){const n=t.map(t=>{var n;if("logic"in t){const o=t.logic.create(null!==(n=t.namespace)&&void 0!==n?n:e);return{namespace:t.namespace,inst:o}}return{namespace:null,inst:t.create(e)}});return{get state(){const t={};for(const{namespace:e,inst:o}of n){const n=o.state();e?t[e]=n:Object.assign(t,n)}return t},async emit(t,e){const o=n.map(({inst:n})=>{var o;return null===(o=n.emit)||void 0===o?void 0:o.call(n,t,e)});return Promise.all(o.filter(Boolean))},subscribe(t){const e=n.map(e=>e.inst.subscribe(t));return()=>{e.forEach(t=>t())}}}}}}function p(){let t=null;return e=>async n=>(null==t||t.abort(),t=new AbortController,e({...n,signal:t.signal}))}function m(t){let e;return n=>o=>{clearTimeout(e),e=setTimeout(()=>n(o),t)}}function g(t=3){return e=>async n=>{let o;for(let r=0;r<t;r++)try{return await e(n)}catch(t){o=t}throw o}}function b(t){const e=[t],n=t=>{return(n=e,t=>n.reduceRight((t,e)=>e(t),t))(t);var n};return n.takeLatest=()=>(e.push(p()),n),n.debounce=t=>(e.push(m(t)),n),n.retry=(t=3)=>(e.push(g(t)),n),n}function v(t,e=Object.is){let n,o=null;return r=>{if(null!==o){const s=t(r);return e(n,s)?n:(n=s,o=r,s)}return o=r,n=t(r),n}}function h(n,o){var r,s;const c=r=>{const s=e.useRef(null);s.current||(s.current=n.create());const c=s.current,a=e.useSyncExternalStore(c.subscribe,c.state,c.state),i=e.useCallback((t,e)=>c.emit(t,e),[c]);return t(o,{...r,state:a,emit:i})};return c.displayName=`withLogic(${null!==(s=null!==(r=o.displayName)&&void 0!==r?r:o.name)&&void 0!==s?s:"View"})`,c}export{d as composeLogic,f as createLogic,v as createSelector,n as createSignal,m as debounce,b as effect,g as retry,p as takeLatest,h as withLogic};
1
+ import{jsx as t}from"react/jsx-runtime";import*as e from"react";function n(t){let e=t;const n=new Set;return{get:()=>e,set(t){Object.is(e,t)||(e=t,n.forEach(t=>t()))},subscribe:t=>(n.add(t),()=>n.delete(t))}}function r(t){const e=new Map,n=new Map,r=[],o=new Map;return{on:function(t,n){const r=e.get(t)||[];r.push(n),e.set(t,r)},effect:function(t,e){const r=n.get(t)||[];r.push(e),n.set(t,r)},emit:async function(a,s,c){const i=(e=>`${t}:${e}`)(a),u=o.get(i);null==u||u.abort();const l=new AbortController;o.set(i,l);const f=(d=r,async(t,e)=>{let n=-1;await async function r(o){if(o<=n)throw new Error("next() called multiple times");n=o;const a=d[o];if(!a)return e();await a(t,()=>r(o+1))}(0)});var d;const p=Array.from(e.entries()).filter(([t])=>function(t,e){return t.endsWith("/*")?e.startsWith(t.slice(0,-2)):t===e}(t,a)).flatMap(([,t])=>t),m=n.get(a)||[];for(const e of p){let n=e;for(let t=m.length-1;t>=0;t--)n=m[t](n);const r={state:c.getState(),payload:s,signal:l.signal,scope:t,emit:c.emit,setState:c.setState};await f(r,async()=>{await n(r)})}},use:function(t){r.push(t)}}}function o(t){const e=function(){let t=0,e=[];return{get records(){return e.slice()},record:function(n){e.push({...n,id:++t,state:structuredClone(n.state)})},replay:async function(t,n){const{from:r=0,to:o=1/0,scope:a}=null!=n?n:{},s=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!a||t.scope===a));for(const e of s){const n=t(e.intent,e.payload);n instanceof Promise&&await n}},clear:function(){e=[],t=0}}}();return{timeline:e,wrap:function(){const n=t.emit.bind(t);t.emit=async(r,o)=>{e.record({type:"emit:start",intent:r,payload:o,scope:t.scope,state:t.state,timestamp:Date.now()});try{await n(r,o)}finally{e.record({type:"emit:end",intent:r,payload:o,scope:t.scope,state:t.state,timestamp:Date.now()})}}}}}var a,s;let c=0;const i="production"!==(null===(s=null===(a=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===a?void 0:a.env)||void 0===s?void 0:s.NODE_ENV);function u(t){var e;const n=null!==(e=t.name)&&void 0!==e?e:"logic";return{create(e){var a,s;const u=null!=e?e:`${n}:${++c}`,l=structuredClone(t.state),f=function(t){let e=t;const n=new Set;return{getState:function(){return e},setState:function(t){const r=e,o=structuredClone(r);t(o),Object.is(r,o)||(e=o,n.forEach(t=>t()))},subscribe:function(t){return n.add(t),()=>n.delete(t)}}}(structuredClone(t.state)),d=function(){const t=new Map,e=new Map,n=new Map;return{compute:function(n,r,o){if(t.has(n))return t.get(n);const a=new Set,s=r({state:new Proxy(o,{get:(t,e,n)=>("string"==typeof e&&e in t&&a.add(e),Reflect.get(t,e,n))})});return t.set(n,s),e.set(n,a),s},invalidate:function(r){e.forEach((e,o)=>{var a;e.has(r)&&(t.delete(o),null===(a=n.get(o))||void 0===a||a.forEach(t=>t()))})},subscribe:function(t,e){var r;const o=null!==(r=n.get(t))&&void 0!==r?r:new Set;return o.add(e),n.set(t,o),()=>o.delete(e)},reset:function(){t.clear(),e.clear(),n.forEach(t=>{t.forEach(t=>t())})}}}(),p=r(u);null===(a=t.intents)||void 0===a||a.call(t,p);const m=[],h=[];let w=null,b=null,y=!1,v=new Set;function g(){const e=f.getState();if(e===w&&b)return b;const n={};var r;return t.computed&&(r=t.computed,Object.keys(r)).forEach(r=>{n[r]=d.compute(r,t.computed[r],e)}),w=e,b=Object.assign({},e,n),b}function E(t){const e=f.getState();f.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&(y?v.add(t):d.invalidate(t))})}const S={scope:u,get state(){return g()},setState:E,reset:function(){var t;f.setState(()=>structuredClone(l)),null===(t=d.reset)||void 0===t||t.call(d),w=null,b=null},batch:function(t){y=!0;try{t()}finally{y=!1,v.forEach(t=>d.invalidate(t)),v.clear()}},async emit(t,e){const n=g();m.forEach(r=>r({intent:t,payload:e,state:n,scope:u}));try{await p.emit(t,e,{getState:g,setState:E,emit:S.emit})}finally{h.forEach(n=>n({intent:t,payload:e,state:g(),scope:u}))}},subscribe:f.subscribe,onIntent(t,e){p.on(t,e)},use:p.use,__internal:{onEmitStart(t){m.push(t)},onEmitEnd(t){h.push(t)}}};let C;return null===(s=t.plugins)||void 0===s||s.forEach(t=>t.setup(S)),i&&(C=o(S),C.wrap(S)),{...S,devtools:C}}}}function l(...t){return{create(e){const n=t.map(t=>{var n;if("logic"in t){const r=t.logic.create(null!==(n=t.namespace)&&void 0!==n?n:e);return{namespace:t.namespace,inst:r}}return{namespace:null,inst:t.create(e)}});return{get state(){const t={};for(const{namespace:e,inst:r}of n){const n=r.state;e?t[e]=n:Object.assign(t,n)}return t},async emit(t,e){const r=n.map(({inst:n})=>{var r;return null===(r=n.emit)||void 0===r?void 0:r.call(n,t,e)});return Promise.all(r.filter(Boolean).map(t=>t.catch(t=>console.error(t))))},subscribe(t){const e=n.map(e=>e.inst.subscribe(t));return()=>{e.forEach(t=>t())}}}}}}function f(){let t=null;return e=>async n=>{null==t||t.abort(),t=new AbortController;const r=t.signal;if(!r.aborted)try{return await e({...n,signal:r})}catch(t){if(r.aborted)return;throw t}}}function d(t){let e;return n=>r=>new Promise((o,a)=>{clearTimeout(e),e=setTimeout(()=>{Promise.resolve(n(r)).then(o,a)},t)})}function p(t=3){return e=>async n=>{var r;let o;for(let a=0;a<t;a++)try{return await e(n)}catch(t){if(null===(r=n.signal)||void 0===r?void 0:r.aborted)return;o=t}throw o}}function m(t){const e=[t];let n=!1;const r=t=>{return(n=!0,r=e,t=>r.reduceRight((t,e)=>e(t),t))(t);var r};return r.takeLatest=()=>{if(n)throw new Error("Effect already built");return e.push(f()),r},r.debounce=t=>{if(n)throw new Error("Effect already built");return e.push(d(t)),r},r.retry=(t=3)=>{if(n)throw new Error("Effect already built");return e.push(p(t)),r},r}function h(t,e=Object.is){let n,r=null;return o=>{if(null!==r){const a=t(o);return e(n,a)?n:(n=a,r=o,a)}return r=o,n=t(o),n}}function w(t){let e=structuredClone(t);const n=r("backend"),o={state:()=>e,setState(t){t(e)},async emit(t,r){await n.emit(t,r,{getState:()=>e,setState:t=>t(e),emit:o.emit})},onIntent:n.on,effect:n.effect,use:n.use,reset(){e=structuredClone(t)}};return o}function b(n,r){var o,a;const s=o=>{const a=e.useRef(null);a.current||(a.current=n.create());const s=a.current,c=e.useSyncExternalStore(s.subscribe,()=>s.state,()=>s.state),i=e.useCallback((t,e)=>s.emit(t,e),[s]);return t(r,{...o,state:c,emit:i})};return s.displayName=`withLogic(${null!==(a=null!==(o=r.displayName)&&void 0!==o?o:r.name)&&void 0!==a?a:"View"})`,s}export{o as attachDevtools,l as composeLogic,w as createBackendRuntime,u as createLogic,h as createSelector,n as createSignal,d as debounce,m as effect,p as retry,f as takeLatest,b as withLogic};
@@ -1,10 +1,6 @@
1
1
  import * as React from "react";
2
- /**
3
- * Logic instance surface for React adapter
4
- * (matches LogicRuntime)
5
- */
6
2
  type LogicInstance<S> = {
7
- state(): Readonly<S>;
3
+ readonly state: Readonly<S>;
8
4
  emit(intent: string, payload?: any): Promise<void>;
9
5
  subscribe(fn: () => void): () => void;
10
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logic-runtime-react-z",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Intent-first runtime for React. No hooks. Deterministic state. Orchestrated effects.",
5
5
  "license": "MIT",
6
6
  "author": "Delpi.Kye",