@kudzujs/core 0.17.0 → 0.17.2

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 CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.17.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Current release: [0.17.0](https://github.com/kudzujs/kudzu/releases/tag/v0.17.0).** Selected calculation effect dependencies accept static primitive configuration alongside at least two direct state inputs, reusing existing normalization and evaluators. Proven read-only effects omit state-write machinery, and ordinary owned effects omit keyed-row machinery. Measured fixture savings range from 253 to 990 raw JavaScript bytes without new runtime concepts. Generator `create-kudzu@0.1.161` targets core `^0.17.0`. Restoration latency and AI-cost/1.0 gates remain unresolved.
17
+ **Current release: [0.17.2](https://github.com/kudzujs/kudzu/releases/tag/v0.17.2).** Delayed evaluators cannot regain authority after DOM or source-state release, including same-node remount and state-ID reuse. Keyed conditions now settle correctly when row data and parent selection change in one turn. Native Chrome regressions cover both fixes; static siblings remain JavaScript-free. Generator `create-kudzu@0.1.163` targets core `^0.17.2`. Output sizes have mixed deltas; no speed improvement is claimed. Restoration latency and AI-cost/1.0 gates remain unresolved.
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
@@ -5,8 +5,6 @@ import { serializeStyle } from "./style.js"
5
5
  const imports = new Map()
6
6
  const bindingTargets = new Map()
7
7
  const conditionTargets = globalThis.__KUDZU_CONDITIONS__ && new Map()
8
- const mountedBindings = new WeakSet()
9
- const mountedConditions = globalThis.__KUDZU_CONDITIONS__ && new WeakSet()
10
8
  const bindingRegistrations = new WeakMap()
11
9
  const conditionRegistrations = globalThis.__KUDZU_CONDITIONS__ && new WeakMap()
12
10
  const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== "undefined" ? JSON.parse(document.body.dataset.kTextBindings ?? "[]") : []
@@ -44,7 +42,7 @@ function commitBindings(id) {
44
42
  if (bindings) {
45
43
  for (const binding of bindings) {
46
44
  if (!binding.node.isConnected) bindings.delete(binding)
47
- else patchBinding(binding.node, binding.target, binding.read())
45
+ else if (binding.read) patchBinding(binding.node, binding.target, binding.read())
48
46
  }
49
47
  }
50
48
  if (!globalThis.__KUDZU_CONDITIONS__) return
@@ -67,8 +65,7 @@ if (typeof document !== "undefined") mountDom(document)
67
65
 
68
66
  function mountBindings(root) {
69
67
  if (globalThis.__KUDZU_ELEMENT_BINDINGS__ !== false) for (const node of matching(root, bindingSelector)) {
70
- if (mountedBindings.has(node)) continue
71
- mountedBindings.add(node)
68
+ if (bindingRegistrations.has(node)) continue
72
69
  const registrations = []
73
70
  bindingRegistrations.set(node, registrations)
74
71
  const descriptors = []
@@ -85,23 +82,14 @@ function mountBindings(root) {
85
82
  patchBinding(node, target, binding.read())
86
83
  continue
87
84
  }
88
- loadEvaluator(descriptor).then(evaluator => {
89
- if (!node.isConnected) return
90
- const binding = { node, target, read: evaluator.read }
91
- for (const id of evaluator.stateIds) {
92
- register(bindingTargets, id, binding)
93
- registrations.push([id, binding])
94
- }
95
- patchBinding(node, target, binding.read())
96
- }).catch(error => console.error(error))
85
+ mountBindingEvaluator(node, target, descriptor, registrations)
97
86
  }
98
87
  }
99
88
  if (globalThis.__KUDZU_TEXT_BINDINGS__) {
100
89
  for (const node of textBindingStarts(root)) {
101
- if (mountedBindings.has(node)) continue
90
+ if (bindingRegistrations.has(node)) continue
102
91
  const descriptor = textDescriptors[Number(node.data.slice("k-text:".length))]
103
92
  if (!descriptor) continue
104
- mountedBindings.add(node)
105
93
  const registrations = []
106
94
  bindingRegistrations.set(node, registrations)
107
95
  if (descriptor.state) {
@@ -111,47 +99,56 @@ function mountBindings(root) {
111
99
  patchBinding(node, "text", binding.read())
112
100
  continue
113
101
  }
114
- loadEvaluator(descriptor).then(evaluator => {
115
- if (!node.isConnected) return
116
- const binding = { node, target: "text", read: evaluator.read }
117
- for (const id of evaluator.stateIds) {
118
- register(bindingTargets, id, binding)
119
- registrations.push([id, binding])
120
- }
121
- patchBinding(node, "text", binding.read())
122
- }).catch(error => console.error(error))
102
+ mountBindingEvaluator(node, "text", descriptor, registrations)
123
103
  }
124
104
  }
125
105
  }
126
106
 
107
+ function mountBindingEvaluator(node, target, descriptor, registrations) {
108
+ const binding = { node, target }
109
+ // Register dependencies before loading so state release also revokes pending work.
110
+ for (const id of bindingStateIds(descriptor)) {
111
+ register(bindingTargets, id, binding)
112
+ registrations.push([id, binding])
113
+ }
114
+ loadEvaluator(descriptor).then(evaluator => {
115
+ if (!node.isConnected || bindingRegistrations.get(node) !== registrations) return
116
+ binding.read = evaluator.read
117
+ patchBinding(node, target, binding.read())
118
+ }).catch(error => console.error(error))
119
+ }
120
+
127
121
  function mountConditions(root) {
128
122
  for (const start of matching(root, "template[data-k-if]")) {
129
- if (mountedConditions.has(start)) continue
130
- mountedConditions.add(start)
123
+ if (conditionRegistrations.has(start)) continue
124
+ // Reserve membership while the evaluator loads, before subscriptions exist.
125
+ conditionRegistrations.set(start, undefined)
131
126
  const descriptor = JSON.parse(start.dataset.kIf)
132
127
  const end = findEnd(start, descriptor.id)
133
128
  const truthy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgTrue : start.content.querySelector("template[data-k-true]")
134
129
  const falsy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgFalse : start.content.querySelector("template[data-k-false]")
135
130
  if (!end || !truthy || !falsy) continue
136
131
  const condition = { start, end, truthy, falsy, svg: globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: globalThis.__KUDZU_CONDITION_MOUNTS__ !== false && descriptor.mount, owned: globalThis.__KUDZU_CONDITION_STATE__ !== false && descriptor.owned }
132
+ const registrations = []
133
+ const registration = { condition, registrations }
134
+ conditionRegistrations.set(start, registration)
135
+ for (const id of descriptor.state ? [descriptor.state] : bindingStateIds(descriptor)) {
136
+ register(conditionTargets, id, condition)
137
+ registrations.push([id, condition])
138
+ }
137
139
  if (globalThis.__KUDZU_CONDITION_STATE__ !== false) mountConditionStates(condition, Boolean(descriptor.initial), false)
138
140
  const mount = evaluator => {
139
- if (!start.isConnected) return
141
+ if (!start.isConnected || conditionRegistrations.get(start) !== registration) return
140
142
  condition.read = evaluator.read
141
- const registrations = []
142
- for (const id of evaluator.stateIds) {
143
- register(conditionTargets, id, condition)
144
- registrations.push([id, condition])
145
- }
146
- conditionRegistrations.set(start, { condition, registrations })
147
143
  updateCondition(condition)
148
144
  }
149
- if (descriptor.state) mount({ read: () => browserState.get(descriptor.state), stateIds: [descriptor.state] })
145
+ if (descriptor.state) mount({ read: () => browserState.get(descriptor.state) })
150
146
  else loadEvaluator(descriptor).then(mount).catch(error => console.error(error))
151
147
  }
152
148
  }
153
149
 
154
150
  function updateCondition(condition) {
151
+ if (!condition.read) return
155
152
  const value = condition.read()
156
153
  const next = conditionKey(condition.kind, value)
157
154
  if (next === condition.current) return
@@ -173,7 +170,7 @@ function updateCondition(condition) {
173
170
  const select = globalThis.__KUDZU_VALUE_BINDINGS__ !== false && condition.start.closest("select[data-k-bind-value]")
174
171
  if (select) {
175
172
  for (const binding of new Set((bindingRegistrations.get(select) ?? []).map(([, entry]) => entry))) {
176
- patchBinding(binding.node, binding.target, binding.read())
173
+ if (binding.read) patchBinding(binding.node, binding.target, binding.read())
177
174
  }
178
175
  }
179
176
  }
@@ -182,13 +179,11 @@ function unmountBindings(root) {
182
179
  if (globalThis.__KUDZU_ELEMENT_BINDINGS__ !== false) for (const node of matching(root, bindingSelector)) {
183
180
  for (const [id, binding] of bindingRegistrations.get(node) ?? []) bindingTargets.get(id)?.delete(binding)
184
181
  bindingRegistrations.delete(node)
185
- mountedBindings.delete(node)
186
182
  }
187
183
  if (globalThis.__KUDZU_TEXT_BINDINGS__) {
188
184
  for (const node of textBindingStarts(root)) {
189
185
  for (const [id, binding] of bindingRegistrations.get(node) ?? []) bindingTargets.get(id)?.delete(binding)
190
186
  bindingRegistrations.delete(node)
191
- mountedBindings.delete(node)
192
187
  }
193
188
  }
194
189
  }
@@ -199,14 +194,14 @@ function unmountConditions(root) {
199
194
  for (const [id, condition] of registration?.registrations ?? []) conditionTargets.get(id)?.delete(condition)
200
195
  if (globalThis.__KUDZU_CONDITION_STATE__ !== false && registration?.condition) unmountConditionStates(registration.condition, registration.condition.current === "true")
201
196
  conditionRegistrations.delete(start)
202
- mountedConditions.delete(start)
203
197
  }
204
198
  }
205
199
 
206
200
  function releaseBindings(id) {
207
201
  for (const binding of bindingTargets.get(id) ?? []) {
208
202
  for (const [bindingId, entry] of bindingRegistrations.get(binding.node) ?? []) bindingTargets.get(bindingId)?.delete(entry)
209
- bindingRegistrations.delete(binding.node)
203
+ // Keep mounted membership until unmount so released bindings cannot regain authority.
204
+ bindingRegistrations.set(binding.node, undefined)
210
205
  }
211
206
  bindingTargets.delete(id)
212
207
  if (!globalThis.__KUDZU_CONDITIONS__) return
@@ -214,7 +209,7 @@ function releaseBindings(id) {
214
209
  const registration = conditionRegistrations.get(condition.start)
215
210
  for (const [conditionId, entry] of registration?.registrations ?? []) conditionTargets.get(conditionId)?.delete(entry)
216
211
  if (globalThis.__KUDZU_CONDITION_STATE__ !== false) unmountConditionStates(condition, condition.current === "true")
217
- conditionRegistrations.delete(condition.start)
212
+ conditionRegistrations.set(condition.start, undefined)
218
213
  }
219
214
  conditionTargets.delete(id)
220
215
  }
@@ -650,13 +650,6 @@ function fillListParts(root, parts, item, revision, index = 0, previous) {
650
650
  }
651
651
  }
652
652
  fillListExpressions(root, parts, item, revision, index)
653
- if (__KUDZU_LIST_CONDITIONS__) {
654
- for (const [marker, descriptor] of parts.conditions) {
655
- evaluate(descriptor, item, index).then(value => {
656
- if (revisions.get(root) === revision && marker.isConnected) updateListCondition(marker, descriptor.kind, value, item, index)
657
- }).catch(error => console.error(error))
658
- }
659
- }
660
653
  }
661
654
 
662
655
  function fillListExpressions(root, parts, item, revision, index) {
@@ -676,6 +669,14 @@ function fillListExpressions(root, parts, item, revision, index) {
676
669
  }
677
670
  }
678
671
  }
672
+ // Parent-state commits invalidate the shared row revision, including pending conditions.
673
+ if (__KUDZU_LIST_CONDITIONS__) {
674
+ for (const [marker, descriptor] of parts.conditions) {
675
+ evaluate(descriptor, item, index).then(value => {
676
+ if (revisions.get(root) === revision && marker.isConnected) updateListCondition(marker, descriptor.kind, value, item, index)
677
+ }).catch(error => console.error(error))
678
+ }
679
+ }
679
680
  }
680
681
 
681
682
  function updateListExpressions(list) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",