@castlemilk/omega 0.6.32 → 0.6.33
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.
|
@@ -13,6 +13,7 @@ When a task asks you to add atomic/signal selectors to Kea, treat it as a framew
|
|
|
13
13
|
- Do NOT create or modify test files. The verifier supplies its own tests.
|
|
14
14
|
- Do NOT leave the engine in a standalone file without wiring it into `selectors.ts`, `build.ts`, and `kea.ts`.
|
|
15
15
|
- Do NOT modify `rollup.config.js`, `tsconfig.json`, or build tooling unless explicitly required.
|
|
16
|
+
- Do NOT break React/Redux referential equality. The tracking proxy must record reads but return the original underlying values to consumers.
|
|
16
17
|
|
|
17
18
|
## Step-by-step wiring
|
|
18
19
|
|
|
@@ -24,11 +25,10 @@ Edit `src/kea/context.ts`. Add `atomicSelectors: false` to the default options m
|
|
|
24
25
|
|
|
25
26
|
### Step 2 — Atomic engine helper
|
|
26
27
|
|
|
27
|
-
Create `src/kea/atomic.ts` with the starter below.
|
|
28
|
+
Create `src/kea/atomic.ts` with the starter below.
|
|
28
29
|
|
|
29
30
|
```ts
|
|
30
31
|
import { getContext } from './context'
|
|
31
|
-
import { Selector } from '../types'
|
|
32
32
|
|
|
33
33
|
export interface AtomicSelectorHealth {
|
|
34
34
|
dependencies: string[]
|
|
@@ -53,6 +53,7 @@ interface SelectorMeta {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const engines = new WeakMap<any, Map<string, SelectorMeta>>()
|
|
56
|
+
const evaluationStack = new Set<string>()
|
|
56
57
|
|
|
57
58
|
export function getAtomicEngine(logic: any): Map<string, SelectorMeta> {
|
|
58
59
|
if (!engines.has(logic)) {
|
|
@@ -80,15 +81,20 @@ export function registerAtomicSelector(logic: any, name: string): void {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export function recordAtomicDependency(logic: any, selectorName: string,
|
|
84
|
+
export function recordAtomicDependency(logic: any, selectorName: string, path: string): void {
|
|
84
85
|
const engine = getAtomicEngine(logic)
|
|
85
86
|
const meta = engine.get(selectorName)
|
|
86
87
|
if (!meta) return
|
|
87
|
-
meta.dependencies.add(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
meta.dependencies.add(path)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function recordAtomicSelectorDependency(logic: any, selectorName: string, inputSelectorName: string): void {
|
|
92
|
+
const engine = getAtomicEngine(logic)
|
|
93
|
+
const meta = engine.get(selectorName)
|
|
94
|
+
const inputMeta = engine.get(inputSelectorName)
|
|
95
|
+
if (meta && inputMeta) {
|
|
96
|
+
meta.dependencies.add(inputSelectorName)
|
|
97
|
+
inputMeta.dependents.add(selectorName)
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -99,6 +105,11 @@ export function startAtomicEvaluation(logic: any, selectorName: string): void {
|
|
|
99
105
|
meta.dirty = false
|
|
100
106
|
meta.dirtyCause = null
|
|
101
107
|
}
|
|
108
|
+
const stackKey = `${logic.pathString || 'unknown'}::${selectorName}`
|
|
109
|
+
if (evaluationStack.has(stackKey)) {
|
|
110
|
+
throw new Error('[KEA] Circular dependency detected')
|
|
111
|
+
}
|
|
112
|
+
evaluationStack.add(stackKey)
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
export function endAtomicEvaluation(logic: any, selectorName: string, result: any): void {
|
|
@@ -108,6 +119,8 @@ export function endAtomicEvaluation(logic: any, selectorName: string, result: an
|
|
|
108
119
|
meta.lastResult = result
|
|
109
120
|
meta.evaluations += 1
|
|
110
121
|
}
|
|
122
|
+
const stackKey = `${logic.pathString || 'unknown'}::${selectorName}`
|
|
123
|
+
evaluationStack.delete(stackKey)
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
export function markAtomicDirty(logic: any, leafPath: string): void {
|
|
@@ -136,17 +149,101 @@ export function buildAtomicHealth(logic: any): AtomicLogicHealth {
|
|
|
136
149
|
return { selectors, topologicalOrder: order }
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
function buildLeafPath(reducerNames: string[], path: string[]): string {
|
|
153
|
+
// The first segment that is a reducer name starts the path.
|
|
154
|
+
let start = 0
|
|
155
|
+
for (let i = 0; i < path.length; i++) {
|
|
156
|
+
if (reducerNames.includes(path[i])) {
|
|
157
|
+
start = i
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return path.slice(start).join('.')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createStateProxy(
|
|
165
|
+
state: any,
|
|
166
|
+
logic: any,
|
|
167
|
+
selectorName: string,
|
|
168
|
+
reducerNames: string[],
|
|
169
|
+
path: string[] = [],
|
|
170
|
+
): any {
|
|
171
|
+
if (state === null || typeof state !== 'object') {
|
|
172
|
+
return state
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const record = (key: string, value: any) => {
|
|
176
|
+
const leafPath = buildLeafPath(reducerNames, path.concat(key))
|
|
177
|
+
recordAtomicDependency(logic, selectorName, leafPath)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (state instanceof Map) {
|
|
181
|
+
return new Proxy(state, {
|
|
182
|
+
get(target, prop) {
|
|
183
|
+
const key = String(prop)
|
|
184
|
+
if (key === 'get') {
|
|
185
|
+
return function (mapKey: any) {
|
|
186
|
+
record(`map:${mapKey}`, target.get(mapKey))
|
|
187
|
+
return target.get(mapKey)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (key === 'has') {
|
|
191
|
+
return function (mapKey: any) {
|
|
192
|
+
record(`map:${mapKey}`, target.has(mapKey))
|
|
193
|
+
return target.has(mapKey)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const value = (target as any)[prop]
|
|
197
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (state instanceof Set) {
|
|
203
|
+
return new Proxy(state, {
|
|
204
|
+
get(target, prop) {
|
|
205
|
+
const key = String(prop)
|
|
206
|
+
if (key === 'has' || key === 'includes') {
|
|
207
|
+
return function (setValue: any) {
|
|
208
|
+
record(`set:${setValue}`, target.has(setValue))
|
|
209
|
+
return target.has(setValue)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const value = (target as any)[prop]
|
|
213
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (Array.isArray(state)) {
|
|
219
|
+
return new Proxy(state, {
|
|
220
|
+
get(target, prop) {
|
|
221
|
+
const key = String(prop)
|
|
222
|
+
if (/^\d+$/.test(key) || key === 'length') {
|
|
223
|
+
record(key, (target as any)[key])
|
|
224
|
+
}
|
|
225
|
+
if (key === 'includes' || key === 'indexOf' || key === 'find' || key === 'some') {
|
|
226
|
+
const fn = (target as any)[key]
|
|
227
|
+
return function (...fnArgs: any[]) {
|
|
228
|
+
// For includes/indexOf the first arg is the searched value.
|
|
229
|
+
if ((key === 'includes' || key === 'indexOf') && fnArgs.length > 0) {
|
|
230
|
+
record(fnArgs[0], true)
|
|
231
|
+
}
|
|
232
|
+
return fn.apply(target, fnArgs)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const value = (target as any)[prop]
|
|
236
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
141
241
|
return new Proxy(state, {
|
|
142
242
|
get(target, prop) {
|
|
143
243
|
const key = String(prop)
|
|
144
244
|
const value = (target as any)[key]
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
// Recursively wrap objects so nested reads can be tracked if the selector reads them directly.
|
|
149
|
-
return createStateProxy(value, logic, selectorName, reducerNames)
|
|
245
|
+
record(key, value)
|
|
246
|
+
return value
|
|
150
247
|
},
|
|
151
248
|
})
|
|
152
249
|
}
|
|
@@ -156,43 +253,38 @@ export function createStateProxy(state: any, logic: any, selectorName: string, r
|
|
|
156
253
|
|
|
157
254
|
Edit `src/core/selectors.ts`:
|
|
158
255
|
|
|
159
|
-
1. Import the atomic helpers
|
|
160
|
-
|
|
161
|
-
import { getContext } from '../kea/context'
|
|
162
|
-
import {
|
|
163
|
-
getAtomicEngine,
|
|
164
|
-
registerAtomicSelector,
|
|
165
|
-
startAtomicEvaluation,
|
|
166
|
-
endAtomicEvaluation,
|
|
167
|
-
buildAtomicHealth,
|
|
168
|
-
} from '../kea/atomic'
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
2. Inside the `selectors` builder, after resolving `selectorInputs`, check atomic mode:
|
|
172
|
-
```ts
|
|
173
|
-
const atomicEnabled = getContext().options.atomicSelectors
|
|
174
|
-
```
|
|
175
|
-
|
|
256
|
+
1. Import the atomic helpers.
|
|
257
|
+
2. Inside the `selectors` builder, after resolving `selectorInputs`, check `const atomicEnabled = getContext().options.atomicSelectors`.
|
|
176
258
|
3. When building each selector, if `atomicEnabled`, wrap it:
|
|
177
259
|
```ts
|
|
178
260
|
if (atomicEnabled) {
|
|
179
261
|
registerAtomicSelector(logic, key)
|
|
180
262
|
const originalCompute = func
|
|
181
|
-
const
|
|
263
|
+
const builtSelector = (state: any, props: any) => {
|
|
182
264
|
startAtomicEvaluation(logic, key)
|
|
183
265
|
const reducerNames = Object.keys(logic.reducers || {})
|
|
184
266
|
const proxyState = createStateProxy(state, logic, key, reducerNames)
|
|
185
|
-
const
|
|
267
|
+
const inputResults = args.map((a) => {
|
|
268
|
+
const inputName = (a as any).__keaSelectorName || (a as any).selectorName
|
|
269
|
+
if (inputName) {
|
|
270
|
+
recordAtomicSelectorDependency(logic, key, inputName)
|
|
271
|
+
}
|
|
272
|
+
return a(proxyState, props)
|
|
273
|
+
})
|
|
274
|
+
const result = originalCompute(...inputResults)
|
|
186
275
|
endAtomicEvaluation(logic, key, result)
|
|
187
276
|
return result
|
|
188
277
|
}
|
|
189
|
-
builtSelectors[key] =
|
|
278
|
+
builtSelectors[key] = builtSelector as Selector
|
|
190
279
|
} else {
|
|
191
280
|
builtSelectors[key] = createSelector(args, func, { memoizeOptions })
|
|
192
281
|
}
|
|
193
282
|
```
|
|
194
|
-
|
|
195
|
-
|
|
283
|
+
4. Tag each built selector with its name so chained selectors can detect dependencies:
|
|
284
|
+
```ts
|
|
285
|
+
;(builtSelectors[key] as any).__keaSelectorName = key
|
|
286
|
+
```
|
|
287
|
+
5. At the end of the builder, attach `selectorHealth` to `logic`:
|
|
196
288
|
```ts
|
|
197
289
|
if (atomicEnabled) {
|
|
198
290
|
logic.selectorHealth = () => buildAtomicHealth(logic)
|
|
@@ -209,11 +301,9 @@ if (getContext().options.atomicSelectors) {
|
|
|
209
301
|
}
|
|
210
302
|
```
|
|
211
303
|
|
|
212
|
-
This is required because verifier tests call `logic.mount(); logic.selectorHealth()` directly on the object returned by `kea({...})`.
|
|
213
|
-
|
|
214
304
|
### Step 5 — Wire reducer updates
|
|
215
305
|
|
|
216
|
-
Edit `src/core/reducers.ts
|
|
306
|
+
Edit `src/core/reducers.ts`:
|
|
217
307
|
- After a reducer returns a new state, compare old and new state at the leaf level.
|
|
218
308
|
- For each changed leaf path, call `markAtomicDirty(logic, leafPath)`.
|
|
219
309
|
- Multiple leaf changes in one action must coalesce into one selector re-evaluation.
|
|
@@ -223,10 +313,11 @@ Edit `src/core/reducers.ts` (or the file that builds Kea reducers):
|
|
|
223
313
|
Edit `src/react/hooks.ts`:
|
|
224
314
|
- Ensure React components re-render only when a leaf path they subscribe to changes.
|
|
225
315
|
- Keep existing behavior when `atomicSelectors` is false.
|
|
316
|
+
- Do not introduce infinite loops; selectors must remain referentially stable.
|
|
226
317
|
|
|
227
318
|
### Step 7 — Exports
|
|
228
319
|
|
|
229
|
-
Edit `src/index.ts` and export public helpers/types
|
|
320
|
+
Edit `src/index.ts` and export public helpers/types.
|
|
230
321
|
|
|
231
322
|
## Verification
|
|
232
323
|
|