@kudzujs/core 0.6.26 → 0.6.28

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.
@@ -1,4 +1,5 @@
1
1
  import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
2
+ import { selectCollection } from "./collection-selector.js"
2
3
 
3
4
  const listTargets = new Map()
4
5
  const listRegistrations = new WeakMap()
@@ -7,11 +8,15 @@ const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
7
8
  const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
8
9
  const itemParts = new WeakMap()
9
10
  const listItems = new WeakMap()
11
+ const listIndexes = __KUDZU_LIST_INDEXES__ ? new WeakMap() : undefined
12
+ const ownershipPaths = __KUDZU_LIST_ROW_HOOKS__ ? new WeakMap() : undefined
13
+ const rowReplacements = __KUDZU_LIST_ROW_HOOKS__ ? new WeakMap() : undefined
10
14
  const ownedLists = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
11
15
  const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
12
16
  const conditionTemplates = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
13
17
  const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
14
18
  const childPrototypes = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
19
+ const itemPartPlans = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
15
20
 
16
21
  function commitLists(id) {
17
22
  const lists = listTargets.get(id)
@@ -31,7 +36,7 @@ if (typeof document !== "undefined") mountDom(document)
31
36
  function mountLists(root) {
32
37
  let owner = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement
33
38
  while (owner && !listItems.has(owner)) owner = owner.parentElement
34
- if (owner) fillListParts(root, listItemParts(root), listItems.get(owner), 0)
39
+ if (owner) fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)
35
40
  for (const start of matching(root, "template[data-k-list]")) {
36
41
  if (mountedLists.has(start)) continue
37
42
  mountedLists.add(start)
@@ -40,10 +45,11 @@ function mountLists(root) {
40
45
  const roots = listRoots(start, end)
41
46
  const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
42
47
  const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start.content.firstElementChild
43
- if (__KUDZU_LIST_ROW_STATES__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])
48
+ if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeGeneralRowHooks(descriptor, descriptor.keys[index], roots[index], nested?.owner)
44
49
  const parts = listItemPartPlan(templateRoot, descriptor.nested)
45
50
  for (const root of roots) {
46
51
  if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
52
+ if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) itemPartPlans.set(root, parts)
47
53
  const initialParts = listItemParts(root, descriptor.nested)
48
54
  mapListConditionTemplates(parts.conditions, initialParts.conditions)
49
55
  } else mapListItemParts(parts, root, descriptor.nested)
@@ -53,10 +59,11 @@ function mountLists(root) {
53
59
  const list = {
54
60
  start,
55
61
  descriptor,
56
- ...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototype ? { childPrototype: nested.childPrototype } : {}) } : {}),
62
+ ...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototypes?.size ? { childPrototypes: nested.childPrototypes } : {}) } : {}),
57
63
  parts,
58
64
  seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
59
65
  roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
66
+ ...(__KUDZU_LIST_STABLE_FAST_PATHS__ ? { orderedRoots: roots } : {}),
60
67
  values: new Map(),
61
68
  items: undefined,
62
69
  container: roots[0]?.parentNode,
@@ -65,8 +72,10 @@ function mountLists(root) {
65
72
  }
66
73
  if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) {
67
74
  if (!list.owner) throw new Error("Nested keyed list has no parent row")
68
- if (ownedLists.has(list.owner)) throw new Error("Keyed list rows support one nested keyed list")
69
- ownedLists.set(list.owner, list)
75
+ const lists = ownedLists.get(list.owner) ?? new Map()
76
+ if (lists.has(descriptor.id)) throw new Error(`Duplicate nested keyed list ID: ${descriptor.id}`)
77
+ lists.set(descriptor.id, list)
78
+ ownedLists.set(list.owner, lists)
70
79
  listRegistrations.set(start, { list, owner: list.owner })
71
80
  } else {
72
81
  register(listTargets, descriptor.state, list)
@@ -84,41 +93,50 @@ function unregisterList(start) {
84
93
  const registration = listRegistrations.get(start)
85
94
  if (registration) {
86
95
  if (__KUDZU_NESTED_LISTS__ && registration.owner) {
87
- if (ownedLists.get(registration.owner) === registration.list) ownedLists.delete(registration.owner)
96
+ const lists = ownedLists.get(registration.owner)
97
+ if (lists?.get(registration.list.descriptor.id) === registration.list) lists.delete(registration.list.descriptor.id)
98
+ if (!lists?.size) ownedLists.delete(registration.owner)
88
99
  } else {
89
100
  const lists = listTargets.get(registration.state)
90
101
  lists?.delete(registration.list)
91
102
  if (!lists?.size) listTargets.delete(registration.state)
92
103
  }
93
- if (__KUDZU_LIST_ROW_STATES__ && registration.list.descriptor.rowStates) for (const token of registration.list.roots.keys()) deleteRowStates(registration.list.descriptor, token)
104
+ if (__KUDZU_LIST_ROW_HOOKS__ && registration.list.descriptor.rowStates) for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))
94
105
  }
95
106
  listRegistrations.delete(start)
96
107
  mountedLists.delete(start)
97
108
  }
98
109
 
99
110
  function updateList(list) {
100
- const items = __KUDZU_NESTED_LISTS__ && list.descriptor.ownerField
111
+ let items = __KUDZU_NESTED_LISTS__ && list.descriptor.ownerField
101
112
  ? listItems.get(list.owner)?.[list.descriptor.ownerField]
102
113
  : browserState.get(list.descriptor.state)
114
+ if (__KUDZU_NESTED_LISTS__ && list.descriptor.ownerField && items == null) items = []
115
+ if (__KUDZU_COLLECTION_SELECTORS__) items = selectCollection(items, list.descriptor.selector)
103
116
  if (!Array.isArray(items)) throw new Error(list.descriptor.ownerField ? `Nested keyed list property "${list.descriptor.ownerField}" must remain an array` : "Keyed list state must remain an array")
104
- if (__KUDZU_NESTED_LISTS__ && (list.descriptor.child || list.descriptor.ownerField) && list.items && updateNestedList(list, items)) return
105
- if (__KUDZU_NESTED_LISTS__ && list.descriptor.child) validateChildLists(items, list.descriptor.child)
106
- if (list.descriptor.reducer && list.items && updateReducerList(list, items)) return
117
+ if (__KUDZU_NESTED_LISTS__ && (list.descriptor.children || list.descriptor.ownerField) && !list.descriptor.indexed && !list.descriptor.selector && list.descriptor.key !== null && list.items && updateNestedList(list, items)) return
118
+ if (__KUDZU_NESTED_LISTS__ && list.descriptor.children) validateChildLists(items, list.descriptor.children)
119
+ if (list.descriptor.reducer && !list.descriptor.selector && list.descriptor.key !== null && list.items && updateReducerList(list, items)) return
120
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__ && list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && !list.descriptor.reducer && !list.descriptor.children && !list.descriptor.ownerField && list.items && updateStableList(list, items)) return
107
121
  const entries = []
108
122
  const keys = new Set()
109
123
  const seen = new Set()
110
- for (const item of items) {
111
- const key = item?.[list.descriptor.key]
124
+ const referenceOnly = usesItemReferences(list)
125
+ for (const [index, item] of items.entries()) {
126
+ const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]
112
127
  if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
113
128
  assertListItem(item)
114
- const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
129
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed, !referenceOnly) : undefined
115
130
  if (seededValue === undefined) assertListValue(item, seen, true)
116
131
  const token = keyToken(key)
117
132
  if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
118
133
  keys.add(token)
119
- entries.push({ item, key, token, value: seededValue === undefined ? JSON.stringify(item) : seededValue })
134
+ entries.push({ item, index, key, token, value: referenceOnly ? item : seededValue === undefined ? JSON.stringify(item) : seededValue })
120
135
  }
121
- if (list.values.size && entries.every(entry => !list.values.has(entry.token) || list.values.get(entry.token) === entry.value)) {
136
+ if (list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && (referenceOnly || list.values.size) && entries.every(entry => {
137
+ const root = list.roots.get(entry.token)
138
+ return !root || (referenceOnly ? listItems.get(root) === entry.item : list.values.get(entry.token) === entry.value)
139
+ })) {
122
140
  const currentTokens = [...list.roots.keys()]
123
141
  const nextTokens = entries.map(entry => entry.token)
124
142
  if (nextTokens.length === currentTokens.length && nextTokens.every((token, index) => token === currentTokens[currentTokens.length - index - 1])) {
@@ -127,6 +145,7 @@ function updateList(list) {
127
145
  reordered.append(...nextTokens.map(token => list.roots.get(token)))
128
146
  parent.insertBefore(reordered, list.boundary)
129
147
  list.roots = new Map(nextTokens.map(token => [token, list.roots.get(token)]))
148
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = nextTokens.map(token => list.roots.get(token))
130
149
  list.container ??= parent
131
150
  return
132
151
  }
@@ -136,12 +155,14 @@ function updateList(list) {
136
155
  if (removed && nextTokens.every((token, index) => token === currentTokens[index >= removedIndex ? index + 1 : index])) {
137
156
  removeListRoot(list, removed)
138
157
  list.roots = new Map(nextTokens.map(token => [token, list.roots.get(token)]))
158
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = nextTokens.map(token => list.roots.get(token))
139
159
  return
140
160
  }
141
161
  }
142
162
  if (nextTokens.length === currentTokens.length + 1 && currentTokens.every((token, index) => token === nextTokens[index])) {
143
163
  const entry = entries.at(-1)
144
164
  addListRoot(list, entry)
165
+ list.items = items
145
166
  return
146
167
  }
147
168
  }
@@ -150,25 +171,25 @@ function updateList(list) {
150
171
  const parent = list.container ?? list.start.parentNode
151
172
  const additions = parent.ownerDocument.createDocumentFragment()
152
173
  let added = false
153
- for (const { item, key, token, value } of entries) {
174
+ for (const { item, index, key, token, value } of entries) {
154
175
  let node = list.roots.get(token)
155
176
  if (!node) {
156
177
  node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
157
178
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
158
179
  if (!node) throw new Error("Keyed list template has no root element")
159
180
  node.removeAttribute("data-k-list-root")
160
- if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
161
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
181
+ if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
182
+ if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
162
183
  mapListItemParts(list.parts, node, list.descriptor.nested)
163
- fillListItem(node, item, list.descriptor.nested)
184
+ fillListItem(node, item, list.descriptor.nested, index)
164
185
  additions.append(node)
165
186
  added = true
166
- } else if (list.values.get(token) !== value) {
167
- fillListItem(node, item, list.descriptor.nested)
187
+ } else if ((referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null) {
188
+ fillListItem(node, item, list.descriptor.nested, index)
168
189
  if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
169
190
  }
170
191
  next.push([token, node])
171
- values.set(token, value)
192
+ if (!referenceOnly) values.set(token, value)
172
193
  }
173
194
  for (const [token, node] of list.roots) {
174
195
  if (keys.has(token)) continue
@@ -176,7 +197,7 @@ function updateList(list) {
176
197
  unmountDom(node)
177
198
  node.remove()
178
199
  } else node.remove()
179
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, token)
200
+ if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
180
201
  }
181
202
  if (added) {
182
203
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
@@ -206,10 +227,102 @@ function updateList(list) {
206
227
  }
207
228
  }
208
229
  list.roots = new Map(next)
230
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = next.map(([, node]) => node)
209
231
  list.values = values
210
232
  list.items = items
211
233
  }
212
234
 
235
+ /* stable-list-fast-path */
236
+ function updateStableList(list, items) {
237
+ const previous = list.items
238
+ const referenceOnly = usesItemReferences(list)
239
+ if (items.length < previous.length) return false
240
+ const appending = items.length > previous.length
241
+ const keys = new Set()
242
+ const seen = new Set()
243
+ const keyField = list.descriptor.key
244
+ const fragment = list.start.ownerDocument.createDocumentFragment()
245
+ let stable = true
246
+ for (let index = 0; index < items.length; index++) {
247
+ const item = items[index]
248
+ assertListItem(item)
249
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed, !referenceOnly) : undefined
250
+ if (seededValue === undefined) assertListValue(item, seen, true)
251
+ const key = item[keyField]
252
+ if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
253
+ const token = keyToken(key)
254
+ if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
255
+ keys.add(token)
256
+ if (index < previous.length && (key !== previous[index][keyField] || appending && item !== previous[index])) stable = false
257
+ if (!stable || items.length === previous.length + 1 || index < previous.length) continue
258
+ let node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
259
+ if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
260
+ if (!node) throw new Error("Keyed list template has no root element")
261
+ node.removeAttribute("data-k-list-root")
262
+ if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
263
+ if (list.parts.directFill) fillStructuralListParts(list.parts, node, item)
264
+ else fillListItem(node, item, list.descriptor.nested, index, mapListItemParts(list.parts, node, list.descriptor.nested))
265
+ fragment.append(node)
266
+ }
267
+ if (!stable) return false
268
+ if (appending) {
269
+ if (items.length === previous.length + 1) {
270
+ const item = items.at(-1)
271
+ const key = item[keyField]
272
+ const token = keyToken(key)
273
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed, !referenceOnly) : undefined
274
+ addListRoot(list, { item, index: previous.length, key, token, value: referenceOnly ? item : seededValue === undefined ? JSON.stringify(item) : seededValue })
275
+ if (referenceOnly) list.values.clear()
276
+ list.items = items
277
+ return true
278
+ }
279
+ const parent = list.container ?? list.start.parentNode
280
+ let node = fragment.firstChild
281
+ for (let index = previous.length; node; index++) {
282
+ const item = items[index]
283
+ const token = keyToken(item[keyField])
284
+ list.roots.set(token, node)
285
+ list.orderedRoots.push(node)
286
+ if (!referenceOnly) {
287
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
288
+ list.values.set(token, seededValue === undefined ? JSON.stringify(item) : seededValue)
289
+ }
290
+ node = node.nextSibling
291
+ }
292
+ const firstAdded = fragment.firstChild
293
+ parent.insertBefore(fragment, list.boundary)
294
+ if (referenceOnly) list.values.clear()
295
+ if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) for (let node = firstAdded; node !== list.boundary; node = node.nextSibling) mountDom(node)
296
+ list.container ??= parent
297
+ list.items = items
298
+ return true
299
+ }
300
+ if (referenceOnly) {
301
+ for (let index = 0; index < items.length; index++) {
302
+ const item = items[index]
303
+ if (item !== previous[index]) fillListItem(list.orderedRoots[index], item, list.descriptor.nested, index)
304
+ }
305
+ } else {
306
+ for (let index = 0; index < items.length; index++) {
307
+ const item = items[index]
308
+ if (item === previous[index]) continue
309
+ const token = keyToken(item[keyField])
310
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
311
+ const value = seededValue === undefined ? JSON.stringify(item) : seededValue
312
+ if (list.values.get(token) !== value) {
313
+ const node = list.orderedRoots[index]
314
+ fillListItem(node, item, list.descriptor.nested, index)
315
+ if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
316
+ list.values.set(token, value)
317
+ }
318
+ }
319
+ }
320
+ if (referenceOnly) list.values.clear()
321
+ list.items = items
322
+ return true
323
+ }
324
+ /* stable-list-fast-path-end */
325
+
213
326
  function updateNestedList(list, items) {
214
327
  const previous = list.items
215
328
  if (items === previous) return false
@@ -274,7 +387,7 @@ function nestedListEntry(list, item) {
274
387
  const key = item?.[list.descriptor.key]
275
388
  if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
276
389
  assertListItem(item)
277
- if (list.descriptor.child) validateChildLists([item], list.descriptor.child)
390
+ if (list.descriptor.children) validateChildLists([item], list.descriptor.children)
278
391
  assertListValue(item, new Set(), true)
279
392
  return { item, key, token: keyToken(key), value: JSON.stringify(item) }
280
393
  }
@@ -326,20 +439,21 @@ function updateReducerList(list, items) {
326
439
  return false
327
440
  }
328
441
 
329
- function addListRoot(list, { item, key, token, value }) {
442
+ function addListRoot(list, { item, index = list.roots.size, key, token, value }) {
330
443
  let node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
331
444
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
332
445
  if (!node) throw new Error("Keyed list template has no root element")
333
446
  node.removeAttribute("data-k-list-root")
334
- if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
335
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
447
+ if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
448
+ if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
336
449
  mapListItemParts(list.parts, node, list.descriptor.nested)
337
- fillListItem(node, item, list.descriptor.nested)
450
+ fillListItem(node, item, list.descriptor.nested, index)
338
451
  const parent = list.container ?? list.start.parentNode
339
452
  parent.insertBefore(node, list.boundary)
340
453
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node)
341
454
  list.roots.set(token, node)
342
- list.values.set(token, value)
455
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots.push(node)
456
+ if (!usesItemReferences(list)) list.values.set(token, value)
343
457
  list.container ??= parent
344
458
  }
345
459
 
@@ -347,24 +461,25 @@ function removeListRoot(list, token) {
347
461
  const node = list.roots.get(token)
348
462
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) unmountDom(node)
349
463
  node.remove()
350
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, token)
464
+ if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
351
465
  list.roots.delete(token)
352
466
  list.values.delete(token)
353
467
  }
354
468
 
355
- function fillListItem(root, item, nested = false) {
469
+ function fillListItem(root, item, nested = false, index = 0, parts = listItemParts(root, nested)) {
356
470
  listItems.set(root, item)
471
+ if (__KUDZU_LIST_INDEXES__) listIndexes.set(root, index)
357
472
  const revision = __KUDZU_LIST_ASYNC_PARTS__ ? (revisions.get(root) ?? 0) + 1 : 0
358
473
  if (__KUDZU_LIST_ASYNC_PARTS__) revisions.set(root, revision)
359
- const parts = listItemParts(root, nested)
360
- fillListParts(root, parts, item, revision)
474
+ fillListParts(root, parts, item, revision, index)
361
475
  if (__KUDZU_NESTED_LISTS__) {
362
- const child = ownedLists.get(root)
363
- if (child) updateList(child)
476
+ const children = ownedLists.get(root)
477
+ if (children) for (const child of children.values()) updateList(child)
364
478
  }
479
+ if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))
365
480
  }
366
481
 
367
- function fillListParts(root, parts, item, revision) {
482
+ function fillListParts(root, parts, item, revision, index = 0) {
368
483
  if (__KUDZU_LIST_EFFECTS__) for (const node of parts.effects) node.dataset.kEffectItem = JSON.stringify(item)
369
484
  for (const [node, field] of parts.directTexts) {
370
485
  const text = item?.[field]
@@ -387,31 +502,31 @@ function fillListParts(root, parts, item, revision) {
387
502
  if (__KUDZU_LIST_EVENTS__) {
388
503
  for (const [node, events] of parts.events) {
389
504
  for (const [event, native] of JSON.parse(events)) {
390
- native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
505
+ native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value]))
391
506
  node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
392
507
  }
393
508
  }
394
509
  }
395
510
  if (__KUDZU_LIST_EXPRESSIONS__) {
396
511
  for (const [marker, descriptor] of parts.expressions) {
397
- evaluate(descriptor, item).then(value => {
398
- if (revisions.get(root) === revision && root.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
512
+ evaluate(descriptor, item, index).then(value => {
513
+ if (revisions.get(root) === revision && marker.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
399
514
  }).catch(error => console.error(error))
400
515
  }
401
516
  }
402
517
  if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__) {
403
518
  for (const [node, attributes] of parts.expressionAttributes) {
404
519
  for (const [target, module, handler] of attributes) {
405
- evaluate({ module, handler }, item).then(value => {
406
- if (revisions.get(root) === revision && root.isConnected) patchBinding(node, target, value)
520
+ evaluate({ module, handler }, item, index).then(value => {
521
+ if (revisions.get(root) === revision && node.isConnected) patchBinding(node, target, value)
407
522
  }).catch(error => console.error(error))
408
523
  }
409
524
  }
410
525
  }
411
526
  if (__KUDZU_LIST_CONDITIONS__) {
412
527
  for (const [marker, descriptor] of parts.conditions) {
413
- evaluate(descriptor, item).then(value => {
414
- if (revisions.get(root) === revision && root.isConnected) updateListCondition(marker, descriptor.kind, value, item)
528
+ evaluate(descriptor, item, index).then(value => {
529
+ if (revisions.get(root) === revision && marker.isConnected) updateListCondition(marker, descriptor.kind, value, item, index)
415
530
  }).catch(error => console.error(error))
416
531
  }
417
532
  }
@@ -423,7 +538,7 @@ function listItemParts(root, nested = false) {
423
538
  parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [], effects: [] }
424
539
  for (const node of nested ? ownedElements(root).filter(node => node.matches(itemPartsSelector)) : matching(root, itemPartsSelector)) {
425
540
  if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
426
- if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
541
+ if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, node.dataset.kListAttrs ? JSON.parse(node.dataset.kListAttrs) : undefined])
427
542
  if (__KUDZU_LIST_EVENTS__ && node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
428
543
  if (__KUDZU_LIST_EXPRESSIONS__ && node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
429
544
  if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ && node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
@@ -434,29 +549,71 @@ function listItemParts(root, nested = false) {
434
549
  }
435
550
  if (__KUDZU_LIST_EFFECTS__ && node.hasAttribute("data-k-effects")) parts.effects.push(node)
436
551
  }
552
+ if (__KUDZU_NESTED_LISTS__) hydrateListItemParts(itemPartPlans.get(root), parts)
437
553
  itemParts.set(root, parts)
438
554
  return parts
439
555
  }
440
556
 
557
+ function hydrateListItemParts(planned, initial) {
558
+ if (!planned) return
559
+ hydrateListItemPartValues(planned.directTexts, initial.directTexts, value => value === "")
560
+ if (__KUDZU_LIST_TEXT_RANGES__) hydrateListItemPartValues(planned.texts, initial.texts, value => value === "")
561
+ if (__KUDZU_LIST_ATTRIBUTES__) hydrateListItemPartValues(planned.attributes, initial.attributes, value => value === undefined)
562
+ }
563
+
564
+ function hydrateListItemPartValues(planned, initial, missing) {
565
+ let index = 0
566
+ for (const entry of initial) {
567
+ if (!missing(entry[1])) continue
568
+ if (!planned[index]) throw new Error("Keyed list item markers do not match its template")
569
+ entry[1] = planned[index++][1]
570
+ }
571
+ if (index !== planned.length) throw new Error("Keyed list item markers do not match its template")
572
+ }
573
+
441
574
  function listItemPartPlan(template, nested = false) {
442
575
  const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
443
576
  const indexes = new Map(source.map((node, index) => [node, index]))
444
577
  const parts = listItemParts(template, nested)
578
+ const structural = !nested && !parts.texts.length && !parts.expressions.length && !parts.expressionAttributes.length && !parts.conditions.length && !parts.effects.length
579
+ const location = structural ? node => elementPath(template, node) : node => indexes.get(node)
445
580
  return {
446
- directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
447
- texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [indexes.get(node), field]) : [],
448
- attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
449
- events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
450
- expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
451
- expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
452
- conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor, node]) : [],
453
- effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(node => indexes.get(node)) : []
581
+ structural,
582
+ directFill: structural && !parts.events.length && !__KUDZU_LIST_ASYNC_PARTS__ && !__KUDZU_NESTED_LISTS__ && !__KUDZU_LIST_INDEXES__ && !__KUDZU_LIST_ROW_HOOKS__,
583
+ directTexts: parts.directTexts.map(([node, field]) => [location(node), field]),
584
+ texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [location(node), field]) : [],
585
+ attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([node, attributes]) => [location(node), attributes]) : [],
586
+ events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [location(node), events]) : [],
587
+ expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [location(node), descriptor]) : [],
588
+ expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [location(node), attributes]) : [],
589
+ conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [location(node), descriptor, node]) : [],
590
+ effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(location) : []
454
591
  }
455
592
  }
456
593
 
457
594
  function mapListItemParts(parts, root, nested = false) {
595
+ if (parts.structural) {
596
+ const target = path => {
597
+ if (!path.length) return root
598
+ let node = root.children[path[0]]
599
+ for (let index = 1; index < path.length; index++) node = node.children[path[index]]
600
+ return node
601
+ }
602
+ const mapped = {
603
+ directTexts: parts.directTexts.map(([path, field]) => [target(path), field]),
604
+ texts: [],
605
+ attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([path, attributes]) => [target(path), attributes]) : [],
606
+ events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([path, events]) => [target(path), events]) : [],
607
+ expressions: [],
608
+ expressionAttributes: [],
609
+ conditions: [],
610
+ effects: []
611
+ }
612
+ itemParts.set(root, mapped)
613
+ return mapped
614
+ }
458
615
  const target = nested ? ownedElements(root) : [root, ...root.querySelectorAll("*")]
459
- itemParts.set(root, {
616
+ const mapped = {
460
617
  directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
461
618
  texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([index, field]) => [target[index], field]) : [],
462
619
  attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([index, attributes]) => [target[index], attributes]) : [],
@@ -468,19 +625,73 @@ function mapListItemParts(parts, root, nested = false) {
468
625
  return [target[index], descriptor]
469
626
  }) : [],
470
627
  effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(index => target[index]) : []
471
- })
628
+ }
629
+ itemParts.set(root, mapped)
630
+ return mapped
631
+ }
632
+
633
+ function fillStructuralListParts(parts, root, item) {
634
+ for (let partIndex = 0; partIndex < parts.directTexts.length; partIndex++) {
635
+ const part = parts.directTexts[partIndex]
636
+ const path = part[0]
637
+ let node = root
638
+ for (let pathIndex = 0; pathIndex < path.length; pathIndex++) node = node.children[path[pathIndex]]
639
+ node.textContent = item[part[1]] ?? ""
640
+ }
641
+ if (__KUDZU_LIST_ATTRIBUTES__) for (let partIndex = 0; partIndex < parts.attributes.length; partIndex++) {
642
+ const part = parts.attributes[partIndex]
643
+ const path = part[0]
644
+ let node = root
645
+ for (let pathIndex = 0; pathIndex < path.length; pathIndex++) node = node.children[path[pathIndex]]
646
+ const attributes = part[1]
647
+ for (let index = 0; index < attributes.length; index++) patchBinding(node, attributes[index][0], item[attributes[index][1]])
648
+ }
649
+ }
650
+
651
+ function elementPath(root, node) {
652
+ const path = []
653
+ while (node !== root) {
654
+ const parent = node.parentElement
655
+ path.push(Array.prototype.indexOf.call(parent.children, node))
656
+ node = parent
657
+ }
658
+ return path.reverse()
472
659
  }
473
660
 
474
661
  function mapListConditionTemplates(planned, initial) {
475
- if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
476
- for (let index = 0; index < initial.length; index++) {
477
- const marker = initial[index][0]
478
- initial[index][1] ??= planned[index][1]
479
- if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
662
+ if (!__KUDZU_DEEP_LIST_CONDITIONS__) {
663
+ if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
664
+ for (let index = 0; index < initial.length; index++) {
665
+ const marker = initial[index][0]
666
+ initial[index][1] ??= planned[index][1]
667
+ if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
668
+ }
669
+ return
670
+ }
671
+ const templates = new Map()
672
+ const collect = root => {
673
+ for (const marker of root.querySelectorAll("template")) {
674
+ if (marker.hasAttribute("data-k-list-condition")) {
675
+ const descriptor = JSON.parse(marker.dataset.kListCondition)
676
+ templates.set(descriptor.handler, [descriptor, marker])
677
+ }
678
+ collect(marker.content)
679
+ }
680
+ }
681
+ for (const entry of planned) {
682
+ templates.set(entry[1].handler, [entry[1], entry[2]])
683
+ collect(entry[2].content)
684
+ }
685
+ for (const entry of initial) {
686
+ const marker = entry[0]
687
+ const template = templates.get(entry[1]?.handler ?? marker.dataset.kListConditionHandler)
688
+ if (!template) throw new Error("Keyed list condition marker has no matching template")
689
+ entry[1] ??= template[0]
690
+ if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, template[1])
480
691
  }
481
692
  }
482
693
 
483
- function updateListCondition(marker, kind, value, item) {
694
+ function updateListCondition(marker, kind, value, item, index) {
484
695
  const current = listConditionKey(kind, value)
485
696
  if (marker.dataset.kListCurrent === current) return
486
697
  let end = marker.nextSibling
@@ -503,7 +714,7 @@ function updateListCondition(marker, kind, value, item) {
503
714
  const nodes = [...fragment.childNodes]
504
715
  const revision = (revisions.get(marker) ?? 0) + 1
505
716
  revisions.set(marker, revision)
506
- fillListParts(marker, listItemParts(fragment), item, revision)
717
+ fillListParts(marker, listItemParts(fragment), item, revision, index)
507
718
  end.parentNode.insertBefore(fragment, end)
508
719
  marker.dataset.kListCurrent = current
509
720
  const owner = conditionOwners.get(marker)
@@ -519,19 +730,20 @@ function renderFalsy(value) {
519
730
  return value === false || value == null || value === true ? "" : String(value)
520
731
  }
521
732
 
522
- function validateChildLists(items, child) {
733
+ function validateChildLists(items, children) {
523
734
  for (const item of items) {
524
- const children = item?.[child.field]
525
- if (!Array.isArray(children)) throw new Error(`Nested keyed list property "${child.field}" must remain an array`)
526
- const keys = new Set()
527
- for (const entry of children) {
528
- const key = entry?.[child.key]
529
- if (!validListKey(key)) throw new Error(`Keyed list key "${child.key}" must be a string or finite number`)
530
- assertListItem(entry)
531
- assertListValue(entry, new Set(), true)
532
- const token = keyToken(key)
533
- if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
534
- keys.add(token)
735
+ for (const child of children) {
736
+ const values = __KUDZU_COLLECTION_SELECTORS__ ? selectCollection(item?.[child.field], child.selector) : item?.[child.field] ?? []
737
+ const keys = new Set()
738
+ for (const entry of values) {
739
+ const key = child.key === null ? keys.size : entry?.[child.key]
740
+ if (!validListKey(key)) throw new Error(`Keyed list key "${child.key}" must be a string or finite number`)
741
+ assertListItem(entry)
742
+ assertListValue(entry, new Set(), true)
743
+ const token = keyToken(key)
744
+ if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
745
+ keys.add(token)
746
+ }
535
747
  }
536
748
  }
537
749
  }
@@ -544,24 +756,24 @@ function listOwner(start) {
544
756
 
545
757
  function mountNestedPrototype(start, descriptor, roots) {
546
758
  const owner = descriptor.ownerField ? listOwner(start) : undefined
547
- const prototypeStart = owner ? childPrototypes.get(owner) : undefined
759
+ const prototypeStart = owner ? childPrototypes.get(owner)?.get(descriptor.id) : undefined
548
760
  const templateRoot = prototypeStart?.content.firstElementChild ?? start.content.firstElementChild
549
761
  if (!templateRoot) throw new Error("Nested keyed list has no shared row prototype")
550
- const childPrototype = descriptor.child ? findChildPrototype(templateRoot, descriptor.child.field) : undefined
551
- if (descriptor.child && !childPrototype) throw new Error("Keyed list template has no nested row prototype")
552
- if (childPrototype) for (const root of roots) childPrototypes.set(root, childPrototype)
762
+ const prototypes = descriptor.children && new Map(descriptor.children.map(child => [child.id, findChildPrototype(templateRoot, child.id)]))
763
+ if (prototypes && [...prototypes.values()].some(prototype => !prototype)) throw new Error("Keyed list template has no nested row prototype")
764
+ if (prototypes?.size) for (const root of roots) childPrototypes.set(root, prototypes)
553
765
  if (prototypeStart && start !== prototypeStart) start.content.replaceChildren()
554
- return { owner, childPrototype, templateRoot }
766
+ return { owner, childPrototypes: prototypes, templateRoot }
555
767
  }
556
768
 
557
- function findChildPrototype(root, field) {
769
+ function findChildPrototype(root, id) {
558
770
  for (const element of root.children) {
559
771
  if (element.matches("template[data-k-list]")) {
560
- if (JSON.parse(element.dataset.kList).ownerField === field) return element
561
- const nested = findChildPrototype(element.content, field)
772
+ if (JSON.parse(element.dataset.kList).id === id) return element
773
+ const nested = findChildPrototype(element.content, id)
562
774
  if (nested) return nested
563
775
  } else {
564
- const nested = findChildPrototype(element, field)
776
+ const nested = findChildPrototype(element, id)
565
777
  if (nested) return nested
566
778
  }
567
779
  }
@@ -633,14 +845,14 @@ function patchListText(marker, endSelector, value) {
633
845
  end.before(marker.ownerDocument.createTextNode(text))
634
846
  }
635
847
 
636
- function evaluate(descriptor, item) {
848
+ function evaluate(descriptor, item, index) {
637
849
  let module = imports.get(descriptor.module)
638
850
  if (!module) {
639
851
  module = import(descriptor.module)
640
852
  imports.set(descriptor.module, module)
641
853
  }
642
854
  return module.then(exports => {
643
- const value = exports[descriptor.handler](item)
855
+ const value = exports[descriptor.handler](item, index)
644
856
  if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
645
857
  return value
646
858
  })
@@ -668,7 +880,8 @@ function patchBinding(node, target, value) {
668
880
  } else if (value == null || (value === false && !isStringBooleanAttribute(target))) {
669
881
  node.removeAttribute(target)
670
882
  } else {
671
- node.setAttribute(target, value === true && !isStringBooleanAttribute(target) ? "" : String(value))
883
+ const next = value === true && !isStringBooleanAttribute(target) ? "" : String(value)
884
+ if (node.getAttribute(target) !== next) node.setAttribute(target, next)
672
885
  }
673
886
  }
674
887
 
@@ -676,27 +889,56 @@ function keyToken(key) {
676
889
  return `${typeof key}:${key}`
677
890
  }
678
891
 
892
+ /* general-row-hooks */
893
+ function initializeGeneralRowHooks(descriptor, key, root, owner) {
894
+ const token = keyToken(key)
895
+ const path = [...(ownershipPaths.get(owner) ?? []), `${descriptor.id}=${token}`]
896
+ ownershipPaths.set(root, path)
897
+ const statePath = descriptor.ownerField ? path : [token]
898
+ root.dataset.kRowPath = encodeURIComponent(statePath.join("/"))
899
+ const replacements = new Map()
900
+ for (const state of descriptor.rowStates ?? []) {
901
+ const id = rowStateId(state.id, statePath)
902
+ if (!browserState.has(id)) browserState.set(id, __KUDZU_COMPLEX_LIST_ROW_STATE__ && state.initialValue !== null && typeof state.initialValue === "object" ? structuredClone(state.initialValue) : state.initialValue)
903
+ replacements.set(state.id, id)
904
+ }
905
+ if (__KUDZU_LIST_ROW_REFS__) for (const ref of descriptor.rowRefs ?? []) replacements.set(ref, rowStateId(ref, statePath))
906
+ for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, statePath))
907
+ rowReplacements.set(root, replacements)
908
+ replaceRowIds(root, replacements)
909
+ }
910
+ /* general-row-hooks-end */
911
+
679
912
  function initializeRowStates(descriptor, key, root) {
680
913
  const token = keyToken(key)
681
914
  const replacements = new Map()
682
915
  for (const state of descriptor.rowStates) {
683
- const id = rowStateId(state.id, token)
916
+ const id = flatRowStateId(state.id, token)
684
917
  if (!browserState.has(id)) browserState.set(id, state.initialValue)
685
918
  replacements.set(state.id, id)
686
919
  }
687
- for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, token))
920
+ for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, flatRowStateId(marker, token))
688
921
  if (root) replaceRowIds(root, replacements)
689
922
  }
690
923
 
691
- function deleteRowStates(descriptor, token) {
692
- for (const state of descriptor.rowStates) browserState.delete(rowStateId(state.id, token))
924
+ function deleteFlatRowStates(descriptor, token) {
925
+ for (const state of descriptor.rowStates) browserState.delete(flatRowStateId(state.id, token))
693
926
  }
694
927
 
695
- function rowStateId(id, token) {
928
+ function flatRowStateId(id, token) {
696
929
  return id.replace("$k", encodeURIComponent(token))
697
930
  }
698
931
 
932
+ function deleteRowStates(descriptor, path) {
933
+ const statePath = descriptor.ownerField ? path : [path.at(-1).slice(path.at(-1).indexOf("=") + 1)]
934
+ for (const state of descriptor.rowStates) browserState.delete(rowStateId(state.id, statePath))
935
+ }
936
+ function rowStateId(id, path) {
937
+ return id.replace("$k", encodeURIComponent(path.join("/")))
938
+ }
939
+
699
940
  function replaceRowIds(root, replacements) {
941
+ if (!replacements) return
700
942
  const replace = node => {
701
943
  for (const attribute of [...node.attributes]) {
702
944
  if (!attribute.name.startsWith("data-k-")) continue
@@ -719,7 +961,7 @@ function assertListItem(item) {
719
961
  if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
720
962
  }
721
963
 
722
- function seededListValue(item, fields, seed) {
964
+ function seededListValue(item, fields, seed, serialize = true) {
723
965
  const keys = Reflect.ownKeys(item)
724
966
  if (fields.length === 1) {
725
967
  const field = fields[0]
@@ -729,10 +971,10 @@ function seededListValue(item, fields, seed) {
729
971
  if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
730
972
  const value = descriptor.value
731
973
  const type = value === null ? "null" : typeof value
732
- return type === seed[field] && !(type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) ? value : undefined
974
+ return type === seed[field] && !(type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) ? serialize ? value : true : undefined
733
975
  }
734
976
  if (keys.length !== fields.length || keys.some(key => typeof key !== "string" || !fields.includes(key))) return undefined
735
- const values = []
977
+ const values = serialize ? [] : undefined
736
978
  for (const field of fields) {
737
979
  const descriptor = Object.getOwnPropertyDescriptor(item, field)
738
980
  if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
@@ -740,9 +982,13 @@ function seededListValue(item, fields, seed) {
740
982
  const value = descriptor.value
741
983
  const type = value === null ? "null" : typeof value
742
984
  if (type !== seed[field] || type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) return undefined
743
- values.push(value)
985
+ if (serialize) values.push(value)
744
986
  }
745
- return values.length === 1 ? values[0] : JSON.stringify(values)
987
+ return serialize ? values.length === 1 ? values[0] : JSON.stringify(values) : true
988
+ }
989
+
990
+ function usesItemReferences(list) {
991
+ return !list.descriptor.mount && !list.descriptor.effects && !list.descriptor.events && !list.descriptor.expressions && !list.descriptor.expressionAttributes && !list.descriptor.conditions
746
992
  }
747
993
 
748
994
  function assertListValue(value, seen, root = false) {