@kudzujs/core 0.6.4 → 0.6.5
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/GOAL_A.md +5 -1
- package/README.md +18 -4
- package/framework/README.md +7 -1
- package/framework/build.mjs +90 -27
- package/framework/core.d.ts +1 -1
- package/framework/core.mjs +9 -4
- package/framework/list-runtime.js +2 -1
- package/framework/shared-runtime.js +18 -0
- package/package.json +1 -1
package/GOAL_A.md
CHANGED
|
@@ -13,7 +13,7 @@ The benchmark runner, framework fixtures, generated artifacts, and raw arrays ar
|
|
|
13
13
|
- **Phase 1 complete**: a local six-route commerce fixture, locked React/Next/Nuxt/SvelteKit comparisons, and a reproducible artifact/build/Chrome runner validate the implementation.
|
|
14
14
|
- **Phase 2 complete**: effects inside conditional ranges and supported keyed row components mount with their DOM owner, unsubscribe and clean up on removal, and remount without affecting effect-free output.
|
|
15
15
|
- **Phase 3 complete**: page-exported layouts render complete documents with compiler-owned route boundaries, collision-free layout/route IDs, and state/effect ownership metadata. Effects remain document effects and client navigation is unchanged.
|
|
16
|
-
- **Phase 4 route/layout effects implemented**: layout effects mount once per document session, route effects remount after awaited route cleanup, and primitive dependency subscriptions exist only while their lifetime is mounted. Conditional/keyed effects use per-lifetime owner registries; route registries are fresh on cached revisits. Disposed effect setters and queued commits are inactive.
|
|
16
|
+
- **Phase 4 route/layout effects implemented**: layout effects mount once per document session, route effects remount after awaited route cleanup, and primitive dependency subscriptions exist only while their lifetime is mounted. Conditional/keyed effects use per-lifetime owner registries; route registries are fresh on cached revisits. Direct primitive keyed-item properties rerun only changed rows; reorder does not rerun and key changes remount. Disposed effect setters and queued commits are inactive.
|
|
17
17
|
- **Phase 4 document prefetch implemented**: visible, near-visible, hovered, or focused eligible group anchors prefetch and validate complete documents without importing target capabilities. The finite in-memory full-URL cache removes the measured product-cart HTML RTT while preserving retry and native fallback.
|
|
18
18
|
- **Phase 5 complete**: matched async cart success/rejection flows prove immediate optimistic updates, duplicate prevention, accessible errors, rollback, route-local reset, and stale-write suppression without new framework APIs.
|
|
19
19
|
- **Phase 6 expansion probe complete**: one layout-owned mock `EventTarget` stream and one route-owned imperative chart stub use existing effects and a relative TypeScript helper across repeated navigation, with exact listener and disposal assertions. This proves only the compatibility seam; Kudzu does not provide telemetry or chart support.
|
|
@@ -29,6 +29,10 @@ The post-Goal-A multiple-group fixture emits a 7,448 B raw / 3,099 B gzip (`gzip
|
|
|
29
29
|
|
|
30
30
|
In a matched one-effect navigation build with the same conditional capability, moving the effect from the route body into the conditional owner changes the route effect entry from 2,181 B raw / 1,036 B gzip to 4,135 B raw / 1,807 B gzip (`+1,954 B` raw / `+771 B` gzip). Owner-hook unsubscription changes the shared runtime from 1,347 B raw / 718 B gzip to 1,459 B raw / 732 B gzip (`+112 B` raw / `+14 B` gzip). Top-level-only and effect-free navigation builds retain their smaller generators.
|
|
31
31
|
|
|
32
|
+
In matched state-only and item-property keyed-row builds, targeted notification adds 821 B raw / 255 B gzip across the route effect entry, shared runtime, and list runtime. Builds without item-property dependencies retain their previous generated path.
|
|
33
|
+
|
|
34
|
+
In the matched 1,000-row keyed-effect runtime microbenchmark, Kudzu measured 3.4 ms selected-row cleanup/update/setup, 2.9 ms unrelated-field update, and 7.8 ms reorder after all rows and effects were ready. React CSR measured 12.3, 6.8, and 19.0 ms; Vue measured 4.7, 2.3, and 10.1 ms; and Svelte measured 5.2, 3.0, and 48.1 ms. Targeted changed-root notification reduced Kudzu's selected update from 6.2 to 3.4 ms; list reconciliation remains O(n). Kudzu emits initial rows while these framework fixtures are CSR, so their JavaScript, output, and build observations are not architecture-equivalent claims.
|
|
35
|
+
|
|
32
36
|
A 0.6.4 release-tree desktop rerun of the matched six-route commerce fixture measured Kudzu at 486.8 ms build, 35,355 deploy bytes, 7,334 B gzip product JavaScript, 332/156 ms cold/warm LCP, 122.6 ms startup task, 4.8 ms interaction, and 5.6 ms product-cart navigation. React measured 545.4 ms build, 61,464 B gzip product JavaScript, 332/264 ms LCP, 179.9 ms startup task, 10.2 ms interaction, and 9.9 ms navigation. Kudzu built 10.7% faster and its top-level-only fixture retained byte-identical deploy, product-graph, and navigation-asset sizes after navigation-owned effects were added. Raw arrays and the consolidated report are retained under the local demo benchmark workspace.
|
|
33
37
|
|
|
34
38
|
The Phase 6 chart probe's complete initial module graph is 11,902 B raw / 5,331 B gzip. It adds no framework API or package and does not change the commerce benchmark fixture.
|
package/README.md
CHANGED
|
@@ -330,9 +330,9 @@ const rows = items.map(item => <ItemRow
|
|
|
330
330
|
/>)
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
-
The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected
|
|
333
|
+
The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected and clean up when it is removed. A direct primitive item dependency such as `[item.name]`, optionally mixed with state as `[version, item.name]`, reruns only rows whose selected value changed; the replacement setup receives the complete latest item. Unrelated fields and reorder do not rerun it, while a key change removes and mounts the row. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
334
334
|
|
|
335
|
-
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. Row components accept destructured projected props, top-level single-`const` calculations and inline effects before one intrinsic return. Effect dependencies inside a row may be empty
|
|
335
|
+
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. Row components accept destructured projected props, top-level single-`const` calculations and inline effects before one intrinsic return. Effect dependencies inside a row may be empty, direct primitive Kudzu state identifiers, or direct `item.<field>` properties whose selected values remain JSON-safe primitives. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` dependencies are rejected. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers used inside calculations, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Package or namespace row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, nested item conditions, lists, or component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
336
336
|
|
|
337
337
|
## Effects
|
|
338
338
|
|
|
@@ -470,7 +470,7 @@ export default {
|
|
|
470
470
|
|
|
471
471
|
Every configured identity must be a unique emitted exact route or `runtimeParams` bracket pattern. Routes within each group must export the same layout function identity; different groups may export different layouts. Kudzu emits one deterministic, route-set-hashed navigation asset per group containing only that group's records and capabilities. Path domains may overlap within a group, where exact and more-specific matching wins, but overlapping exact/runtime or runtime/runtime domains across groups fail the build.
|
|
472
472
|
|
|
473
|
-
The layout DOM, state, and effects persist within its group; route state, parameters, and effects reset after cleanup on each transition. Conditional effects mount only while their DOM is connected. Keyed row effects mount per connected row, survive reorder,
|
|
473
|
+
The layout DOM, state, and effects persist within its group; route state, parameters, and effects reset after cleanup on each transition. Conditional effects mount only while their DOM is connected. Keyed row effects mount per connected row, survive reorder, rerun only rows whose selected direct primitive item dependency changed, receive the latest complete item, and clean up on removal. Cached route modules create fresh route owner records and subscriptions on every revisit. Eligible same-group anchors prefetch validated complete documents into a finite memory cache. Cross-group links, ungrouped routes, direct requests, reloads, malformed runtime paths, JavaScript failures, and unsupported links retain native document navigation.
|
|
474
474
|
|
|
475
475
|
This produces fast same-document route changes, but it does not add a coordinated transition animation. CSS entry animations can style newly inserted route content; exit and shared-element View Transitions are not integrated yet.
|
|
476
476
|
|
|
@@ -645,7 +645,21 @@ An intrinsic-root versus projected-prop row-component A/B build produced byte-fo
|
|
|
645
645
|
|
|
646
646
|
Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 23.2 ms, 10.7 ms behind the hand-authored Astro baseline and 7.1 ms ahead of React across all four operations.
|
|
647
647
|
|
|
648
|
-
|
|
648
|
+
### 1,000-item Keyed Effect
|
|
649
|
+
|
|
650
|
+
Each keyed row owns one effect depending on `item.name`. The measured actions rename only row 500 and wait for exactly one cleanup/setup, change an unrelated detail and require no lifecycle work, then reverse all rows and again require no lifecycle work. Medians use seven fresh Chrome profiles; builds use one warm-up and seven rotating clean runs.
|
|
651
|
+
|
|
652
|
+
| Framework | Initial rows | Initial JS gzip | Total output | Build | Selected update | Unrelated update | Reverse |
|
|
653
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
654
|
+
| Astro native | Yes | **381 B** | **90,734 B** | 1,022 ms | **0.4 ms** | **0.2 ms** | **5.5 ms** |
|
|
655
|
+
| Kudzu | Yes | 7,070 B | 221,056 B | **437 ms** | 3.4 ms | 2.9 ms | 7.8 ms |
|
|
656
|
+
| Vue CSR | No | 25,091 B | 63,368 B | 893 ms | 4.7 ms | **2.3 ms** | 10.1 ms |
|
|
657
|
+
| Svelte CSR | No | 12,848 B | 33,222 B | 1,012 ms | 5.2 ms | 3.0 ms | 48.1 ms |
|
|
658
|
+
| React CSR | No | 60,921 B | 194,301 B | 1,132 ms | 12.3 ms | 6.8 ms | 19.0 ms |
|
|
659
|
+
|
|
660
|
+
This is a post-initialization runtime microbenchmark, not an architecture-equivalent loading comparison. Kudzu and Astro emit all 1,000 rows in HTML while React, Vue, and Svelte use empty CSR shells, so their JavaScript, output, and build columns are observations rather than framework-size or startup claims. Once every target has 1,000 rows and effects ready, targeted changed-root notification reduces Kudzu's selected update from 6.2 to 3.4 ms, versus Vue at 4.7 ms, Svelte at 5.2 ms, and React at 12.3 ms. It adds 126 B gzip to Kudzu's initial graph. List reconciliation remains O(n), which dominates unrelated-field updates; Vue measures 2.3 ms there versus Kudzu's 2.9 ms. Astro is the hand-written direct-DOM lower bound.
|
|
661
|
+
|
|
662
|
+
The general benchmark snapshot was collected on July 22, 2026 and the keyed-effect comparison on July 27, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
|
|
649
663
|
|
|
650
664
|
## Development
|
|
651
665
|
|
package/framework/README.md
CHANGED
|
@@ -19,6 +19,12 @@ Static routes receive no browser runtime. Command routes receive `runtime.js`; d
|
|
|
19
19
|
|
|
20
20
|
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Source CSS and global `kudzu.config` styles are emitted in document heads before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal, unrelated fields do nothing, and key changes remain remove plus mount. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
23
|
+
|
|
24
|
+
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
23
25
|
|
|
24
26
|
The current matched commerce profile emits 35,355 deploy bytes and loads 7,334 B gzip of product-route JavaScript, including 2,425 B for navigation. These sizes are unchanged because its top-level-only navigation effects retain the smaller specialized path. Validated prefetch reduced the original 128.7 ms product-to-cart navigation to 5.6 ms in the current run. Seven interleaved artifact-clean builds after warm-up measured Kudzu at 486.8 ms and React at 545.4 ms, making Kudzu 10.7% faster. Cache-disabled output is byte-for-byte identical.
|
|
27
|
+
|
|
28
|
+
In matched state-only and item-property keyed-row builds, the minified route effect entry changes from 3,829 B raw/1,667 B gzip to 4,392 B raw/1,823 B gzip, the shared runtime from 1,291 B raw/671 B gzip to 1,503 B raw/751 B gzip, and the list runtime from 6,606 B raw/2,474 B gzip to 6,652 B raw/2,493 B gzip. The complete targeted-notification capability costs +821 B raw/+255 B gzip and remains absent from builds without item dependencies. Seven clean builds of the expanded three-route fixture measured 420-440 ms with a 430 ms median; this records current build cost rather than claiming a cross-version speed change.
|
|
29
|
+
|
|
30
|
+
The matched 1,000-row cross-framework effect fixture measured Kudzu at 7,070 B initial JavaScript gzip, 437 ms build, 3.4 ms selected-row cleanup/update/setup, 2.9 ms unrelated-field update, and 7.8 ms reorder. React CSR measured 60,921 B, 1,132 ms, 12.3 ms, 6.8 ms, and 19.0 ms respectively; Vue measured 4.7, 2.3, and 10.1 ms for the browser operations, and Svelte measured 5.2, 3.0, and 48.1 ms. Browser operations begin only after all targets have 1,000 rows and effects ready. Kudzu emits those rows in HTML while the framework CSR fixtures begin from empty shells, so JavaScript, output, and build values are not architecture-equivalent comparisons. Targeted changed-root notification removed the extra O(n) effect-record scan and reduced Kudzu's selected update from 6.2 to 3.4 ms. List validation, serialization, and reconciliation remain O(n).
|
package/framework/build.mjs
CHANGED
|
@@ -172,6 +172,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
172
172
|
const hasListExpressionAttributes = plans.some(plan => plan.lists.some(list => list.expressionAttributes))
|
|
173
173
|
const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed))
|
|
174
174
|
const hasListEffects = plans.some(plan => plan.lists.some(list => list.effects))
|
|
175
|
+
const hasItemDependencies = plans.some(plan => plan.effects.some(effect => effect.itemDependencies?.length))
|
|
175
176
|
const hasListAsyncParts = hasListExpressions || hasListExpressionAttributes || hasListConditions
|
|
176
177
|
const hasListMounts = hasListConditions || plans.some(plan => plan.lists.some(list => list.mount))
|
|
177
178
|
const hasNestedStateCaptures = hasNestedCaptureState(plans)
|
|
@@ -194,6 +195,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
194
195
|
if (navigationRoutes.length || behaviorCount && (hasSharedRuntime || regularBehaviorCount)) {
|
|
195
196
|
const runtimeFile = hasSharedRuntime ? "./shared-runtime.js" : "./runtime.js"
|
|
196
197
|
let runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, regularStateSeedCount > 0)
|
|
198
|
+
if (!hasItemDependencies) runtime = runtime.replace(/\/\* list-item-hooks \*\/[\s\S]*?\/\* list-item-hooks-end \*\/\n/, "")
|
|
197
199
|
if (hasNavigableEffects) runtime = runtime.replace("export function registerCommitter(commit) {\n committers.push(commit)\n}", "export function registerCommitter(commit) {\n committers.push(commit)\n return () => {\n const index = committers.indexOf(commit)\n if (index !== -1) committers.splice(index, 1)\n }\n}")
|
|
198
200
|
if (hasNavigableOwners) runtime = runtime
|
|
199
201
|
.replace("export function registerMountHook(mount) {\n mountHooks.push(mount)\n}", "export function registerMountHook(mount) {\n mountHooks.push(mount)\n return () => {\n const index = mountHooks.indexOf(mount)\n if (index !== -1) mountHooks.splice(index, 1)\n }\n}")
|
|
@@ -231,6 +233,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
231
233
|
if (listCount) {
|
|
232
234
|
let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
|
|
233
235
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
236
|
+
if (!hasItemDependencies) listRuntime = listRuntime.replace(", notifyListItem", "")
|
|
234
237
|
const stylePatch = ` if (target === "style") {
|
|
235
238
|
const style = serializeStyle(value)
|
|
236
239
|
if (style) node.setAttribute("style", style)
|
|
@@ -249,7 +252,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
249
252
|
__KUDZU_LIST_SEEDS__: String(hasListSeeds),
|
|
250
253
|
__KUDZU_LIST_EFFECTS__: String(hasListEffects),
|
|
251
254
|
__KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
|
|
252
|
-
__KUDZU_LIST_MOUNTS__: String(hasListMounts)
|
|
255
|
+
__KUDZU_LIST_MOUNTS__: String(hasListMounts),
|
|
256
|
+
__KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies)
|
|
253
257
|
})
|
|
254
258
|
}
|
|
255
259
|
if (hasNativeHandlers) {
|
|
@@ -396,7 +400,7 @@ function specializeNativeRuntime(source, events, modules) {
|
|
|
396
400
|
|
|
397
401
|
function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName) {
|
|
398
402
|
const hasCleanup = effects.some(effect => effect.cleanup)
|
|
399
|
-
const hasDependencies = effects.some(effect => effect.dependencies?.length)
|
|
403
|
+
const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
|
|
400
404
|
const hasOwners = effects.some(effect => effect.owner)
|
|
401
405
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
402
406
|
const modules = moduleUrls.map(url => {
|
|
@@ -684,6 +688,7 @@ function mount(lifetime) {
|
|
|
684
688
|
}
|
|
685
689
|
|
|
686
690
|
function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
|
|
691
|
+
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
687
692
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
688
693
|
const modules = moduleUrls.map(url => {
|
|
689
694
|
const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
|
|
@@ -728,7 +733,12 @@ function mount(lifetime) {
|
|
|
728
733
|
}) : undefined
|
|
729
734
|
const unsubscribeMount = __kRuntime.registerMountHook(mountOwned)
|
|
730
735
|
const unsubscribeUnmount = __kRuntime.registerUnmountHook(unmountOwned)
|
|
731
|
-
|
|
736
|
+
${hasItemDependencies ? `const unsubscribeItems = [...new Set(selectedEffects.filter(({ effect }) => effect.itemDependencies?.length).map(({ effect }) => effect.listState))].map(listState => __kRuntime.registerListItemHook(listState, root => {
|
|
737
|
+
if (!active) return
|
|
738
|
+
for (const record of registrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
|
|
739
|
+
schedule()
|
|
740
|
+
}))
|
|
741
|
+
` : ""}for (const record of records) if (record.mounted) start(record)
|
|
732
742
|
mountOwned(document)
|
|
733
743
|
function createRecord(template, mounted = true) {
|
|
734
744
|
const record = { ...template, order: order++, mounted, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
|
|
@@ -833,16 +843,21 @@ function mount(lifetime) {
|
|
|
833
843
|
for (const record of selected) {
|
|
834
844
|
try {
|
|
835
845
|
const values = readDependencies(record)
|
|
836
|
-
if (!record.values || values.some((value, index) => !Object.is(value, record.values[index])))
|
|
837
|
-
record.values = values
|
|
838
|
-
changed.push([record, record.version])
|
|
839
|
-
}
|
|
846
|
+
if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) changed.push([record, record.version])
|
|
840
847
|
} catch (error) {
|
|
841
848
|
console.error(error)
|
|
842
849
|
}
|
|
843
850
|
}
|
|
844
851
|
for (const [record] of changed) await cleanup(record)
|
|
845
|
-
if (active) for (const [record, version] of changed) if (record.mounted && record.version === version)
|
|
852
|
+
if (active) for (const [record, version] of changed) if (record.mounted && record.version === version) {
|
|
853
|
+
try {
|
|
854
|
+
record.values = readDependencies(record)
|
|
855
|
+
invoke(record)
|
|
856
|
+
} catch (error) {
|
|
857
|
+
record.values = undefined
|
|
858
|
+
console.error(error)
|
|
859
|
+
}
|
|
860
|
+
}
|
|
846
861
|
})()
|
|
847
862
|
flushing = operation
|
|
848
863
|
try { await operation } finally {
|
|
@@ -851,11 +866,20 @@ function mount(lifetime) {
|
|
|
851
866
|
}
|
|
852
867
|
}
|
|
853
868
|
function readDependencies(record) {
|
|
854
|
-
|
|
869
|
+
const values = (record.effect.dependencies ?? []).map(id => {
|
|
855
870
|
const value = __kRuntime.browserState.get(id)
|
|
856
871
|
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
857
872
|
return value
|
|
858
873
|
})
|
|
874
|
+
${hasItemDependencies ? `if (record.effect.itemDependencies) {
|
|
875
|
+
const item = JSON.parse(record.marker.dataset.kEffectItem)
|
|
876
|
+
for (const field of record.effect.itemDependencies) {
|
|
877
|
+
const value = item[field]
|
|
878
|
+
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error(\`useEffect() keyed item dependency "\${field}" must remain a JSON-safe primitive\`)
|
|
879
|
+
values.push(value)
|
|
880
|
+
}
|
|
881
|
+
}` : ""}
|
|
882
|
+
return values
|
|
859
883
|
}
|
|
860
884
|
function invoke(record) {
|
|
861
885
|
const token = { active: true }
|
|
@@ -896,7 +920,7 @@ function mount(lifetime) {
|
|
|
896
920
|
disposal = (async () => {
|
|
897
921
|
active = false
|
|
898
922
|
unsubscribeCommitter?.()
|
|
899
|
-
unsubscribeMount()
|
|
923
|
+
${hasItemDependencies ? "for (const unsubscribe of unsubscribeItems) unsubscribe()\n " : ""}unsubscribeMount()
|
|
900
924
|
unsubscribeUnmount()
|
|
901
925
|
pending.clear()
|
|
902
926
|
for (const record of records) if (record.token) record.token.active = false
|
|
@@ -919,6 +943,7 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
919
943
|
module: effect.module,
|
|
920
944
|
handler: effect.handler,
|
|
921
945
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
946
|
+
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
922
947
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
923
948
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
924
949
|
...(effect.list ? { list: true } : {}),
|
|
@@ -929,10 +954,12 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
929
954
|
}
|
|
930
955
|
|
|
931
956
|
function printOwnedEffectEntry(imports, effects, entries) {
|
|
957
|
+
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
958
|
+
const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
|
|
932
959
|
return `${imports.join("\n")}
|
|
933
960
|
const effects = ${inlineJson(effects)}
|
|
934
961
|
const modules = new Map([${entries}])
|
|
935
|
-
const records = effects.map((effect, index) => effect.list ? undefined : createRecord(effect, index)).filter(Boolean)
|
|
962
|
+
${hasItemDependencies ? "let order = 0\n" : ""}const records = effects.map((effect, index) => effect.list ? undefined : createRecord(effect, index)).filter(Boolean)
|
|
936
963
|
const listTemplates = new Map(effects.map((effect, index) => effect.list ? [effect.owner, { effect, index }] : undefined).filter(Boolean))
|
|
937
964
|
const owners = new Map(records.filter(record => record.effect.owner).map(record => [record.effect.owner, record]))
|
|
938
965
|
const listRegistrations = new WeakMap()
|
|
@@ -944,7 +971,7 @@ let flushing = false
|
|
|
944
971
|
let active = true
|
|
945
972
|
for (const record of records) registerDependencies(record)
|
|
946
973
|
function createRecord(effect, index) {
|
|
947
|
-
return { effect, index, mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
|
|
974
|
+
return { effect, index, ${hasItemDependencies ? "order: order++, " : ""}mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
|
|
948
975
|
}
|
|
949
976
|
function registerDependencies(record) {
|
|
950
977
|
for (const id of record.effect.dependencies ?? []) {
|
|
@@ -960,12 +987,17 @@ function unregisterDependencies(record) {
|
|
|
960
987
|
if (!subscribers?.size) dependencies.delete(id)
|
|
961
988
|
}
|
|
962
989
|
}
|
|
963
|
-
__kRuntime.registerCommitter(id => {
|
|
990
|
+
${hasItemDependencies ? `if (${hasOrdinaryDependencies}) ` : ""}__kRuntime.registerCommitter(id => {
|
|
964
991
|
if (!active) return
|
|
965
992
|
for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
|
|
966
993
|
schedule()
|
|
967
994
|
})
|
|
968
|
-
__kRuntime.
|
|
995
|
+
${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect => effect.itemDependencies?.length).map(effect => effect.listState))) __kRuntime.registerListItemHook(listState, root => {
|
|
996
|
+
if (!active) return
|
|
997
|
+
for (const record of listRegistrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
|
|
998
|
+
schedule()
|
|
999
|
+
})
|
|
1000
|
+
` : ""}__kRuntime.registerMountHook(root => {
|
|
969
1001
|
if (!active) return
|
|
970
1002
|
for (const marker of matching(root)) {
|
|
971
1003
|
if (marker.dataset.kEffects) {
|
|
@@ -1053,33 +1085,47 @@ async function flush() {
|
|
|
1053
1085
|
if (!active) return pending.clear()
|
|
1054
1086
|
flushing = true
|
|
1055
1087
|
try {
|
|
1056
|
-
const selected = [...pending].filter(record => record.mounted).sort((left, right) => left.index - right.index)
|
|
1088
|
+
const selected = [...pending].filter(record => record.mounted).sort((left, right) => left.index - right.index${hasItemDependencies ? " || left.order - right.order" : ""})
|
|
1057
1089
|
pending.clear()
|
|
1058
1090
|
const changed = []
|
|
1059
1091
|
for (const record of selected) {
|
|
1060
1092
|
try {
|
|
1061
1093
|
const values = readDependencies(record)
|
|
1062
|
-
if (!record.values || values.some((value, index) => !Object.is(value, record.values[index])))
|
|
1063
|
-
record.values = values
|
|
1064
|
-
changed.push([record, record.version])
|
|
1065
|
-
}
|
|
1094
|
+
if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) changed.push([record, record.version])
|
|
1066
1095
|
} catch (error) {
|
|
1067
1096
|
console.error(error)
|
|
1068
1097
|
}
|
|
1069
1098
|
}
|
|
1070
1099
|
for (const [record] of changed) await invokeCleanup(record)
|
|
1071
|
-
if (active) for (const [record, version] of changed) if (record.mounted && record.version === version)
|
|
1100
|
+
if (active) for (const [record, version] of changed) if (record.mounted && record.version === version) {
|
|
1101
|
+
try {
|
|
1102
|
+
record.values = readDependencies(record)
|
|
1103
|
+
invoke(record)
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
record.values = undefined
|
|
1106
|
+
console.error(error)
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1072
1109
|
} finally {
|
|
1073
1110
|
flushing = false
|
|
1074
1111
|
if (active) schedule()
|
|
1075
1112
|
}
|
|
1076
1113
|
}
|
|
1077
1114
|
function readDependencies(record) {
|
|
1078
|
-
|
|
1115
|
+
const values = (record.effect.dependencies ?? []).map(id => {
|
|
1079
1116
|
const value = browserState.get(id)
|
|
1080
1117
|
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
1081
1118
|
return value
|
|
1082
1119
|
})
|
|
1120
|
+
${hasItemDependencies ? `if (record.effect.itemDependencies) {
|
|
1121
|
+
const item = JSON.parse(record.marker.dataset.kEffectItem)
|
|
1122
|
+
for (const field of record.effect.itemDependencies) {
|
|
1123
|
+
const value = item[field]
|
|
1124
|
+
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error(\`useEffect() keyed item dependency "\${field}" must remain a JSON-safe primitive\`)
|
|
1125
|
+
values.push(value)
|
|
1126
|
+
}
|
|
1127
|
+
}` : ""}
|
|
1128
|
+
return values
|
|
1083
1129
|
}
|
|
1084
1130
|
function invoke(record) {
|
|
1085
1131
|
const token = { active: true }
|
|
@@ -1563,6 +1609,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1563
1609
|
let usesConditional = false
|
|
1564
1610
|
let usesList = false
|
|
1565
1611
|
let usesListEffects = false
|
|
1612
|
+
let usesListItem = false
|
|
1566
1613
|
|
|
1567
1614
|
const collect = node => {
|
|
1568
1615
|
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
@@ -1782,10 +1829,23 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1782
1829
|
if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
|
|
1783
1830
|
if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
|
|
1784
1831
|
if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
|
|
1785
|
-
|
|
1786
|
-
|
|
1832
|
+
const itemDependencies = []
|
|
1833
|
+
const ordinaryDependencies = []
|
|
1834
|
+
let dependencyItem = listEffect?.item
|
|
1835
|
+
for (const dependency of dependencies.elements) {
|
|
1836
|
+
const value = unwrapExpression(dependency)
|
|
1837
|
+
if (!dependencyItem && ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && isDestructuredParameter(value.expression, nearestFunction(node))) dependencyItem = value.expression.text
|
|
1838
|
+
const field = dependencyItem && directProperty(dependency, dependencyItem)
|
|
1839
|
+
if (field) {
|
|
1840
|
+
if (["__proto__", "constructor", "prototype"].includes(field)) effectFail(dependency, `useEffect() keyed item property "${field}" is not supported`)
|
|
1841
|
+
itemDependencies.push(field)
|
|
1842
|
+
} else if (dependencyItem && referencesIdentifier(dependency, dependencyItem)) {
|
|
1843
|
+
effectFail(dependency, "useEffect() keyed item dependencies must be direct item.<field> properties")
|
|
1844
|
+
} else {
|
|
1845
|
+
ordinaryDependencies.push(dependency)
|
|
1846
|
+
}
|
|
1787
1847
|
}
|
|
1788
|
-
const invalidDependency =
|
|
1848
|
+
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
1789
1849
|
if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
1790
1850
|
if (!nearestFunction(node)) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
1791
1851
|
if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
|
|
@@ -1795,17 +1855,19 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1795
1855
|
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
1796
1856
|
if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
|
|
1797
1857
|
const setters = settersForNode(node, settersByFunction)
|
|
1798
|
-
const descriptor = compileNativeCallback(callback, setters, factory, effectHandlers, listEffect?.imports ?? importBindings, clientImports, "effect",
|
|
1858
|
+
const descriptor = compileNativeCallback(callback, setters, factory, effectHandlers, listEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
1859
|
+
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
1799
1860
|
usesBehavior = true
|
|
1800
1861
|
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
1801
1862
|
callback,
|
|
1802
|
-
|
|
1863
|
+
factory.createArrayLiteralExpression(ordinaryDependencies),
|
|
1803
1864
|
factory.createStringLiteral(handlerUrl),
|
|
1804
1865
|
factory.createStringLiteral(descriptor.exportName),
|
|
1805
1866
|
descriptor.states,
|
|
1806
1867
|
descriptor.scope,
|
|
1807
1868
|
factory.createStringLiteral(listEffect ? sourceLocation(listEffect.source, listEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
1808
|
-
returns.cleanup ? factory.createTrue() : factory.createFalse()
|
|
1869
|
+
returns.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
1870
|
+
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field)))
|
|
1809
1871
|
])
|
|
1810
1872
|
}
|
|
1811
1873
|
|
|
@@ -1918,6 +1980,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1918
1980
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
|
|
1919
1981
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listConditional"), factory.createIdentifier("__kListConditional")))
|
|
1920
1982
|
}
|
|
1983
|
+
if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
|
|
1921
1984
|
if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
|
|
1922
1985
|
if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
|
|
1923
1986
|
const behaviorImport = factory.createImportDeclaration(
|
package/framework/core.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
78
78
|
commands?: Array<[string, string, unknown]>
|
|
79
79
|
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
80
80
|
}>
|
|
81
|
-
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; lifetime?: "layout" | "route"; dependencies?: string[]; cleanup?: true; owner?: string; list?: true }>
|
|
81
|
+
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; lifetime?: "layout" | "route"; dependencies?: string[]; itemDependencies?: string[]; listState?: string; cleanup?: true; owner?: string; list?: true }>
|
|
82
82
|
bindings: Array<{
|
|
83
83
|
target: string
|
|
84
84
|
state?: string
|
package/framework/core.mjs
CHANGED
|
@@ -66,9 +66,10 @@ function createSignal(id, value) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup) {
|
|
69
|
+
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = []) {
|
|
70
70
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
71
71
|
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
72
|
+
if (itemDependencies.length && !renderContext.listDepth) throw new Error(`${source} useEffect() item-property dependencies are only supported in direct keyed row components`)
|
|
72
73
|
const dependencyIds = dependencies.map(dependency => {
|
|
73
74
|
if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
74
75
|
return dependency.id
|
|
@@ -88,13 +89,16 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
88
89
|
if (!owner) throw new Error(`${source} Keyed row effects must have the same hook order for every item`)
|
|
89
90
|
}
|
|
90
91
|
effects.push(owner)
|
|
92
|
+
if (!renderContext.listRoot.template) for (const field of itemDependencies) {
|
|
93
|
+
if (!validEffectDependency(renderContext.listRoot.item[field])) throw new Error(`${source} useEffect() keyed item dependency "${field}" must be a JSON-safe primitive`)
|
|
94
|
+
}
|
|
91
95
|
} else if (renderContext.conditionDepth) {
|
|
92
96
|
const owners = renderContext.effectOwners.at(-1)
|
|
93
97
|
if (!owners) throw new Error(`${source} useEffect() inside conditional DOM must belong to a rendered function component`)
|
|
94
98
|
owner = nextRenderId("e")
|
|
95
99
|
owners.push(owner)
|
|
96
100
|
}
|
|
97
|
-
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
101
|
+
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
98
102
|
renderContext.hasBehaviors = true
|
|
99
103
|
renderContext.hasEffects = true
|
|
100
104
|
}
|
|
@@ -303,6 +307,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
303
307
|
module: effect.module,
|
|
304
308
|
handler: effect.handler,
|
|
305
309
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
310
|
+
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
306
311
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
307
312
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
308
313
|
...(effect.list ? { list: true } : {}),
|
|
@@ -682,7 +687,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
682
687
|
renderContext.listTemplate = true
|
|
683
688
|
renderContext.listEffectOwners = []
|
|
684
689
|
renderContext.listFields = new Set([node.keyField])
|
|
685
|
-
renderContext.listRoot = { id, template: true, effects: [], item: {} }
|
|
690
|
+
renderContext.listRoot = { id, state: node.items.id, template: true, effects: [], item: {} }
|
|
686
691
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
687
692
|
if (template.includes("data-k-native-") || template.includes("data-k-effects=")) descriptor.mount = true
|
|
688
693
|
if (template.includes("data-k-effects=")) descriptor.effects = true
|
|
@@ -698,7 +703,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
698
703
|
renderContext.listTemplate = false
|
|
699
704
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
700
705
|
for (const item of node.items.value) {
|
|
701
|
-
renderContext.listRoot = { id, key: item[node.keyField], template: false, effects: [], item }
|
|
706
|
+
renderContext.listRoot = { id, state: node.items.id, key: item[node.keyField], template: false, effects: [], item }
|
|
702
707
|
current += await renderNode(node.render(item), namespace, selectValue)
|
|
703
708
|
}
|
|
704
709
|
renderContext.lists.push(descriptor)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { browserState, mountDom, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
|
|
1
|
+
import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
|
|
2
2
|
|
|
3
3
|
const listTargets = new Map()
|
|
4
4
|
const listRegistrations = new WeakMap()
|
|
@@ -102,6 +102,7 @@ function updateList(list) {
|
|
|
102
102
|
added = true
|
|
103
103
|
} else if (list.values.get(token) !== value) {
|
|
104
104
|
fillListItem(node, item)
|
|
105
|
+
if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
|
|
105
106
|
}
|
|
106
107
|
next.push([token, node])
|
|
107
108
|
values.set(token, value)
|
|
@@ -19,6 +19,24 @@ const committers = []
|
|
|
19
19
|
const mountHooks = []
|
|
20
20
|
const unmountHooks = []
|
|
21
21
|
|
|
22
|
+
/* list-item-hooks */
|
|
23
|
+
const listItemHooks = new Map()
|
|
24
|
+
|
|
25
|
+
export function registerListItemHook(id, hook) {
|
|
26
|
+
const hooks = listItemHooks.get(id) ?? new Set()
|
|
27
|
+
hooks.add(hook)
|
|
28
|
+
listItemHooks.set(id, hooks)
|
|
29
|
+
return () => {
|
|
30
|
+
hooks.delete(hook)
|
|
31
|
+
if (!hooks.size) listItemHooks.delete(id)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function notifyListItem(id, root) {
|
|
36
|
+
for (const hook of listItemHooks.get(id) ?? []) hook(root)
|
|
37
|
+
}
|
|
38
|
+
/* list-item-hooks-end */
|
|
39
|
+
|
|
22
40
|
export function registerCommitter(commit) {
|
|
23
41
|
committers.push(commit)
|
|
24
42
|
}
|