logic-runtime-react-z 1.0.1 → 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 +448 -188
- package/build/core/backend-runtime.d.ts +9 -0
- package/build/core/compose.d.ts +2 -2
- package/build/core/devtools.d.ts +1 -1
- package/build/core/intent.d.ts +2 -1
- package/build/core/middleware.d.ts +2 -0
- package/build/core/types.d.ts +4 -1
- 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 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,218 +1,324 @@
|
|
|
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++
|
|
162
|
-
setState(s => {
|
|
163
|
-
s.count += 1
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// emit("inc") ❌ no nested
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
// Intent handlers can be sync or async
|
|
170
|
-
bus.on("add", ({ payload, setState }) => {
|
|
171
|
-
setState(s => {
|
|
172
|
-
s.count += payload
|
|
173
|
-
})
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
bus.on("reset", ({ setState }) => {
|
|
159
|
+
bus.on("set", ({ payload, setState }) => {
|
|
177
160
|
setState(s => {
|
|
178
|
-
s.
|
|
161
|
+
s.value = payload
|
|
179
162
|
})
|
|
180
163
|
})
|
|
181
164
|
},
|
|
182
165
|
})
|
|
183
166
|
|
|
184
|
-
|
|
185
|
-
// const runtime = counterLogic.create()
|
|
186
|
-
const runtime = counterLogic.create("counter:main")
|
|
167
|
+
const runtime = logic.create()
|
|
187
168
|
|
|
188
|
-
runtime.emit("
|
|
189
|
-
|
|
190
|
-
runtime.
|
|
169
|
+
await runtime.emit("set", 4)
|
|
170
|
+
|
|
171
|
+
expect(runtime.state.squared).toBe(16)
|
|
191
172
|
|
|
192
|
-
// console.log
|
|
193
|
-
console.log(runtime.state.count) // 7
|
|
194
|
-
console.log(runtime.state.double) // 14
|
|
195
173
|
```
|
|
196
174
|
|
|
197
175
|
---
|
|
198
176
|
|
|
199
|
-
##
|
|
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
|
+
```
|
|
200
192
|
|
|
201
|
-
#### Plugins extend the runtime behavior **without touching business logic**.
|
|
202
193
|
|
|
194
|
+
#### 🚀 1. Backend Runtime
|
|
203
195
|
```ts
|
|
204
|
-
|
|
205
|
-
|
|
196
|
+
1️⃣ // create backend
|
|
197
|
+
import { createBackendRuntime } from "logic-runtime-react-z"
|
|
206
198
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
})
|
|
199
|
+
type BackendState = {
|
|
200
|
+
user: null | {
|
|
201
|
+
id: string
|
|
202
|
+
name: string
|
|
213
203
|
}
|
|
204
|
+
token: string | null
|
|
205
|
+
loading: boolean
|
|
214
206
|
}
|
|
215
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",
|
|
293
|
+
}
|
|
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"
|
|
321
|
+
|
|
216
322
|
export const persistPlugin: LogicPlugin = {
|
|
217
323
|
name: "persist",
|
|
218
324
|
|
|
@@ -223,127 +329,281 @@ export const persistPlugin: LogicPlugin = {
|
|
|
223
329
|
JSON.stringify(runtime.state)
|
|
224
330
|
)
|
|
225
331
|
})
|
|
226
|
-
}
|
|
332
|
+
},
|
|
227
333
|
}
|
|
334
|
+
|
|
228
335
|
```
|
|
229
336
|
|
|
230
337
|
---
|
|
231
338
|
|
|
232
|
-
|
|
339
|
+
#### ⚡ Effects (Async / Side-effects)
|
|
233
340
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
})
|
|
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
|
+
)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
#### 🧮 Selectors
|
|
381
|
+
|
|
382
|
+
Selectors are pure, memoized, reusable functions.
|
|
237
383
|
|
|
238
384
|
```ts
|
|
239
385
|
import { createSelector } from "logic-runtime-react-z"
|
|
240
386
|
|
|
241
387
|
const selectIsAdult = createSelector(
|
|
242
|
-
(state: { age: number }) => state.age
|
|
243
|
-
age => age >= 18
|
|
388
|
+
(state: { age: number }) => state.age >= 18
|
|
244
389
|
)
|
|
390
|
+
|
|
245
391
|
```
|
|
246
392
|
|
|
247
|
-
####
|
|
248
|
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
- cancellation
|
|
253
|
-
- async orchestration
|
|
393
|
+
#### 🧭 Devtools & Timeline
|
|
394
|
+
|
|
395
|
+
- Records every intent
|
|
396
|
+
- Replayable
|
|
397
|
+
- Deterministic async flow
|
|
254
398
|
|
|
255
399
|
```ts
|
|
256
|
-
|
|
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
|
+
---
|
|
410
|
+
|
|
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
|
+
---
|
|
257
423
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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")
|
|
262
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)
|
|
263
463
|
})
|
|
264
464
|
```
|
|
265
465
|
|
|
266
|
-
####
|
|
466
|
+
#### ❌ 2. Calling handlers directly instead of emitting intent
|
|
267
467
|
```ts
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
468
|
+
// ❌ Don't call handlers manually
|
|
469
|
+
loginHandler(payload)
|
|
470
|
+
```
|
|
471
|
+
Why this is wrong
|
|
271
472
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
throw new Error("Not allowed")
|
|
276
|
-
}
|
|
473
|
+
- Skips middleware & effects
|
|
474
|
+
- Breaks devtools timeline
|
|
475
|
+
- Makes behavior non-deterministic
|
|
277
476
|
|
|
278
|
-
|
|
279
|
-
})
|
|
477
|
+
✅ Correct
|
|
280
478
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
})
|
|
286
|
-
})
|
|
479
|
+
```ts
|
|
480
|
+
runtime.emit("login", payload)
|
|
481
|
+
```
|
|
482
|
+
Intent is the only entry point. Always.
|
|
287
483
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
```
|
|
291
492
|
|
|
292
|
-
|
|
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 })
|
|
293
517
|
```
|
|
294
518
|
|
|
295
|
-
|
|
519
|
+
Why this is wrong
|
|
296
520
|
|
|
297
|
-
|
|
298
|
-
-
|
|
299
|
-
|
|
300
|
-
|
|
521
|
+
- Intent should describe user or system intention
|
|
522
|
+
- Not raw state mutation
|
|
523
|
+
|
|
524
|
+
✅ Correct
|
|
301
525
|
|
|
302
526
|
```ts
|
|
303
|
-
|
|
304
|
-
|
|
527
|
+
emit("login:start")
|
|
528
|
+
emit("login:success", user)
|
|
529
|
+
emit("login:failed", error)
|
|
305
530
|
```
|
|
531
|
+
Intents are verbs, not patches.
|
|
306
532
|
|
|
307
|
-
|
|
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
|
|
308
542
|
|
|
309
|
-
|
|
543
|
+
✅ Correct
|
|
310
544
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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.). |
|
|
545
|
+
```ts
|
|
546
|
+
emit("update:user:name", "admin")
|
|
547
|
+
```
|
|
318
548
|
|
|
319
|
-
|
|
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
|
|
320
559
|
|
|
321
|
-
|
|
560
|
+
- Behavior split across layers
|
|
561
|
+
- Impossible to replay or test headlessly
|
|
562
|
+
|
|
563
|
+
✅ Correct
|
|
322
564
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
331
587
|
|
|
332
|
-
|
|
588
|
+
✅ Correct
|
|
333
589
|
|
|
334
|
-
|
|
590
|
+
```ts
|
|
591
|
+
composeLogic(
|
|
592
|
+
userLogic,
|
|
593
|
+
cartLogic,
|
|
594
|
+
productLogic
|
|
595
|
+
)
|
|
596
|
+
```
|
|
335
597
|
|
|
336
|
-
|
|
598
|
+
---
|
|
337
599
|
|
|
338
|
-
|
|
339
|
-
- ❌ Not a replacement for Redux Toolkit
|
|
340
|
-
- ❌ Not a UI framework
|
|
341
|
-
- ❌ Not tied to React (runtime is headless)
|
|
600
|
+
## 🧠 Philosophy
|
|
342
601
|
|
|
343
|
-
|
|
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.
|
|
344
604
|
|
|
345
605
|
---
|
|
346
606
|
|
|
347
607
|
## 📜 License
|
|
348
608
|
|
|
349
|
-
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
package/build/core/devtools.d.ts
CHANGED
package/build/core/intent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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;
|
|
@@ -7,4 +7,5 @@ export declare function createIntentBus<W extends object, R extends object = W,
|
|
|
7
7
|
setState: (fn: (s: W) => void) => void;
|
|
8
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
|
@@ -10,12 +10,15 @@ 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
|
-
state(): Readonly<S & C>;
|
|
16
|
+
get 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
|
+
use(mw: IntentMiddleware<S, S & C>): void;
|
|
19
22
|
subscribe(fn: Listener): () => void;
|
|
20
23
|
onIntent(intent: string, handler: IntentHandler<S, S & C>): void;
|
|
21
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};
|