logic-runtime-react-z 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +447 -186
- package/build/core/backend-runtime.d.ts +9 -0
- package/build/core/compose.d.ts +2 -11
- package/build/core/devtools.d.ts +1 -7
- package/build/core/effect.d.ts +0 -9
- package/build/core/intent.d.ts +4 -4
- package/build/core/middleware.d.ts +2 -0
- package/build/core/timeline.d.ts +0 -15
- package/build/core/types.d.ts +5 -6
- package/build/index.cjs.js +1 -1
- package/build/index.d.ts +6 -4
- package/build/index.esm.js +1 -1
- package/build/react/withLogic.d.ts +1 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,215 +1,323 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
# ⚙️ logic-runtime-react-z
|
|
2
3
|
|
|
3
4
|
[](https://www.npmjs.com/package/logic-runtime-react-z)
|
|
4
5
|

|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
**logic-runtime-react-z** is an intent-first external runtime,
|
|
11
|
-
designed to run business logic **outside React**.
|
|
7
|
+
**logic-runtime-react-z** is an **intent-first business logic runtime**
|
|
8
|
+
designed to run **outside React**, with React acting purely as a view layer.
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
> Think of it as:
|
|
17
|
-
> **“Business logic runtime outside React — React is just the view.”**
|
|
10
|
+
> **Business logic lives outside React.
|
|
11
|
+
> React only renders state and emits intent.**
|
|
18
12
|
|
|
19
13
|
---
|
|
20
14
|
|
|
21
15
|
## ✨ Why / When to Use
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
|
|
17
|
+
Use this library when:
|
|
18
|
+
|
|
19
|
+
- You want **zero hooks inside UI components**
|
|
20
|
+
- Business logic must not depend on React lifecycle
|
|
21
|
+
- UI should **emit intent**, not orchestrate behavior
|
|
25
22
|
- Async flows are complex (login → fetch → redirect)
|
|
26
23
|
- Side effects must be predictable & testable
|
|
27
|
-
- You want headless
|
|
28
|
-
- You prefer architecture-driven design
|
|
24
|
+
- You want **headless testing** without rendering
|
|
25
|
+
- You prefer **architecture-driven** design
|
|
29
26
|
|
|
30
27
|
---
|
|
31
28
|
|
|
32
29
|
## 🧠 Mental Model
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
UI (Pure View)
|
|
36
|
-
└─ emits intent (no logic)
|
|
37
|
-
↓
|
|
38
|
-
External Runtime (logic-runtime-react-z)
|
|
39
|
-
├─ state store
|
|
40
|
-
├─ intent handlers
|
|
41
|
-
├─ effect pipeline
|
|
42
|
-
├─ computed graph
|
|
43
|
-
├─ selectors
|
|
44
|
-
└─ devtools timeline
|
|
45
|
-
```
|
|
31
|
+

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