logic-runtime-react-z 1.0.1 → 2.0.1
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 +283 -233
- package/build/core/effect.d.ts +2 -7
- package/build/core/index.d.ts +3 -0
- package/build/core/intent.d.ts +5 -4
- package/build/core/middleware.d.ts +2 -0
- package/build/core/types.d.ts +5 -1
- package/build/devtools/index.d.ts +2 -0
- 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 -1
- package/build/runtime/backend.d.ts +29 -0
- package/build/{core → runtime}/compose.d.ts +2 -2
- 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 → devtools}/devtools.d.ts +1 -1
- /package/build/{core → devtools}/timeline.d.ts +0 -0
- /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,349 +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
|
-
|
|
166
|
-
// emit("inc") ❌ no nested
|
|
167
|
-
})
|
|
126
|
+
// Create runtime with initial state
|
|
127
|
+
const runtime = createBackendRuntime({
|
|
128
|
+
user: null,
|
|
129
|
+
loading: false,
|
|
130
|
+
})
|
|
168
131
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
})
|
|
175
145
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
s.count = 0
|
|
179
|
-
})
|
|
180
|
-
})
|
|
181
|
-
},
|
|
146
|
+
runtime.onIntent("logout", ({ setState }) => {
|
|
147
|
+
setState(s => { s.user = null })
|
|
182
148
|
})
|
|
183
149
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
150
|
+
// Emit some intents
|
|
151
|
+
await runtime.emit("login", { username: "alice", password: "123" })
|
|
152
|
+
await runtime.emit("logout")
|
|
153
|
+
|
|
154
|
+
// ----------------- Using devtools -----------------
|
|
187
155
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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)
|
|
191
165
|
|
|
192
|
-
// console.log
|
|
193
|
-
console.log(runtime.state.count) // 7
|
|
194
|
-
console.log(runtime.state.double) // 14
|
|
195
166
|
```
|
|
196
167
|
|
|
197
168
|
---
|
|
198
169
|
|
|
199
|
-
##
|
|
200
|
-
|
|
201
|
-
#### Plugins extend the runtime behavior **without touching business logic**.
|
|
170
|
+
## 🧪 Unit Test Example (Headless)
|
|
202
171
|
|
|
203
172
|
```ts
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!ctx.state.user?.isAdmin) {
|
|
210
|
-
throw new Error("Forbidden")
|
|
211
|
-
}
|
|
212
|
-
})
|
|
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 }))
|
|
213
178
|
}
|
|
214
|
-
}
|
|
179
|
+
})
|
|
215
180
|
|
|
216
|
-
|
|
217
|
-
|
|
181
|
+
const runtime = logic.create()
|
|
182
|
+
await runtime.emit("set", 4)
|
|
183
|
+
expect(runtime.state.squared).toBe(16)
|
|
218
184
|
|
|
219
|
-
setup(runtime) {
|
|
220
|
-
runtime.subscribe(() => {
|
|
221
|
-
localStorage.setItem(
|
|
222
|
-
runtime.scope,
|
|
223
|
-
JSON.stringify(runtime.state)
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
185
|
```
|
|
229
186
|
|
|
230
187
|
---
|
|
231
188
|
|
|
232
|
-
##
|
|
189
|
+
## 🔍 Comparison
|
|
233
190
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
|
237
200
|
|
|
238
|
-
```ts
|
|
239
|
-
import { createSelector } from "logic-runtime-react-z"
|
|
240
201
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
245
242
|
```
|
|
243
|
+
Why this is wrong
|
|
244
|
+
- Logic tied to React lifecycle
|
|
245
|
+
- Hard to test without rendering
|
|
246
|
+
- Side-effects scattered in UI
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
Effects here are middleware-style intent interceptors, not React effects. They are used for:
|
|
249
|
-
- logging
|
|
250
|
-
- permission checks
|
|
251
|
-
- retries / debounce
|
|
252
|
-
- cancellation
|
|
253
|
-
- async orchestration
|
|
248
|
+
✅ Correct
|
|
254
249
|
|
|
255
250
|
```ts
|
|
256
|
-
|
|
251
|
+
runtime.emit("login")
|
|
252
|
+
```
|
|
257
253
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
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)
|
|
263
260
|
})
|
|
264
261
|
```
|
|
265
262
|
|
|
266
|
-
####
|
|
263
|
+
#### ❌ 2. Calling handlers directly instead of emitting intent
|
|
267
264
|
```ts
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
265
|
+
// ❌ Don't call handlers manually
|
|
266
|
+
loginHandler(payload)
|
|
267
|
+
```
|
|
268
|
+
Why this is wrong
|
|
271
269
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
throw new Error("Not allowed")
|
|
276
|
-
}
|
|
270
|
+
- Skips middleware & effects
|
|
271
|
+
- Breaks devtools timeline
|
|
272
|
+
- Makes behavior non-deterministic
|
|
277
273
|
|
|
278
|
-
|
|
279
|
-
})
|
|
274
|
+
✅ Correct
|
|
280
275
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
})
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// bus.effect("save", debounce(300))
|
|
289
|
-
// bus.effect("save", retry(2))
|
|
290
|
-
// bus.effect("save", takeLatest())
|
|
276
|
+
```ts
|
|
277
|
+
runtime.emit("login", payload)
|
|
278
|
+
```
|
|
279
|
+
Intent is the only entry point. Always.
|
|
291
280
|
|
|
292
|
-
|
|
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
|
+
})
|
|
293
288
|
```
|
|
294
289
|
|
|
295
|
-
|
|
290
|
+
Why this is wrong
|
|
291
|
+
|
|
292
|
+
- Effects are orchestration, not business logic
|
|
293
|
+
- Hard to reason about ordering
|
|
294
|
+
- Blurs responsibility
|
|
296
295
|
|
|
297
|
-
|
|
298
|
-
- Every intent is recorded
|
|
299
|
-
- Replayable
|
|
300
|
-
- Deterministic async flows
|
|
296
|
+
✅ Correct
|
|
301
297
|
|
|
302
298
|
```ts
|
|
303
|
-
|
|
304
|
-
|
|
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 })
|
|
305
314
|
```
|
|
306
315
|
|
|
307
|
-
|
|
316
|
+
Why this is wrong
|
|
308
317
|
|
|
309
|
-
|
|
318
|
+
- Intent should describe user or system intention
|
|
319
|
+
- Not raw state mutation
|
|
310
320
|
|
|
311
|
-
|
|
312
|
-
| ---------- | ---------------------- | ---------------------------------------------------------------------------------- |
|
|
313
|
-
| `name` | `string?` | Optional logic name. Used for debugging, devtools, and default runtime scope. |
|
|
314
|
-
| `state` | `S` | **Base mutable state** (source of truth). Can only be changed via `setState`. |
|
|
315
|
-
| `computed` | `ComputedDef<S, C>?` | **Derived read-only state**, automatically recomputed when base state changes. |
|
|
316
|
-
| `intents` | `(bus) => void` | Defines **business actions** (intent handlers). Intents describe behavior, not UI. |
|
|
317
|
-
| `plugins` | `LogicPlugin<S, C>[]?` | Runtime extensions (devtools, logging, persistence, analytics, etc.). |
|
|
321
|
+
✅ Correct
|
|
318
322
|
|
|
319
|
-
|
|
323
|
+
```ts
|
|
324
|
+
emit("login:start")
|
|
325
|
+
emit("login:success", user)
|
|
326
|
+
emit("login:failed", error)
|
|
327
|
+
```
|
|
328
|
+
Intents are verbs, not patches.
|
|
320
329
|
|
|
321
|
-
|
|
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
|
|
322
339
|
|
|
323
|
-
|
|
324
|
-
| --------------- | ----------------------- | ----- | -------- |
|
|
325
|
-
| No-hook UI | ✅ | ❌ | ❌ |
|
|
326
|
-
| Intent-first | ✅ | ❌ | ❌ |
|
|
327
|
-
| Async built-in | ✅ | ⚠️ | ⚠️ |
|
|
328
|
-
| Computed graph | ✅ | ❌ | ❌ |
|
|
329
|
-
| Headless test | ✅ | ⚠️ | ⚠️ |
|
|
330
|
-
| Devtools replay | ✅ | ⚠️ | ❌ |
|
|
340
|
+
✅ Correct
|
|
331
341
|
|
|
332
|
-
|
|
342
|
+
```ts
|
|
343
|
+
emit("update:user:name", "admin")
|
|
344
|
+
```
|
|
333
345
|
|
|
334
|
-
|
|
346
|
+
#### ❌ 6. Using React hooks to replace runtime behavior
|
|
347
|
+
```ts
|
|
348
|
+
// ❌ useEffect as orchestration
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (state.loggedIn) {
|
|
351
|
+
fetchProfile()
|
|
352
|
+
}
|
|
353
|
+
}, [state.loggedIn])
|
|
354
|
+
```
|
|
355
|
+
Why this is wrong
|
|
356
|
+
|
|
357
|
+
- Behavior split across layers
|
|
358
|
+
- Impossible to replay or test headlessly
|
|
335
359
|
|
|
336
|
-
|
|
360
|
+
✅ Correct
|
|
361
|
+
|
|
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
|
|
337
384
|
|
|
338
|
-
|
|
339
|
-
- ❌ Not a replacement for Redux Toolkit
|
|
340
|
-
- ❌ Not a UI framework
|
|
341
|
-
- ❌ Not tied to React (runtime is headless)
|
|
385
|
+
✅ Correct
|
|
342
386
|
|
|
343
|
-
|
|
387
|
+
```ts
|
|
388
|
+
composeLogic(
|
|
389
|
+
userLogic,
|
|
390
|
+
cartLogic,
|
|
391
|
+
productLogic
|
|
392
|
+
)
|
|
393
|
+
```
|
|
344
394
|
|
|
345
395
|
---
|
|
346
396
|
|
|
347
397
|
## 📜 License
|
|
348
398
|
|
|
349
|
-
MIT
|
|
399
|
+
MIT / Delpi
|
package/build/core/effect.d.ts
CHANGED
|
@@ -1,11 +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
3
|
export declare function takeLatest<W, R>(): Effect<W, R>;
|
|
4
4
|
export declare function debounce<W, R>(ms: number): Effect<W, R>;
|
|
5
5
|
export declare function retry<W, R>(count?: number): Effect<W, R>;
|
|
6
|
-
export
|
|
7
|
-
takeLatest(): EffectBuilder<W, R>;
|
|
8
|
-
debounce(ms: number): EffectBuilder<W, R>;
|
|
9
|
-
retry(count?: number): EffectBuilder<W, R>;
|
|
10
|
-
};
|
|
11
|
-
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,10 +1,11 @@
|
|
|
1
|
-
import type { IntentHandler, Effect } from "./types";
|
|
1
|
+
import type { IntentHandler, Effect, IntentMiddleware } from "./types";
|
|
2
2
|
export declare function createIntentBus<W extends object, R extends object = W, P = any>(scope: string): {
|
|
3
3
|
on: (intent: string, handler: IntentHandler<W, R, P>) => void;
|
|
4
4
|
effect: (intent: string, fx: Effect<W, R, P>) => void;
|
|
5
5
|
emit: (intent: string, payload: P, ctx: {
|
|
6
|
-
getState
|
|
7
|
-
setState
|
|
8
|
-
emit
|
|
6
|
+
getState(): R;
|
|
7
|
+
setState(fn: (s: W) => void): void;
|
|
8
|
+
emit(intent: string, payload?: any): Promise<void>;
|
|
9
9
|
}) => Promise<void>;
|
|
10
|
+
use: (mw: IntentMiddleware<W, R, P>) => void;
|
|
10
11
|
};
|
package/build/core/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
state: Readonly<R>;
|
|
@@ -10,12 +10,16 @@ export type EffectCtx<W = any, R = W, P = any> = {
|
|
|
10
10
|
};
|
|
11
11
|
export type IntentHandler<W = any, R = W, P = any> = (ctx: EffectCtx<W, R, P>) => void | Promise<void>;
|
|
12
12
|
export type Effect<W = any, R = W, P = any> = (next: IntentHandler<W, R, P>) => IntentHandler<W, R, P>;
|
|
13
|
+
export type IntentMiddleware<W = any, R = W, P = any> = (ctx: EffectCtx<W, R, P>, next: () => Promise<void>) => Promise<void>;
|
|
13
14
|
export type LogicRuntime<S extends object, C extends object = {}> = {
|
|
14
15
|
scope: string;
|
|
15
16
|
state(): Readonly<S & C>;
|
|
16
17
|
setState(mutator: (s: S) => void): void;
|
|
17
18
|
reset(): void;
|
|
19
|
+
batch(fn: () => void): void;
|
|
18
20
|
emit(intent: string, payload?: any): Promise<void>;
|
|
21
|
+
effect(intent: string, fx: Effect<S, S & C>): void;
|
|
22
|
+
use(mw: IntentMiddleware<S, S & C>): void;
|
|
19
23
|
subscribe(fn: Listener): () => void;
|
|
20
24
|
onIntent(intent: string, handler: IntentHandler<S, S & C>): void;
|
|
21
25
|
devtools?: {
|
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 a(){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,a=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,a),e.set(n,s),a},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 c(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)},emit:async function(s,a,c){var i,l;const u=`${t}:${s}`;null===(i=o.get(u))||void 0===i||i.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:c.getState(),payload:a,signal:f.signal,scope:t,emit:c.emit,setState:c.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)}},use:function(t){r.push(t)}}}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:{},a=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!s||t.scope===s));for(const e of a){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 l=0;const u="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 a=t[s]||n;return a?Promise.resolve(a(e,()=>o(s+1))):Promise.resolve()}(0)}},exports.createBackendRuntime=function(t){var e,n;let r=structuredClone(t);const o=a(),s=c("backend"),l=[],u=[];let f=!1,d=new Set;function p(t){const e=structuredClone(r);if(t(r),f)for(const t in r)e[t]!==r[t]&&d.add(t);else for(const t in r)e[t]!==r[t]&&o.invalidate(t)}function m(t){return{...t,set(t){p(e=>Object.assign(e,t))},emit:v.emit}}const v={state:()=>r,setState:p,batch:function(t){f=!0;try{t()}finally{f=!1,d.forEach(t=>o.invalidate(t)),d.clear()}},async emit(t,e){l.forEach(n=>n({intent:t,payload:e,state:structuredClone(r)}));try{await s.emit(t,e,{getState:()=>r,setState:p,emit:v.emit})}finally{u.forEach(n=>n({intent:t,payload:e,state:structuredClone(r)}))}},onIntent:s.on,effect:s.effect,use:s.use,registerIntents:function(t){for(const e in t){const n=t[e];s.on(e,async t=>{await n(m({state:r,signal:t.signal}))})}},__internal:{onEmitStart(t){l.push(t)},onEmitEnd(t){u.push(t)}},reset:function(){r=structuredClone(t),o.reset()}};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(v);t.wrap(v),v.devtools=t}return v},exports.createComputed=a,exports.createIntentBus=c,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}:${++l}`,d=structuredClone(t.state),p=s(structuredClone(t.state)),m=a(),v=c(f);null===(r=t.intents)||void 0===r||r.call(t,v);let b=null,g=null,h=!1,y=new Set;const w=[],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?y.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}()},use:v.use,effect:v.effect,batch:function(t){h=!0;try{t()}finally{h=!1,y.forEach(t=>m.invalidate(t)),y.clear()}},async emit(t,e){const n=S();w.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){w.push(t)},onEmitEnd(t){E.push(t)}}};let O;return null===(o=t.plugins)||void 0===o||o.forEach(t=>t.setup(j)),u&&(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,a;const c=s=>{const a=o.useRef(null);a.current||(a.current=e.create(r));const c=a.current,i=o.useSyncExternalStore(c.subscribe,c.state,c.state),l=o.useCallback((t,e)=>c.emit(t,e),[c]);return t.jsx(n,{...s,state:i,emit:l})};return c.displayName=`withLogic(${null!==(a=null!==(s=n.displayName)&&void 0!==s?s:n.name)&&void 0!==a?a:"View"})`,c};
|
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,a=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,a),e.set(n,s),a},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 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)},emit:async function(s,a,i){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:i.getState(),payload:a,signal:f.signal,scope:t,emit:i.emit,setState:i.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)}},use:function(t){r.push(t)}}}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:{},a=e.filter(t=>"emit"===t.type&&t.id>=r&&t.id<=o&&(!s||t.scope===s));for(const e of a){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=a(l);null===(s=t.intents)||void 0===s||s.call(t,v);let h=null,b=null,g=!1,y=new Set;const w=[],E=[];function S(){const e=p.getState();if(e===h&&b)return b;const n={};var r;return t.computed&&(r=t.computed,Object.keys(r)).forEach(r=>{n[r]=m.compute(r,t.computed[r],e)}),h=e,b=Object.assign({},e,n),b}function C(t){const e=p.getState();p.setState(n=>{t(n);for(const t in n)e[t]!==n[t]&&(g?y.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),h=null,b=null}()},use:v.use,effect:v.effect,batch:function(t){g=!0;try{t()}finally{g=!1,y.forEach(t=>m.invalidate(t)),y.clear()}},async emit(t,e){const n=S();w.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){w.push(t)},onEmitEnd(t){E.push(t)}}};let O;return null===(c=t.plugins)||void 0===c||c.forEach(t=>t.setup(j)),f&&(O=i(j),O.wrap(j)),{...j,devtools:O}}}}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 h(t){let e;return n=>r=>new Promise(o=>{clearTimeout(e),e=setTimeout(()=>o(n(r)),t)})}function b(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(h(t)),r},r.retry=(t=3)=>{if(n)throw new Error("Effect already built");return e.push(b(t)),r},r}function y(t){var e,n;let o=structuredClone(t);const s=r(),c=a("backend"),l=[],u=[];let f=!1,d=new Set;function p(t){const e=structuredClone(o);if(t(o),f)for(const t in o)e[t]!==o[t]&&d.add(t);else for(const t in o)e[t]!==o[t]&&s.invalidate(t)}function m(t){return{...t,set(t){p(e=>Object.assign(e,t))},emit:v.emit}}const v={state:()=>o,setState:p,batch:function(t){f=!0;try{t()}finally{f=!1,d.forEach(t=>s.invalidate(t)),d.clear()}},async emit(t,e){l.forEach(n=>n({intent:t,payload:e,state:structuredClone(o)}));try{await c.emit(t,e,{getState:()=>o,setState:p,emit:v.emit})}finally{u.forEach(n=>n({intent:t,payload:e,state:structuredClone(o)}))}},onIntent:c.on,effect:c.effect,use:c.use,registerIntents:function(t){for(const e in t){const n=t[e];c.on(e,async t=>{await n(m({state:o,signal:t.signal}))})}},__internal:{onEmitStart(t){l.push(t)},onEmitEnd(t){u.push(t)}},reset:function(){o=structuredClone(t),s.reset()}};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(v);t.wrap(v),v.devtools=t}return v}function w(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 a=t[s]||n;return a?Promise.resolve(a(e,()=>o(s+1))):Promise.resolve()}(0)}}function E(n,r,o){var s,a;const i=s=>{const a=e.useRef(null);a.current||(a.current=n.create(o));const i=a.current,c=e.useSyncExternalStore(i.subscribe,i.state,i.state),l=e.useCallback((t,e)=>i.emit(t,e),[i]);return t(r,{...s,state:c,emit:l})};return i.displayName=`withLogic(${null!==(a=null!==(s=r.displayName)&&void 0!==s?s:r.name)&&void 0!==a?a:"View"})`,i}export{i as attachDevtools,m as composeEffects,p as composeLogic,w as composeMiddleware,y as createBackendRuntime,r as createComputed,a as createIntentBus,d as createLogic,o as createSelector,s as createSignal,n as createStore,h as debounce,g as effect,b as retry,v as takeLatest,E as withLogic};
|
|
@@ -10,5 +10,5 @@ type LogicFactory<S> = {
|
|
|
10
10
|
export declare function withLogic<S, P extends object>(logic: LogicFactory<S>, View: React.ComponentType<P & {
|
|
11
11
|
state: Readonly<S>;
|
|
12
12
|
emit: (intent: string, payload?: any) => Promise<void>;
|
|
13
|
-
}
|
|
13
|
+
}>, scope?: string): React.FC<Omit<P, "state" | "emit">>;
|
|
14
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 = {
|
|
@@ -14,7 +14,7 @@ type ComposedLogic = {
|
|
|
14
14
|
export declare function composeLogic(...entries: ComposedLogic[]): {
|
|
15
15
|
create(scope?: string): {
|
|
16
16
|
readonly state: any;
|
|
17
|
-
emit(intent: string, payload?: any): Promise<
|
|
17
|
+
emit(intent: string, payload?: any): Promise<void>;
|
|
18
18
|
subscribe(fn: () => void): () => void;
|
|
19
19
|
};
|
|
20
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
|
@@ -5,8 +5,8 @@ export type Devtools = {
|
|
|
5
5
|
};
|
|
6
6
|
type RuntimeLike = {
|
|
7
7
|
scope: string;
|
|
8
|
-
emit(intent: string, payload?: any): Promise<void>;
|
|
9
8
|
state(): any;
|
|
9
|
+
emit(intent: string, payload?: any): Promise<void>;
|
|
10
10
|
};
|
|
11
11
|
export declare function attachDevtools(target: RuntimeLike): Devtools;
|
|
12
12
|
export {};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|