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 +282 -231
- package/build/core/effect.d.ts +2 -16
- package/build/core/index.d.ts +3 -0
- package/build/core/intent.d.ts +6 -6
- package/build/core/middleware.d.ts +2 -0
- package/build/core/types.d.ts +14 -8
- package/build/{core → devtools}/devtools.d.ts +1 -7
- package/build/devtools/index.d.ts +2 -0
- package/build/{core → devtools}/timeline.d.ts +0 -15
- package/build/index.cjs.js +1 -1
- package/build/index.d.ts +15 -8
- package/build/index.esm.js +1 -1
- package/build/{core/plugin.d.ts → plugins/index.d.ts} +1 -1
- package/build/react/withLogic.d.ts +1 -5
- package/build/runtime/backend.d.ts +29 -0
- package/build/{core → runtime}/compose.d.ts +2 -11
- package/build/runtime/index.d.ts +3 -0
- package/build/{core → runtime}/logic.d.ts +2 -2
- package/build/state/index.d.ts +4 -0
- package/build/{core → state}/store.d.ts +1 -1
- package/package.json +1 -1
- /package/build/{core → state}/computed.d.ts +0 -0
- /package/build/{core → state}/selector.d.ts +0 -0
- /package/build/{core → state}/signal.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,348 +1,399 @@
|
|
|
1
|
-
|
|
1
|
+
# ⚙️ logic-runtime-react-z
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/logic-runtime-react-z)
|
|
4
|
-

|
|
3
|
+
[](https://www.npmjs.com/package/logic-runtime-react-z) 
|
|
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
|
-
|
|
14
|
-
It provides a headless runtime layer that lives outside React, responsible for:
|
|
10
|
+
---
|
|
15
11
|
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
43
|
+
|
|
44
|
+
```bash
|
|
54
45
|
npm install logic-runtime-react-z
|
|
55
46
|
```
|
|
47
|
+
|
|
56
48
|
---
|
|
57
49
|
|
|
58
|
-
##
|
|
50
|
+
## 🚀 Quick Start (Headless)
|
|
51
|
+
|
|
59
52
|
```ts
|
|
60
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
72
|
+
## ⚛️ React Integration (No Hooks in View)
|
|
76
73
|
|
|
77
74
|
```ts
|
|
78
|
-
import { createLogic,
|
|
75
|
+
import { createLogic, effect, withLogic } from "logic-runtime-react-z"
|
|
79
76
|
|
|
80
77
|
interface State {
|
|
81
78
|
count: number;
|
|
82
|
-
|
|
83
|
-
|
|
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", ({
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
144
|
-
```ts
|
|
119
|
+
---
|
|
145
120
|
|
|
146
|
-
|
|
147
|
-
const counterLogic = createLogic({
|
|
148
|
-
state: {
|
|
149
|
-
count: 0
|
|
150
|
-
},
|
|
121
|
+
## 🧪 Middleware Example (Backend)
|
|
151
122
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return state.count * 2
|
|
155
|
-
}
|
|
156
|
-
},
|
|
123
|
+
```ts
|
|
124
|
+
import { createBackendRuntime } from "logic-runtime-react-z"
|
|
157
125
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
s.count = 0
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
},
|
|
146
|
+
runtime.onIntent("logout", ({ setState }) => {
|
|
147
|
+
setState(s => { s.user = null })
|
|
180
148
|
})
|
|
181
149
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
##
|
|
198
|
-
|
|
199
|
-
#### Plugins extend the runtime behavior **without touching business logic**.
|
|
170
|
+
## 🧪 Unit Test Example (Headless)
|
|
200
171
|
|
|
201
172
|
```ts
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
##
|
|
189
|
+
## 🔍 Comparison
|
|
231
190
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
+
runtime.emit("login")
|
|
252
|
+
```
|
|
255
253
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
####
|
|
263
|
+
#### ❌ 2. Calling handlers directly instead of emitting intent
|
|
265
264
|
```ts
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
// ❌ Don't call handlers manually
|
|
266
|
+
loginHandler(payload)
|
|
267
|
+
```
|
|
268
|
+
Why this is wrong
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
throw new Error("Not allowed")
|
|
274
|
-
}
|
|
270
|
+
- Skips middleware & effects
|
|
271
|
+
- Breaks devtools timeline
|
|
272
|
+
- Makes behavior non-deterministic
|
|
275
273
|
|
|
276
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
296
|
-
- Every intent is recorded
|
|
297
|
-
- Replayable
|
|
298
|
-
- Deterministic async flows
|
|
296
|
+
✅ Correct
|
|
299
297
|
|
|
300
298
|
```ts
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
318
|
+
- Intent should describe user or system intention
|
|
319
|
+
- Not raw state mutation
|
|
308
320
|
|
|
309
|
-
|
|
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
|
-
|
|
340
|
+
✅ Correct
|
|
321
341
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
357
|
+
- Behavior split across layers
|
|
358
|
+
- Impossible to replay or test headlessly
|
|
332
359
|
|
|
333
|
-
|
|
360
|
+
✅ Correct
|
|
334
361
|
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/build/core/effect.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/build/core/intent.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { IntentHandler, Effect } from "./types";
|
|
2
|
-
export declare function createIntentBus<W extends object,
|
|
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
|
|
9
|
-
setState
|
|
7
|
+
getState(): R;
|
|
8
|
+
setState(fn: (s: W) => void): void;
|
|
9
|
+
emit(intent: string, payload?: any): Promise<void>;
|
|
10
10
|
}) => Promise<void>;
|
|
11
11
|
};
|
package/build/core/types.d.ts
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
import type { Timeline } from "
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|
|
@@ -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;
|
package/build/index.cjs.js
CHANGED
|
@@ -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)
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
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";
|
package/build/index.esm.js
CHANGED
|
@@ -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
|
|
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,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
|
-
}
|
|
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 |
|
|
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
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LogicRuntime, IntentHandler, Effect } from "
|
|
2
|
-
import
|
|
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;
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|