logic-runtime-react-z 1.0.0 → 2.0.0-z

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,348 +1,399 @@
1
- ## 🔀 logic-runtime-react-z
1
+ # ⚙️ logic-runtime-react-z
2
2
 
3
- [![NPM](https://img.shields.io/npm/v/logic-runtime-react-z.svg)](https://www.npmjs.com/package/logic-runtime-react-z)
4
- ![Downloads](https://img.shields.io/npm/dt/logic-runtime-react-z.svg)
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)
5
4
 
6
5
  <a href="https://codesandbox.io/p/sandbox/x3jf32" target="_blank">LIVE EXAMPLE</a>
7
6
 
8
- ---
9
-
10
- **logic-runtime-react-z** is an intent-first external runtime,
11
- designed to run business logic **outside React**.
7
+ **Intent-first business logic runtime**
8
+ React is a view. Logic lives elsewhere.
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:
10
+ ---
15
11
 
16
- > Think of it as:
17
- > **“Business logic runtime outside React — React is just the view.”**
12
+ ## Core Idea
18
13
 
19
- ---
14
+ > **Business logic lives outside React. React only renders state and emits intent.**
20
15
 
21
- ## 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
25
- - Async flows are complex (login → fetch → redirect)
26
- - Side effects must be predictable & testable
27
- - You want headless tests without rendering
28
- - You prefer architecture-driven design over component-driven logic
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
29
21
 
30
22
  ---
31
23
 
32
24
  ## 🧠 Mental Model
33
25
 
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
26
  ```
46
-
47
- - React is just an adapter.
48
- - The runtime owns behavior.
27
+ UI / HTTP / Queue / Cron
28
+
29
+ emit(intent)
30
+
31
+ middleware / effects
32
+
33
+ intent handlers
34
+
35
+ mutate state
36
+
37
+ computed / subscribers
38
+ ```
49
39
 
50
40
  ---
51
41
 
52
42
  ## 📦 Installation
53
- ```ts
43
+
44
+ ```bash
54
45
  npm install logic-runtime-react-z
55
46
  ```
47
+
56
48
  ---
57
49
 
58
- ## Basic
50
+ ## 🚀 Quick Start (Headless)
51
+
59
52
  ```ts
60
- const logic = createLogic({
53
+ import { createLogic } from "logic-runtime-react-z"
54
+
55
+ const counterLogic = createLogic({
61
56
  state: { count: 0 },
62
57
  intents: bus => {
63
- bus.on("inc", ({ setState }) => {
64
- setState(s => { s.count++ })
65
- })
66
- }
58
+ bus.on("inc", ({ setState }) => setState(s => { s.count++ }))
59
+ bus.on("add", ({ payload, setState }) => setState(s => { s.count += payload }))
60
+ },
67
61
  })
68
62
 
69
- const runtime = logic.create()
70
- runtime.emit("inc")
63
+ const runtime = counterLogic.create()
64
+ await runtime.emit("inc")
65
+ await runtime.emit("add", 5)
66
+ console.log(runtime.state.count) // 6
67
+
71
68
  ```
72
69
 
73
70
  ---
74
71
 
75
- ## ⚛️ React Usage (No Hooks in View)
72
+ ## ⚛️ React Integration (No Hooks in View)
76
73
 
77
74
  ```ts
78
- import { createLogic, withLogic, createSelector } from "logic-runtime-react-z"
75
+ import { createLogic, effect, withLogic } from "logic-runtime-react-z"
79
76
 
80
77
  interface State {
81
78
  count: number;
82
-
83
- // computed
84
- double: number
79
+ loading: boolean;
80
+ double: number;
85
81
  }
86
82
 
83
+ // Async effect for takeLatest behavior
84
+ const asyncEffect = effect(async ({ payload, setState }) => {
85
+ console.log("Effect fired for payload:", payload);
86
+ }).takeLatest()
87
+
87
88
  const counterLogic = createLogic({
88
89
  name: "counter",
89
- state: { count: 1 },
90
-
91
- computed: {
92
- double({ state }): number {
93
- return state.count * 2
94
- }
95
- },
96
-
90
+ state: { count: 1, loading: false },
91
+ computed: { double: ({ state }) => state.count * 2 },
97
92
  intents: bus => {
98
- bus.on("inc", ({ state, setState }) => {
99
- // if (!selectIsAdult(state)) {
100
- // throw new Error("Not allowed")
101
- // }
102
-
103
- setState(s => {
104
- s.count++
105
- })
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 })
106
98
  })
99
+ bus.effect("inc-async", asyncEffect)
107
100
  },
108
101
  })
109
102
 
110
- // Pure View (No Hooks)
111
- function CounterView({
112
- state,
113
- emit,
114
- }: {
115
- state: State
116
- emit: (intent: string) => void
117
- }) {
103
+ // React view (pure, no hooks)
104
+ function CounterView({ state, emit }: { state: State; emit: (intent: string, payload?: any) => void | Promise<void> }) {
118
105
  return (
119
106
  <div>
120
107
  <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>
108
+ <button disabled={state.loading} onClick={() => emit("inc")}>Plus</button>
109
+ <button disabled={state.loading} onClick={() => emit("inc-async", 100)}>Async +100</button>
133
110
  <div>Double: {state.double}</div>
134
- <button onClick={() => emit("inc")}>+</button>
135
111
  </div>
136
112
  )
137
113
  }
138
114
 
139
- // Bind Logic to React
140
115
  export const Counter = withLogic(counterLogic, CounterView)
116
+
141
117
  ```
142
118
 
143
- ## 🧪 No React
144
- ```ts
119
+ ---
145
120
 
146
- // import { createLogic } from "logic-runtime-react-z"
147
- const counterLogic = createLogic({
148
- state: {
149
- count: 0
150
- },
121
+ ## 🧪 Middleware Example (Backend)
151
122
 
152
- computed: {
153
- double({ state }) {
154
- return state.count * 2
155
- }
156
- },
123
+ ```ts
124
+ import { createBackendRuntime } from "logic-runtime-react-z"
157
125
 
158
- intents: bus => {
159
- bus.on("inc", ({ state, setState }) => {
160
- // state is READONLY snapshot (read-only)
161
- // ❌ state.count++
162
- setState(s => {
163
- s.count += 1
164
- })
165
- })
126
+ // Create runtime with initial state
127
+ const runtime = createBackendRuntime({
128
+ user: null,
129
+ loading: false,
130
+ })
166
131
 
167
- // Intent handlers can be sync or async
168
- bus.on("add", ({ payload, setState }) => {
169
- setState(s => {
170
- s.count += payload
171
- })
172
- })
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
+ })
173
145
 
174
- bus.on("reset", ({ setState }) => {
175
- setState(s => {
176
- s.count = 0
177
- })
178
- })
179
- },
146
+ runtime.onIntent("logout", ({ setState }) => {
147
+ setState(s => { s.user = null })
180
148
  })
181
149
 
182
- // initial
183
- // const runtime = counterLogic.create()
184
- const runtime = counterLogic.create("counter:main")
150
+ // Emit some intents
151
+ await runtime.emit("login", { username: "alice", password: "123" })
152
+ await runtime.emit("logout")
153
+
154
+ // ----------------- Using devtools -----------------
185
155
 
186
- runtime.emit("inc")
187
- // await runtime.emit("inc") // if async
188
- runtime.emit("add", 5)
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)
189
165
 
190
- // console.log
191
- console.log(runtime.state.count) // 7
192
- console.log(runtime.state.double) // 14
193
166
  ```
194
167
 
195
168
  ---
196
169
 
197
- ## 🧩 Plugins Runtime extensions (cross-cutting concerns)
198
-
199
- #### Plugins extend the runtime behavior **without touching business logic**.
170
+ ## 🧪 Unit Test Example (Headless)
200
171
 
201
172
  ```ts
202
- export const authPlugin: LogicPlugin = {
203
- name: "auth",
204
-
205
- setup(runtime) {
206
- runtime.onIntent("delete", ctx => {
207
- if (!ctx.state.user?.isAdmin) {
208
- throw new Error("Forbidden")
209
- }
210
- })
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 }))
211
178
  }
212
- }
179
+ })
213
180
 
214
- export const persistPlugin: LogicPlugin = {
215
- name: "persist",
181
+ const runtime = logic.create()
182
+ await runtime.emit("set", 4)
183
+ expect(runtime.state.squared).toBe(16)
216
184
 
217
- setup(runtime) {
218
- runtime.subscribe(() => {
219
- localStorage.setItem(
220
- runtime.scope,
221
- JSON.stringify(runtime.state)
222
- )
223
- })
224
- }
225
- }
226
185
  ```
227
186
 
228
187
  ---
229
188
 
230
- ## Async Effects + Selectors (Advanced Usage)
189
+ ## 🔍 Comparison
231
190
 
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).
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 | ✅ | ⚠️ | ⚠️ | ⚠️ |
235
200
 
236
- ```ts
237
- import { createSelector } from "logic-runtime-react-z"
238
201
 
239
- const selectIsAdult = createSelector(
240
- (state: { age: number }) => state.age,
241
- age => age >= 18
242
- )
202
+ ---
203
+
204
+ ## ⚖️ Comparison with Vue2
205
+
206
+ While logic-runtime-react-z uses a **reactive + computed pattern** similar to Vue2, the behavior is quite different:
207
+
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 |
217
+
218
+ > **Takeaway:** It feels familiar if you know Vue2 reactivity, but under the hood it's **intent-first, headless, and fully testable**, unlike Vue2.
219
+
220
+ ---
221
+
222
+ ## 🚫 Anti-patterns (What NOT to do)
223
+
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
229
+
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
+ }
243
242
  ```
243
+ Why this is wrong
244
+ - Logic tied to React lifecycle
245
+ - Hard to test without rendering
246
+ - Side-effects scattered in UI
244
247
 
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
248
+ Correct
252
249
 
253
250
  ```ts
254
- import { effect, debounce, retry, takeLatest } from "logic-runtime-react-z"
251
+ runtime.emit("login")
252
+ ```
255
253
 
256
- const withLogging = effect(next => {
257
- return async ctx => {
258
- console.log("saving...")
259
- await next(ctx)
260
- }
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)
261
260
  })
262
261
  ```
263
262
 
264
- #### Combining Effects + Selectors in Intents
263
+ #### 2. Calling handlers directly instead of emitting intent
265
264
  ```ts
266
- intents: bus => {
267
- // attach effect to intent
268
- bus.effect("save", withLogging)
265
+ // Don't call handlers manually
266
+ loginHandler(payload)
267
+ ```
268
+ Why this is wrong
269
269
 
270
- // intent handler (business rule)
271
- bus.on("save", ({ state }) => {
272
- if (!selectIsAdult(state)) {
273
- throw new Error("Not allowed")
274
- }
270
+ - Skips middleware & effects
271
+ - Breaks devtools timeline
272
+ - Makes behavior non-deterministic
275
273
 
276
- // perform save logic...
277
- })
278
-
279
- bus.on("save-user", async ({ state, setState }) => {
280
- await api.save(state.form)
281
- setState(s => {
282
- s.saved = true
283
- })
284
- })
274
+ Correct
285
275
 
286
- // bus.effect("save", debounce(300))
287
- // bus.effect("save", retry(2))
288
- // bus.effect("save", takeLatest())
276
+ ```ts
277
+ runtime.emit("login", payload)
278
+ ```
279
+ Intent is the only entry point. Always.
289
280
 
290
- }
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
+ })
291
288
  ```
292
289
 
293
- ---
290
+ Why this is wrong
291
+
292
+ - Effects are orchestration, not business logic
293
+ - Hard to reason about ordering
294
+ - Blurs responsibility
294
295
 
295
- ## 🧭 Devtools & Timeline
296
- - Every intent is recorded
297
- - Replayable
298
- - Deterministic async flows
296
+ Correct
299
297
 
300
298
  ```ts
301
- // devtool process.env.NODE_ENV !== "production"
302
- runtime.devtools?.timeline.replay(runtime.emit)
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 })
303
314
  ```
304
315
 
305
- ---
316
+ Why this is wrong
306
317
 
307
- ## Props
318
+ - Intent should describe user or system intention
319
+ - Not raw state mutation
308
320
 
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.). |
321
+ Correct
316
322
 
323
+ ```ts
324
+ emit("login:start")
325
+ emit("login:success", user)
326
+ emit("login:failed", error)
327
+ ```
328
+ Intents are verbs, not patches.
317
329
 
318
- ---
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
319
339
 
320
- ## 🔍 Comparison
340
+ Correct
321
341
 
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 | ✅ | ⚠️ | ❌ |
342
+ ```ts
343
+ emit("update:user:name", "admin")
344
+ ```
345
+
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
330
356
 
331
- > This comparison focuses on **architecture and mental model**, not ecosystem size.
357
+ - Behavior split across layers
358
+ - Impossible to replay or test headlessly
332
359
 
333
- ---
360
+ ✅ Correct
334
361
 
335
- ## 🚫 What this library is NOT
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
336
384
 
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)
385
+ Correct
341
386
 
342
- It is a **behavior runtime**.
387
+ ```ts
388
+ composeLogic(
389
+ userLogic,
390
+ cartLogic,
391
+ productLogic
392
+ )
393
+ ```
343
394
 
344
395
  ---
345
396
 
346
397
  ## 📜 License
347
398
 
348
- MIT
399
+ MIT / Delpi
@@ -1,20 +1,6 @@
1
- import type { Effect } from "./types";
1
+ import type { Effect, EffectCtx } 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
- export type EffectBuilder<W, R> = Effect<W, R> & {
16
- takeLatest(): EffectBuilder<W, R>;
17
- debounce(ms: number): EffectBuilder<W, R>;
18
- retry(count?: number): EffectBuilder<W, R>;
19
- };
20
- export declare function effect<W extends object = any, R extends object = W>(fx: Effect<W, R>): EffectBuilder<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;
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./intent";
3
+ export * from "./effect";
@@ -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;
5
+ use: (mw: IntentMiddleware<W, R, P>) => void;
7
6
  emit: (intent: string, payload: P, ctx: {
8
- getState: () => R;
9
- setState: (fn: (s: W) => void) => void;
7
+ getState(): R;
8
+ setState(fn: (s: W) => void): void;
9
+ emit(intent: string, payload?: any): Promise<void>;
10
10
  }) => Promise<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>;
@@ -1,32 +1,38 @@
1
- import type { Timeline } from "./timeline";
1
+ import type { Timeline } from "../devtools/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
  };
11
+ export type EmitHookCtx<S = any, C = any, P = any> = {
12
+ intent: string;
13
+ payload: P;
14
+ state: Readonly<S & C>;
15
+ scope: string;
16
+ };
12
17
  export type IntentHandler<W = any, R = W, P = any> = (ctx: EffectCtx<W, R, P>) => void | Promise<void>;
13
18
  export type Effect<W = any, R = W, P = any> = (next: IntentHandler<W, R, P>) => IntentHandler<W, R, P>;
19
+ export type IntentMiddleware<W = any, R = W, P = any> = (ctx: EffectCtx<W, R, P>, next: () => Promise<void>) => Promise<void>;
14
20
  export type LogicRuntime<S extends object, C extends object = {}> = {
15
21
  scope: string;
16
- /** full snapshot */
17
22
  state(): Readonly<S & C>;
18
- /** base state only */
19
23
  setState(mutator: (s: S) => void): void;
20
24
  reset(): void;
21
- /** await full intent pipeline */
25
+ batch(fn: () => void): void;
22
26
  emit(intent: string, payload?: any): Promise<void>;
27
+ effect(intent: string, fx: Effect<S, S & C>): void;
28
+ use(mw: IntentMiddleware<S, S & C>): void;
23
29
  subscribe(fn: Listener): () => void;
24
30
  onIntent(intent: string, handler: IntentHandler<S, S & C>): void;
25
31
  devtools?: {
26
32
  timeline: Timeline;
27
33
  };
28
34
  __internal: {
29
- onEmitStart(fn: any): void;
30
- onEmitEnd(fn: any): void;
35
+ onEmitStart(fn: (ctx: EmitHookCtx<S, S & C, any>) => void): void;
36
+ onEmitEnd(fn: (ctx: EmitHookCtx<S, S & C, any>) => void): void;
31
37
  };
32
38
  };
@@ -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;
14
- emit(intent: string, payload?: any): Promise<void>;
15
8
  state(): any;
9
+ emit(intent: string, payload?: any): Promise<void>;
16
10
  };
17
11
  export declare function attachDevtools(target: RuntimeLike): Devtools;
18
12
  export {};
@@ -0,0 +1,2 @@
1
+ export { attachDevtools } from "./devtools";
2
+ export * from "./timeline";
@@ -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 +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 s(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)}}}function c(){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 s=new Set,c=r({state:new Proxy(o,{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(r){e.forEach((e,o)=>{var s;e.has(r)&&(t.delete(o),null===(s=n.get(o))||void 0===s||s.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()))}}}function a(t){const e=new Map,n=new Map,r=[],o=new Map;return{on:function(t,n){var r;const o=null!==(r=e.get(t))&&void 0!==r?r:[];o.push(n),e.set(t,o)},effect:function(t,e){var r;const o=null!==(r=n.get(t))&&void 0!==r?r:[];o.push(e),n.set(t,o)},use:function(t){r.push(t)},emit:async function(s,c,a){var i,u;const l=`${t}:${s}`;null===(i=o.get(l))||void 0===i||i.abort();const f=new AbortController;o.set(l,f);const d=Array.from(e.entries()).filter(([t])=>function(t,e){return t.endsWith("/*")?e.startsWith(t.slice(0,-2)):t===e}(t,s)).flatMap(([,t])=>t),p=null!==(u=n.get(s))&&void 0!==u?u:[];for(const e of d){let n=e;for(let t=p.length-1;t>=0;t--)n=p[t](n);const o={state:a.getState(),payload:c,signal:f.signal,scope:t,emit:a.emit,setState:a.setState};let s=-1;const i=async t=>{if(t<=s)throw new Error("next() called multiple times");s=t;const e=r[t];if(!e)return n(o);await e(o,()=>i(t+1))};await i(0)}}}}function i(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:s}=null!=n?n:{},c=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!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(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 u=0;const l="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 f(t){return e=>t.reduceRight((t,e)=>e(t),e)}function d(){let t=null;return e=>async n=>{if(null==t||t.abort(),t=new AbortController,!t.signal.aborted)return e({...n,signal:t.signal})}}function p(t){let e;return n=>r=>new Promise(o=>{clearTimeout(e),e=setTimeout(()=>o(n(r)),t)})}function m(t=3){return e=>async n=>{let r;for(let o=0;o<t;o++)try{return await e(n)}catch(t){if(n.signal.aborted)return;r=t}throw r}}exports.attachDevtools=i,exports.composeEffects=f,exports.composeLogic=function(...t){return{create(e){const n=t.map(t=>{var n,r;if("logic"in t){const o=t.logic.create(null!==(n=t.namespace)&&void 0!==n?n:e);return{namespace:null!==(r=t.namespace)&&void 0!==r?r:null,inst:o}}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(n=>{var r,o;return null===(o=(r=n.inst).emit)||void 0===o?void 0:o.call(r,t,e)}).filter(Boolean);await Promise.all(r)},subscribe(t){const e=n.map(e=>e.inst.subscribe(t));return()=>e.forEach(t=>t())}}}}},exports.composeMiddleware=function(t){return function(e,n){let r=-1;return function o(s){if(s<=r)return Promise.reject(new Error("next() called multiple times"));r=s;const c=t[s]||n;return c?Promise.resolve(c(e,()=>o(s+1))):Promise.resolve()}(0)}},exports.createBackendRuntime=function(t){var e,n;let r=structuredClone(t);const o=a("backend"),s={state:()=>r,setState:function(t){},batch:function(t){},async emit(t,e){},onIntent:o.on,effect:o.effect,use:o.use,registerIntents:function(t){},__internal:{onEmitStart(t){},onEmitEnd(t){}},reset:function(){}};if("production"!==(null===(n=null===(e=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===e?void 0:e.env)||void 0===n?void 0:n.NODE_ENV)){const t=i(s);t.wrap(s),s.devtools=t}return s},exports.createComputed=c,exports.createIntentBus=a,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}:${++u}`,d=structuredClone(t.state),p=s(structuredClone(t.state)),m=c(),v=a(f);null===(r=t.intents)||void 0===r||r.call(t,v);let b=null,g=null,h=!1,w=new Set;const y=[],E=[];function S(){const e=p.getState();if(e===b&&g)return g;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,g=Object.assign({},e,n),g}function x(t){const e=p.getState();p.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&(h?w.add(t):m.invalidate(t))})}const j={scope:f,state:()=>S(),setState:x,reset(){!function(){var t;p.setState(()=>structuredClone(d)),null===(t=m.reset)||void 0===t||t.call(m),b=null,g=null}()},batch:function(t){h=!0;try{t()}finally{h=!1,w.forEach(t=>m.invalidate(t)),w.clear()}},use:v.use,effect:v.effect,async emit(t,e){const n=S();y.forEach(r=>r({intent:t,payload:e,state:n,scope:f}));try{await v.emit(t,e,{getState:S,setState:x,emit:j.emit})}finally{E.forEach(n=>n({intent:t,payload:e,state:S(),scope:f}))}},subscribe:p.subscribe,onIntent(t,e){v.on(t,e)},__internal:{onEmitStart(t){y.push(t)},onEmitEnd(t){E.push(t)}}};let O;return null===(o=t.plugins)||void 0===o||o.forEach(t=>t.setup(j)),l&&(O=i(j),O.wrap(j)),{...j,devtools:O}}}},exports.createSelector=function(t,e=Object.is){let n,r=null;return o=>{if(null!==r){const s=t(o);return e(n,s)?n:(n=s,r=o,s)}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.createStore=s,exports.debounce=p,exports.effect=function(t){const e=[e=>async n=>{if(!n.signal.aborted)return await t(n),e(n)}];let n=!1;const r=t=>(n=!0,f(e))(t);return r.takeLatest=()=>{if(n)throw new Error("Effect already built");return e.push(d()),r},r.debounce=t=>{if(n)throw new Error("Effect already built");return e.push(p(t)),r},r.retry=(t=3)=>{if(n)throw new Error("Effect already built");return e.push(m(t)),r},r},exports.retry=m,exports.takeLatest=d,exports.withLogic=function(e,n,r){var s,c;const a=s=>{const c=o.useRef(null);c.current||(c.current=e.create(r));const a=c.current,i=o.useSyncExternalStore(a.subscribe,a.state,a.state),u=o.useCallback((t,e)=>a.emit(t,e),[a]);return t.jsx(n,{...s,state:i,emit:u})};return a.displayName=`withLogic(${null!==(c=null!==(s=n.displayName)&&void 0!==s?s:n.name)&&void 0!==c?c:"View"})`,a};
package/build/index.d.ts CHANGED
@@ -1,9 +1,16 @@
1
- export { createSignal } from "./core/signal";
2
- export { createLogic } from "./core/logic";
3
- export { composeLogic } from "./core/compose";
4
- export { effect, takeLatest, debounce, retry } from "./core/effect";
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";
1
+ export { createLogic } from "./runtime/logic";
2
+ export { composeLogic } from "./runtime/compose";
3
+ export { createBackendRuntime } from "./runtime/backend";
4
+ export type { SimpleBackendCtx } from "./runtime/backend";
5
+ export { createSignal } from "./state/signal";
6
+ export { createStore } from "./state/store";
7
+ export { createComputed } from "./state/computed";
8
+ export { createSelector } from "./state/selector";
9
+ export type { Subscriber } from "./state/signal";
10
+ export { effect, takeLatest, debounce, retry, composeEffects } from "./core/effect";
11
+ export { createIntentBus } from "./core/intent";
12
+ export { compose as composeMiddleware } from "./core/middleware";
13
+ export type { LogicRuntime, IntentHandler, Effect, EffectCtx, IntentMiddleware, Listener, } from "./core/types";
14
+ export { attachDevtools } from "./devtools/devtools";
15
+ export type { Timeline, IntentRecord, } from "./devtools/timeline";
9
16
  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{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)}}}function r(){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 s=new Set,i=r({state:new Proxy(o,{get:(t,e,n)=>("string"==typeof e&&e in t&&s.add(e),Reflect.get(t,e,n))})});return t.set(n,i),e.set(n,s),i},invalidate:function(r){e.forEach((e,o)=>{var s;e.has(r)&&(t.delete(o),null===(s=n.get(o))||void 0===s||s.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()))}}}function o(t,e=Object.is){let n,r=null;return o=>{if(null!==r){const s=t(o);return e(n,s)?n:(n=s,r=o,s)}return r=o,n=t(o),n}}function s(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 i(t){const e=new Map,n=new Map,r=[],o=new Map;return{on:function(t,n){var r;const o=null!==(r=e.get(t))&&void 0!==r?r:[];o.push(n),e.set(t,o)},effect:function(t,e){var r;const o=null!==(r=n.get(t))&&void 0!==r?r:[];o.push(e),n.set(t,o)},use:function(t){r.push(t)},emit:async function(s,i,a){var c,l;const u=`${t}:${s}`;null===(c=o.get(u))||void 0===c||c.abort();const f=new AbortController;o.set(u,f);const d=Array.from(e.entries()).filter(([t])=>function(t,e){return t.endsWith("/*")?e.startsWith(t.slice(0,-2)):t===e}(t,s)).flatMap(([,t])=>t),p=null!==(l=n.get(s))&&void 0!==l?l:[];for(const e of d){let n=e;for(let t=p.length-1;t>=0;t--)n=p[t](n);const o={state:a.getState(),payload:i,signal:f.signal,scope:t,emit:a.emit,setState:a.setState};let s=-1;const c=async t=>{if(t<=s)throw new Error("next() called multiple times");s=t;const e=r[t];if(!e)return n(o);await e(o,()=>c(t+1))};await c(0)}}}}function a(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:s}=null!=n?n:{},i=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!s||t.scope===s));for(const e of i){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 c,l;let u=0;const f="production"!==(null===(l=null===(c=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===c?void 0:c.env)||void 0===l?void 0:l.NODE_ENV);function d(t){var e;const o=null!==(e=t.name)&&void 0!==e?e:"logic";return{create(e){var s,c;const l=null!=e?e:`${o}:${++u}`,d=structuredClone(t.state),p=n(structuredClone(t.state)),m=r(),v=i(l);null===(s=t.intents)||void 0===s||s.call(t,v);let b=null,h=null,g=!1,w=new Set;const y=[],E=[];function S(){const e=p.getState();if(e===b&&h)return h;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,h=Object.assign({},e,n),h}function C(t){const e=p.getState();p.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&(g?w.add(t):m.invalidate(t))})}const j={scope:l,state:()=>S(),setState:C,reset(){!function(){var t;p.setState(()=>structuredClone(d)),null===(t=m.reset)||void 0===t||t.call(m),b=null,h=null}()},batch:function(t){g=!0;try{t()}finally{g=!1,w.forEach(t=>m.invalidate(t)),w.clear()}},use:v.use,effect:v.effect,async emit(t,e){const n=S();y.forEach(r=>r({intent:t,payload:e,state:n,scope:l}));try{await v.emit(t,e,{getState:S,setState:C,emit:j.emit})}finally{E.forEach(n=>n({intent:t,payload:e,state:S(),scope:l}))}},subscribe:p.subscribe,onIntent(t,e){v.on(t,e)},__internal:{onEmitStart(t){y.push(t)},onEmitEnd(t){E.push(t)}}};let M;return null===(c=t.plugins)||void 0===c||c.forEach(t=>t.setup(j)),f&&(M=a(j),M.wrap(j)),{...j,devtools:M}}}}function p(...t){return{create(e){const n=t.map(t=>{var n,r;if("logic"in t){const o=t.logic.create(null!==(n=t.namespace)&&void 0!==n?n:e);return{namespace:null!==(r=t.namespace)&&void 0!==r?r:null,inst:o}}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(n=>{var r,o;return null===(o=(r=n.inst).emit)||void 0===o?void 0:o.call(r,t,e)}).filter(Boolean);await Promise.all(r)},subscribe(t){const e=n.map(e=>e.inst.subscribe(t));return()=>e.forEach(t=>t())}}}}}function m(t){return e=>t.reduceRight((t,e)=>e(t),e)}function v(){let t=null;return e=>async n=>{if(null==t||t.abort(),t=new AbortController,!t.signal.aborted)return e({...n,signal:t.signal})}}function b(t){let e;return n=>r=>new Promise(o=>{clearTimeout(e),e=setTimeout(()=>o(n(r)),t)})}function h(t=3){return e=>async n=>{let r;for(let o=0;o<t;o++)try{return await e(n)}catch(t){if(n.signal.aborted)return;r=t}throw r}}function g(t){const e=[e=>async n=>{if(!n.signal.aborted)return await t(n),e(n)}];let n=!1;const r=t=>(n=!0,m(e))(t);return r.takeLatest=()=>{if(n)throw new Error("Effect already built");return e.push(v()),r},r.debounce=t=>{if(n)throw new Error("Effect already built");return e.push(b(t)),r},r.retry=(t=3)=>{if(n)throw new Error("Effect already built");return e.push(h(t)),r},r}function w(t){var e,n;let r=structuredClone(t);const o=i("backend");const s={state:()=>r,setState:function(t){},batch:function(t){},async emit(t,e){},onIntent:o.on,effect:o.effect,use:o.use,registerIntents:function(t){},__internal:{onEmitStart(t){},onEmitEnd(t){}},reset:function(){}};if("production"!==(null===(n=null===(e=null===globalThis||void 0===globalThis?void 0:globalThis.process)||void 0===e?void 0:e.env)||void 0===n?void 0:n.NODE_ENV)){const t=a(s);t.wrap(s),s.devtools=t}return s}function y(t){return function(e,n){let r=-1;return function o(s){if(s<=r)return Promise.reject(new Error("next() called multiple times"));r=s;const i=t[s]||n;return i?Promise.resolve(i(e,()=>o(s+1))):Promise.resolve()}(0)}}function E(n,r,o){var s,i;const a=s=>{const i=e.useRef(null);i.current||(i.current=n.create(o));const a=i.current,c=e.useSyncExternalStore(a.subscribe,a.state,a.state),l=e.useCallback((t,e)=>a.emit(t,e),[a]);return t(r,{...s,state:c,emit:l})};return a.displayName=`withLogic(${null!==(i=null!==(s=r.displayName)&&void 0!==s?s:r.name)&&void 0!==i?i:"View"})`,a}export{a as attachDevtools,m as composeEffects,p as composeLogic,y as composeMiddleware,w as createBackendRuntime,r as createComputed,i as createIntentBus,d as createLogic,o as createSelector,s as createSignal,n as createStore,b as debounce,g as effect,h as retry,v as takeLatest,E as withLogic};
@@ -1,4 +1,4 @@
1
- import type { LogicRuntime } from "./types";
1
+ import type { LogicRuntime } from "../core";
2
2
  export type LogicPlugin<S extends object, C extends object = {}> = {
3
3
  name: string;
4
4
  setup(runtime: LogicRuntime<S, C>): void;
@@ -1,8 +1,4 @@
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
3
  state(): Readonly<S>;
8
4
  emit(intent: string, payload?: any): Promise<void>;
@@ -14,5 +10,5 @@ type LogicFactory<S> = {
14
10
  export declare function withLogic<S, P extends object>(logic: LogicFactory<S>, View: React.ComponentType<P & {
15
11
  state: Readonly<S>;
16
12
  emit: (intent: string, payload?: any) => Promise<void>;
17
- }>): React.FC<Omit<P, "state" | "emit">>;
13
+ }>, scope?: string): React.FC<Omit<P, "state" | "emit">>;
18
14
  export {};
@@ -0,0 +1,29 @@
1
+ export type SimpleBackendCtx<S> = {
2
+ state: Readonly<S>;
3
+ set: (patch: Partial<S>) => void;
4
+ emit: (intent: string, payload?: any) => Promise<void>;
5
+ signal: AbortSignal;
6
+ };
7
+ export declare function createBackendRuntime<S extends object>(initial: S): {
8
+ state(): S;
9
+ setState: (mutator: (s: S) => void) => void;
10
+ batch: (fn: () => void) => void;
11
+ emit(intent: string, payload?: any): Promise<void>;
12
+ onIntent: (intent: string, handler: import("../core").IntentHandler<S, S, any>) => void;
13
+ effect: (intent: string, fx: import("../core").Effect<S, S, any>) => void;
14
+ use: (mw: import("../core").IntentMiddleware<S, S, any>) => void;
15
+ registerIntents: (intentsObj: Record<string, (ctx: SimpleBackendCtx<S>) => void | Promise<void>>) => void;
16
+ __internal: {
17
+ onEmitStart(fn: (ctx: {
18
+ intent: string;
19
+ payload: any;
20
+ state: S;
21
+ }) => void): void;
22
+ onEmitEnd(fn: (ctx: {
23
+ intent: string;
24
+ payload: any;
25
+ state: S;
26
+ }) => void): void;
27
+ };
28
+ reset: () => void;
29
+ };
@@ -1,6 +1,6 @@
1
1
  type LogicInstance = {
2
2
  state(): any;
3
- emit?: (intent: string, payload?: any) => any | Promise<any>;
3
+ emit?: (intent: string, payload?: any) => Promise<any> | any;
4
4
  subscribe(fn: () => void): () => void;
5
5
  };
6
6
  type ComposedLogic = {
@@ -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
- emit(intent: string, payload?: any): Promise<any[]>;
24
- /**
25
- * subscription fan-out
26
- */
17
+ emit(intent: string, payload?: any): Promise<void>;
27
18
  subscribe(fn: () => void): () => void;
28
19
  };
29
20
  };
@@ -0,0 +1,3 @@
1
+ export { createLogic } from "./logic";
2
+ export { createBackendRuntime } from "./backend";
3
+ export { composeLogic } from "./compose";
@@ -1,5 +1,5 @@
1
- import type { LogicRuntime, IntentHandler, Effect } from "./types";
2
- import type { LogicPlugin } from "./plugin";
1
+ import type { LogicRuntime, IntentHandler, Effect } from "../core/types";
2
+ import { LogicPlugin } from "../plugins";
3
3
  type ComputedDef<S, C> = {
4
4
  [K in keyof C]: (ctx: {
5
5
  state: S;
@@ -0,0 +1,4 @@
1
+ export { createStore } from "./store";
2
+ export { createComputed } from "./computed";
3
+ export { createSelector } from "./selector";
4
+ export { createSignal } from "./signal";
@@ -1,4 +1,4 @@
1
- import { Listener } from "./types";
1
+ import { Listener } from "../core/types";
2
2
  export declare function createStore<S extends object>(initial: S): {
3
3
  getState: () => S;
4
4
  setState: (mutator: (s: S) => void) => void;
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-z",
4
4
  "description": "Intent-first runtime for React. No hooks. Deterministic state. Orchestrated effects.",
5
5
  "license": "MIT",
6
6
  "author": "Delpi.Kye",
File without changes
File without changes
File without changes