@abide/abide 0.40.1 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +24 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/lib/ui/compile/destructureBindingNames.ts +38 -0
- package/src/lib/ui/compile/generateBuild.ts +85 -8
- package/src/lib/ui/compile/generateSSR.ts +9 -1
- package/src/lib/ui/compile/lowerContext.ts +29 -9
- package/src/lib/ui/compile/parseTemplate.ts +2 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +19 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +2 -0
- package/src/lib/ui/dom/awaitBlock.ts +69 -15
- package/src/lib/ui/dom/each.ts +28 -8
- package/src/lib/ui/dom/eachAsync.ts +24 -11
- package/src/lib/ui/dom/types/EachRow.ts +9 -2
package/AGENTS.md
CHANGED
|
@@ -58,7 +58,8 @@ Aliases: `$server` → `src/server/`, `$ui` → `src/ui/`, `$shared` → `src/sh
|
|
|
58
58
|
| `abide run <file> [args]` | Run a script under the abide preload (jobs, tests) |
|
|
59
59
|
| `abide compile` | Build a standalone server executable with embedded assets |
|
|
60
60
|
| `abide cli` | Build a thin CLI binary that ships the server (`--platforms` cross-compiles) |
|
|
61
|
-
| `abide bundle` | Build a movable, self-contained desktop app bundle for this platform |
|
|
61
|
+
| `abide bundle` | Build a movable, self-contained desktop app bundle for this platform (unsigned `.app` on macOS, flat dir elsewhere) |
|
|
62
|
+
| `abide lsp` | Run the `.abide` language server over stdio (JSON-RPC editor diagnostics) |
|
|
62
63
|
| `abide init-agent` | Write/refresh a `CLAUDE.md` pointer to this surface map |
|
|
63
64
|
|
|
64
65
|
Tests preload the framework: `preload = ["@abide/abide/preload"]` in `bunfig.toml`
|
|
@@ -158,7 +159,7 @@ Control flow — native `<template>`:
|
|
|
158
159
|
| Directive | Shape |
|
|
159
160
|
| --- | --- |
|
|
160
161
|
| `<template if={c}>` … `<template elseif={c2}>` … `<template else>` | conditional chain (`else` must be last) |
|
|
161
|
-
| `<template each={list} as="x" key="x.id">` | keyed list |
|
|
162
|
+
| `<template each={list} as="x" key="x.id" index="i">` | keyed list (`index` binds the row's reactive position) |
|
|
162
163
|
| `<template await={p}>` `<template then="v">` `<template catch="e">` `<template finally>` | promise (streams; branch value bound by `then`/`catch`) |
|
|
163
164
|
| `<template switch={s}>` `<template case={v}>` … `<template default>` | first strict-`===` match |
|
|
164
165
|
| `<template try>` `<template catch="e">` `<template finally>` | synchronous error boundary |
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`ba08127`](https://github.com/briancray/abide/commit/ba081270c4721d64655457681e4ebcc79681f1d4) - `index="i"` on `<template each>` — bind the row's reactive position
|
|
8
|
+
|
|
9
|
+
`<template each={list} as="item" key="item.id" index="i">` binds the iteration index to a name. In a keyed `each` the index is reactive: a reorder/insert/remove that shifts a surviving row repaints its `{i}` in place (same DOM, no rebuild), riding the same `Object.is` cell-write path as the item binding. SSR renders the index via `entries()` so hydration stays congruent; async `each await` carries the stream arrival ordinal.
|
|
10
|
+
|
|
11
|
+
- [`ba08127`](https://github.com/briancray/abide/commit/ba081270c4721d64655457681e4ebcc79681f1d4) - In-place reactive updates for `<template await>` `then` values and keyed `each` items
|
|
12
|
+
|
|
13
|
+
A re-settling `await` block and a re-keyed `each` row no longer rebuild their subtree — they update through a reactive value cell instead, so a live cache patch updates only what changed and never flashes the surrounding DOM.
|
|
14
|
+
|
|
15
|
+
- `awaitBlock` keeps the mounted `then`-branch across a re-run, setting a reactive value cell rather than detaching + rebuilding. A revalidation now keeps the stale branch visible and patches in place; the branch is rebuilt only across a pending/catch ↔ then kind change.
|
|
16
|
+
- The `then` binding is lowered to read that cell reactively — both a single identifier (`then="value"`) and a destructure (`then="[a, b]"` / `{ x, y }`), where each leaf is derived per-read so only the leaves whose value changed propagate.
|
|
17
|
+
- Keyed `each` holds each row's item in a reactive cell; a re-key with a changed value (same key, new object) writes the cell through `Object.is`, re-running only that row's effects with no DOM rebuild. The row `render` now receives `State<T>` (the compiler binds the `as` name to read it).
|
|
18
|
+
- A destructuring `each … as="[a, b]"` (or `{ x, y }`) with no explicit `key` now defaults the key to the row's raw item identity, like a plain `as`. Previously the default key re-emitted the destructure pattern, allocating a fresh array/object per reconcile, so keys never matched and every row rebuilt on any list change.
|
|
19
|
+
- A block value binding (`then="x"` / `each … as="x"`) now correctly shadows a same-named component `state`/`computed`/`derived`: it is a nearer lexical scope, so `{x}` in the block reads the resolved/row cell rather than the component signal. A destructure pattern's default/computed-key initializer is also lowered, so `then="{ id, label = fallback }"` resolves `fallback` against the component scope instead of emitting it raw.
|
|
20
|
+
|
|
21
|
+
## 0.40.2
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [`315bc88`](https://github.com/briancray/abide/commit/315bc8859e03a81c845f9919a759b6b1f8fb0ea1) - document the abide lsp command and add an if/else template example ([`98b7555`](https://github.com/briancray/abide/commit/98b755509e4788712b7e0516a1cb05f04355a75b))
|
|
26
|
+
|
|
3
27
|
## 0.40.1
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -133,6 +133,11 @@ async function send() {
|
|
|
133
133
|
<button disabled={!text} class="border px-3">send</button>
|
|
134
134
|
</form>
|
|
135
135
|
|
|
136
|
+
<template if={pinned}>
|
|
137
|
+
<p class="text-xs text-amber-700">room pinned</p>
|
|
138
|
+
<template else><p class="text-xs text-slate-400">not pinned</p></template>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
136
141
|
<template switch={sort}>
|
|
137
142
|
<template case={'newest'}><p class="text-xs">newest first</p></template>
|
|
138
143
|
<template default><p class="text-xs">oldest first</p></template>
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The leaf binding names a destructuring pattern introduces, in source order —
|
|
5
|
+
`[a, b]` → `a, b`; `{ x, y: z }` → `x, z`; `[a, ...rest]` → `a, rest`; nested
|
|
6
|
+
patterns flatten. Used to re-bind an `await` `then` destructure as per-leaf
|
|
7
|
+
reactive reads of the resolved-value cell, so a re-settle updates each leaf in
|
|
8
|
+
place instead of rebuilding the branch.
|
|
9
|
+
*/
|
|
10
|
+
export function destructureBindingNames(pattern: string): string[] {
|
|
11
|
+
const source = ts.createSourceFile(
|
|
12
|
+
'pattern.ts',
|
|
13
|
+
`const ${pattern} = $;`,
|
|
14
|
+
ts.ScriptTarget.Latest,
|
|
15
|
+
true,
|
|
16
|
+
)
|
|
17
|
+
const names: string[] = []
|
|
18
|
+
const collect = (name: ts.BindingName): void => {
|
|
19
|
+
if (ts.isIdentifier(name)) {
|
|
20
|
+
names.push(name.text)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
// Object/array pattern: each element binds; array holes are OmittedExpression, not BindingElement.
|
|
24
|
+
for (const element of name.elements) {
|
|
25
|
+
if (ts.isBindingElement(element)) {
|
|
26
|
+
collect(element.name)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const statement of source.statements) {
|
|
31
|
+
if (ts.isVariableStatement(statement)) {
|
|
32
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
33
|
+
collect(declaration.name)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return names
|
|
38
|
+
}
|
|
@@ -4,6 +4,7 @@ import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
|
4
4
|
import { asOutlet } from './asOutlet.ts'
|
|
5
5
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
6
6
|
import { composeProps } from './composeProps.ts'
|
|
7
|
+
import { destructureBindingNames } from './destructureBindingNames.ts'
|
|
7
8
|
import { groupBindParts } from './groupBindParts.ts'
|
|
8
9
|
import { isControlFlow } from './isControlFlow.ts'
|
|
9
10
|
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
@@ -85,10 +86,50 @@ export function generateBuild(
|
|
|
85
86
|
expression: lowerExpression,
|
|
86
87
|
statement: lowerStatement,
|
|
87
88
|
withNestedScripts,
|
|
89
|
+
withLocalDerived,
|
|
88
90
|
bindRead,
|
|
89
91
|
bindWrite,
|
|
90
92
|
} = lowerContext(stateNames, derivedNames, computedNames)
|
|
91
93
|
|
|
94
|
+
/* A value binding the runtime can update in place (an `await` `then` value, a keyed `each`
|
|
95
|
+
item) is passed as a reactive `.value` cell; the consuming branch/row reads its NAME(s)
|
|
96
|
+
as a deref so it re-runs in place when a re-settle/re-key sets the cell. */
|
|
97
|
+
const isPlainIdentifier = (name: string | undefined): name is string =>
|
|
98
|
+
name !== undefined && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)
|
|
99
|
+
|
|
100
|
+
/* How a reactive value param binds: `param` is the thunk's value parameter (the cell),
|
|
101
|
+
`prefix` declares any per-leaf readers, `localNames` enter the deref scope for the body.
|
|
102
|
+
A plain identifier reads the cell directly (`item` → `item.value`); a destructure
|
|
103
|
+
re-applies over the cell per read so each leaf stays reactive (JS handles
|
|
104
|
+
defaults/rest/rename/nesting). The caller lowers the body with `localNames` derived. */
|
|
105
|
+
function reactiveBinding(authorParam: string): {
|
|
106
|
+
param: string
|
|
107
|
+
prefix: string
|
|
108
|
+
localNames: string[]
|
|
109
|
+
} {
|
|
110
|
+
if (isPlainIdentifier(authorParam)) {
|
|
111
|
+
return { param: authorParam, prefix: '', localNames: [authorParam] }
|
|
112
|
+
}
|
|
113
|
+
const cellParam = nextVar('aw')
|
|
114
|
+
const deriveVar = nextVar('ad')
|
|
115
|
+
const leaves = destructureBindingNames(authorParam)
|
|
116
|
+
/* Lower the destructure declaration so a default/computed-key initializer that
|
|
117
|
+
references a component signal (`{ label = fallback }`, `{ [key]: v }`) is rewritten
|
|
118
|
+
to its `model`/cell form — the bound leaf names are name-slots and stay untouched.
|
|
119
|
+
A pattern with no such initializer lowers to itself, so the common case is unchanged
|
|
120
|
+
and SSR (which lowers the same declaration) stays congruent. */
|
|
121
|
+
const declaration = lowerStatement(`const ${authorParam} = ${cellParam}.value`)
|
|
122
|
+
const prefix =
|
|
123
|
+
`const ${deriveVar} = { get value() { ${declaration} return { ${leaves.join(', ')} }; } };\n` +
|
|
124
|
+
leaves
|
|
125
|
+
.map(
|
|
126
|
+
(leaf) =>
|
|
127
|
+
`const ${leaf} = { get value() { return ${deriveVar}.value.${leaf}; } };\n`,
|
|
128
|
+
)
|
|
129
|
+
.join('')
|
|
130
|
+
return { param: cellParam, prefix, localNames: leaves }
|
|
131
|
+
}
|
|
132
|
+
|
|
92
133
|
/* Emits the wiring for one non-static attribute against an already-obtained skeleton
|
|
93
134
|
element var — reactive `attr`, `on` listener, `attach`, or a two-way `bind`. */
|
|
94
135
|
function dynamicAttr(
|
|
@@ -498,13 +539,21 @@ export function generateBuild(
|
|
|
498
539
|
const pending = node.blocking
|
|
499
540
|
? []
|
|
500
541
|
: node.children.filter((child) => child.kind !== 'branch')
|
|
542
|
+
/* The resolved value is reactive: a re-settle updates it in place rather than
|
|
543
|
+
rebuilding the branch (see awaitBlock). The branch reads it as a `.value` cell. */
|
|
501
544
|
const thenThunk = node.blocking
|
|
502
545
|
? branchThunk(
|
|
503
546
|
node.children.filter((child) => child.kind !== 'branch'),
|
|
504
547
|
node.as ?? '_value',
|
|
505
548
|
finallyChildren,
|
|
549
|
+
true,
|
|
550
|
+
)
|
|
551
|
+
: branchThunk(
|
|
552
|
+
thenBranch?.children ?? [],
|
|
553
|
+
thenBranch?.as ?? '_value',
|
|
554
|
+
finallyChildren,
|
|
555
|
+
true,
|
|
506
556
|
)
|
|
507
|
-
: branchThunk(thenBranch?.children ?? [], thenBranch?.as ?? '_value', finallyChildren)
|
|
508
557
|
/* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
|
|
509
558
|
rejection (surfacing it) instead of rendering an empty branch. A finally-only
|
|
510
559
|
block keeps a catch thunk that renders just finally. */
|
|
@@ -536,18 +585,29 @@ export function generateBuild(
|
|
|
536
585
|
children: TemplateNode[],
|
|
537
586
|
valueParam?: string,
|
|
538
587
|
finallyChildren: TemplateNode[] = [],
|
|
588
|
+
reactiveValue = false,
|
|
539
589
|
): string {
|
|
540
590
|
const parentParam = nextVar('p')
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
591
|
+
/* A reactive value (an `await` `then`) arrives as a `.value` cell the runtime can set
|
|
592
|
+
in place, so the branch re-runs in place on a re-settle instead of being rebuilt. */
|
|
593
|
+
const binding =
|
|
594
|
+
reactiveValue && valueParam !== undefined ? reactiveBinding(valueParam) : undefined
|
|
595
|
+
const param = binding?.param ?? valueParam
|
|
596
|
+
const prefix = binding?.prefix ?? ''
|
|
597
|
+
const localNames = binding?.localNames ?? []
|
|
598
|
+
const head = param === undefined ? `(${parentParam})` : `(${parentParam}, ${param})`
|
|
599
|
+
const body = withNestedScripts(children, () =>
|
|
600
|
+
localNames.length === 0
|
|
601
|
+
? generateChildren(children, parentParam)
|
|
602
|
+
: withLocalDerived(localNames, () => generateChildren(children, parentParam)),
|
|
603
|
+
)
|
|
544
604
|
const finallyBody =
|
|
545
605
|
finallyChildren.length > 0
|
|
546
606
|
? withNestedScripts(finallyChildren, () =>
|
|
547
607
|
generateChildren(finallyChildren, parentParam),
|
|
548
608
|
)
|
|
549
609
|
: ''
|
|
550
|
-
return `${head} => {\n${body}${finallyBody}}`
|
|
610
|
+
return `${head} => {\n${prefix}${body}${finallyBody}}`
|
|
551
611
|
}
|
|
552
612
|
|
|
553
613
|
/* True when a branch has content worth a render thunk — vs an absent/empty branch
|
|
@@ -626,13 +686,30 @@ export function generateBuild(
|
|
|
626
686
|
before: string,
|
|
627
687
|
): string {
|
|
628
688
|
const rowParam = nextVar('p')
|
|
689
|
+
/* The item is a reactive `.value` cell so a re-key with a changed value updates the row
|
|
690
|
+
in place (no rebuild). `keyOf` receives the RAW item; the key expression is lowered
|
|
691
|
+
with the author name plain — derive it BEFORE the row body puts that name in the
|
|
692
|
+
deref scope. With no explicit `key`, default the key to the item's own identity: a
|
|
693
|
+
plain `as` returns its name; a destructuring `as` binds a fresh param and returns
|
|
694
|
+
THAT, not the pattern re-wrapped (`[i,crumb]` → `[i,crumb]` would allocate a fresh
|
|
695
|
+
array per reconcile, so keys never match and every row rebuilds). An explicit `key`
|
|
696
|
+
destructures the item via `node.as` to read its leaves. */
|
|
697
|
+
const rawItemParam = isPlainIdentifier(node.as) ? node.as : nextVar('k')
|
|
698
|
+
const keyParam = node.key === undefined ? rawItemParam : node.as
|
|
699
|
+
const keyExpression = node.key === undefined ? rawItemParam : lowerExpression(node.key)
|
|
700
|
+
const binding = reactiveBinding(node.as)
|
|
701
|
+
/* `index="i"` binds the row's position as a third reactive cell param (the runtime
|
|
702
|
+
always passes it). It is a plain identifier — read as `i.value` — so it enters the
|
|
703
|
+
body's deref scope alongside the item's leaf names; an unnamed param when absent. */
|
|
704
|
+
const indexParam = node.index === undefined ? '' : `, ${node.index}`
|
|
705
|
+
const bodyLocalNames =
|
|
706
|
+
node.index === undefined ? binding.localNames : [...binding.localNames, node.index]
|
|
629
707
|
/* The row body builds its children (a `<script>` declares per-row local signals,
|
|
630
708
|
emitted in document order) into the row parent. A `<template catch>` child is
|
|
631
709
|
consumed by the async-each, not the row — `generateChildren` skips it. */
|
|
632
710
|
const rowBody = withNestedScripts(node.children, () =>
|
|
633
|
-
generateChildren(node.children, rowParam),
|
|
711
|
+
withLocalDerived(bodyLocalNames, () => generateChildren(node.children, rowParam)),
|
|
634
712
|
)
|
|
635
|
-
const keyExpression = node.key === undefined ? node.as : lowerExpression(node.key)
|
|
636
713
|
/* `await` → the AsyncIterable runtime, drained row-by-row on the client, with an
|
|
637
714
|
optional `<template catch>` branch rendered (after the streamed rows) when the
|
|
638
715
|
iterator rejects. Absent → `undefined`, so the rejection surfaces instead. */
|
|
@@ -643,7 +720,7 @@ export function generateBuild(
|
|
|
643
720
|
: ''
|
|
644
721
|
return (
|
|
645
722
|
`${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
|
|
646
|
-
`(${
|
|
723
|
+
`(${keyParam}) => (${keyExpression}), (${rowParam}, ${binding.param}${indexParam}) => {\n${binding.prefix}${rowBody}}${catchArg}, ${before});\n`
|
|
647
724
|
)
|
|
648
725
|
}
|
|
649
726
|
|
|
@@ -269,7 +269,15 @@ export function generateSSR(
|
|
|
269
269
|
if (node.async) {
|
|
270
270
|
return anchor
|
|
271
271
|
}
|
|
272
|
-
|
|
272
|
+
const rowBody = `${openRange(target)}${branchContent(node.children, target)}${closeRange(target)}`
|
|
273
|
+
/* `index="i"` binds the row position. SSR reads it as a plain number from
|
|
274
|
+
`entries()` over a materialized array; the client reads the same number from a
|
|
275
|
+
cell, so first paint is congruent. No index → a plain `for…of` over the items. */
|
|
276
|
+
const header =
|
|
277
|
+
node.index === undefined
|
|
278
|
+
? `for (const ${node.as} of (${lowerExpression(node.items)}))`
|
|
279
|
+
: `for (const [${node.index}, ${node.as}] of [...(${lowerExpression(node.items)})].entries())`
|
|
280
|
+
return `${anchor}${header} {\n${rowBody}}\n`
|
|
273
281
|
}
|
|
274
282
|
if (node.kind === 'await') {
|
|
275
283
|
return `${anchor}${generateAwait(node, target)}`
|
|
@@ -22,20 +22,21 @@ export function lowerContext(
|
|
|
22
22
|
derivedNames: ReadonlySet<string>,
|
|
23
23
|
computedNames: ReadonlySet<string> = new Set(),
|
|
24
24
|
) {
|
|
25
|
-
/* Branch-scoped signal bindings (from nested `<script>`s
|
|
26
|
-
`.value` like a `computed
|
|
27
|
-
|
|
25
|
+
/* Branch-scoped signal bindings (from nested `<script>`s, and the block value params
|
|
26
|
+
pushed by `withLocalDerived`) — they deref to `.value` like a `computed`, and as a
|
|
27
|
+
nearer lexical scope they SHADOW a same-named component signal. Pushed while a
|
|
28
|
+
branch's script + markup compile, popped after, so they shadow only within that
|
|
29
|
+
subtree. */
|
|
28
30
|
const localDerived = new Set<string>()
|
|
29
|
-
const derefScope = (): ReadonlySet<string> =>
|
|
30
|
-
localDerived.size === 0 ? derivedNames : new Set([...derivedNames, ...localDerived])
|
|
31
31
|
|
|
32
32
|
/* Parse `code` once and chain the reference rename and doc-access lowering over the
|
|
33
|
-
one tree — the two string passes would each parse + reprint. `
|
|
34
|
-
per call
|
|
33
|
+
one tree — the two string passes would each parse + reprint. `localDerived` is
|
|
34
|
+
snapshotted per call (as the transformer's block-local shadow set) so a binding
|
|
35
|
+
pushed mid-compile is honoured AND shadows a same-named component signal. */
|
|
35
36
|
function lowerOnce(code: string): string {
|
|
36
37
|
const source = ts.createSourceFile('expr.ts', code, ts.ScriptTarget.Latest, true)
|
|
37
38
|
const result = ts.transform(source, [
|
|
38
|
-
signalRefsTransformer(stateNames,
|
|
39
|
+
signalRefsTransformer(stateNames, derivedNames, computedNames, new Set(localDerived)),
|
|
39
40
|
docAccessTransformer('model'),
|
|
40
41
|
])
|
|
41
42
|
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile).trim()
|
|
@@ -113,5 +114,24 @@ export function lowerContext(
|
|
|
113
114
|
return result
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
/* Pushes explicit names into the deref scope for `body` then pops them — the
|
|
118
|
+
programmatic counterpart to `withNestedScripts`, used to bind a block's value param
|
|
119
|
+
(an `await` `then` value, a keyed `each` item) as a reactive `.value` cell so the
|
|
120
|
+
branch reads it reactively and re-runs in place when the block sets the cell. */
|
|
121
|
+
function withLocalDerived<T>(names: string[], body: () => T): T {
|
|
122
|
+
const added: string[] = []
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
if (!localDerived.has(name)) {
|
|
125
|
+
localDerived.add(name)
|
|
126
|
+
added.push(name)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = body()
|
|
130
|
+
for (const name of added) {
|
|
131
|
+
localDerived.delete(name)
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { expression, statement, withNestedScripts, withLocalDerived, bindRead, bindWrite }
|
|
117
137
|
}
|
|
@@ -560,11 +560,13 @@ function toControlFlow(attrs: TemplateAttr[], children: TemplateNode[]): Templat
|
|
|
560
560
|
}
|
|
561
561
|
const as = find('as')
|
|
562
562
|
const key = find('key')
|
|
563
|
+
const index = find('index')
|
|
563
564
|
return {
|
|
564
565
|
kind: 'each',
|
|
565
566
|
items: itemsCode,
|
|
566
567
|
as: (as === undefined ? undefined : attrText(as)) ?? '_item',
|
|
567
568
|
key: key === undefined ? undefined : attrText(key),
|
|
569
|
+
index: index === undefined ? undefined : attrText(index),
|
|
568
570
|
async: find('await') !== undefined, // `<template each await>` over an AsyncIterable
|
|
569
571
|
children,
|
|
570
572
|
loc: attrLoc(items),
|
|
@@ -43,10 +43,19 @@ export function signalRefsTransformer(
|
|
|
43
43
|
stateNames: ReadonlySet<string>,
|
|
44
44
|
derivedNames: ReadonlySet<string>,
|
|
45
45
|
computedNames: ReadonlySet<string> = new Set(),
|
|
46
|
+
/* Block-local signal bindings (an `await` `then` / keyed `each` value param, a nested
|
|
47
|
+
`<script>` declaration) — a nearer lexical scope than the component signals, so a
|
|
48
|
+
name here shadows a same-named `state`/`computed`/`derived` and derefs as a cell. */
|
|
49
|
+
blockLocal: ReadonlySet<string> = new Set(),
|
|
46
50
|
): ts.TransformerFactory<ts.SourceFile> {
|
|
47
51
|
/* The signal names that a nested binding can shadow — only these matter for
|
|
48
52
|
scope tracking, so we ignore every other local binding. */
|
|
49
|
-
const signalNames = new Set<string>([
|
|
53
|
+
const signalNames = new Set<string>([
|
|
54
|
+
...stateNames,
|
|
55
|
+
...derivedNames,
|
|
56
|
+
...computedNames,
|
|
57
|
+
...blockLocal,
|
|
58
|
+
])
|
|
50
59
|
return (context) => (root) => {
|
|
51
60
|
/* The identifier nodes that are names, not value reads — collected by walking
|
|
52
61
|
each parent and recording its name children (`parent.name`, a label, a
|
|
@@ -88,6 +97,7 @@ export function signalRefsTransformer(
|
|
|
88
97
|
stateNames,
|
|
89
98
|
derivedNames,
|
|
90
99
|
computedNames,
|
|
100
|
+
blockLocal,
|
|
91
101
|
)
|
|
92
102
|
if (replacement !== undefined) {
|
|
93
103
|
return ts.factory.createPropertyAssignment(node.name.text, replacement)
|
|
@@ -99,6 +109,7 @@ export function signalRefsTransformer(
|
|
|
99
109
|
stateNames,
|
|
100
110
|
derivedNames,
|
|
101
111
|
computedNames,
|
|
112
|
+
blockLocal,
|
|
102
113
|
)
|
|
103
114
|
if (replacement !== undefined) {
|
|
104
115
|
return replacement
|
|
@@ -305,13 +316,19 @@ function functionParameters(node: ts.Node): ts.NodeArray<ts.ParameterDeclaration
|
|
|
305
316
|
|
|
306
317
|
/* `model.<name>` for a state binding, `<name>()` for a computed doc-slot (the
|
|
307
318
|
string-free reader `scope().derive` returns), `<name>.value` for a runtime cell
|
|
308
|
-
(linked / lens / transform-state), else undefined.
|
|
319
|
+
(linked / lens / transform-state), else undefined. A `blockLocal` binding shadows
|
|
320
|
+
any same-named component signal — it is a nearer lexical scope — so it derefs as a
|
|
321
|
+
cell (`<name>.value`) regardless of a colliding `state`/`computed`/`derived`. */
|
|
309
322
|
function referenceFor(
|
|
310
323
|
name: string,
|
|
311
324
|
stateNames: ReadonlySet<string>,
|
|
312
325
|
derivedNames: ReadonlySet<string>,
|
|
313
326
|
computedNames: ReadonlySet<string>,
|
|
327
|
+
blockLocal: ReadonlySet<string> = new Set(),
|
|
314
328
|
): ts.Expression | undefined {
|
|
329
|
+
if (blockLocal.has(name)) {
|
|
330
|
+
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 'value')
|
|
331
|
+
}
|
|
315
332
|
if (stateNames.has(name)) {
|
|
316
333
|
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('model'), name)
|
|
317
334
|
}
|
|
@@ -34,6 +34,8 @@ export type TemplateNode =
|
|
|
34
34
|
items: string
|
|
35
35
|
as: string
|
|
36
36
|
key: string | undefined
|
|
37
|
+
/* `index="i"` → the row's reactive position, bound to this name; absent → unbound. */
|
|
38
|
+
index: string | undefined
|
|
37
39
|
/* `await` on the tag → `items` is an AsyncIterable, drained on the client. */
|
|
38
40
|
async: boolean
|
|
39
41
|
children: TemplateNode[]
|
|
@@ -6,6 +6,8 @@ import type { ResumeEntry } from '../runtime/RESUME.ts'
|
|
|
6
6
|
import { RESUME } from '../runtime/RESUME.ts'
|
|
7
7
|
import { scope } from '../runtime/scope.ts'
|
|
8
8
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
9
|
+
import type { State } from '../runtime/types/State.ts'
|
|
10
|
+
import { state } from '../state.ts'
|
|
9
11
|
import { discardBoundary } from './discardBoundary.ts'
|
|
10
12
|
import { enterNamespace } from './enterNamespace.ts'
|
|
11
13
|
|
|
@@ -52,6 +54,15 @@ export function awaitBlock(
|
|
|
52
54
|
let first = true
|
|
53
55
|
/* Bumped each run so a prior run's in-flight promise can't clobber a newer one. */
|
|
54
56
|
let generation = 0
|
|
57
|
+
/* The resolved value, held as a reactive cell so the then-branch reads it through its
|
|
58
|
+
own effects. A re-run that resolves to a NEW value SETS this cell instead of rebuilding
|
|
59
|
+
the branch — the branch (and any keyed `each` inside it) survives and updates in place,
|
|
60
|
+
so a live cache patch no longer flashes the whole subtree. The branch is rebuilt only
|
|
61
|
+
across a kind change (pending/catch ↔ then), where it has to be. */
|
|
62
|
+
let valueCell: State<unknown> | undefined
|
|
63
|
+
/* Which branch is currently mounted, so a settle knows whether it can update the cell in
|
|
64
|
+
place (then→then) or must build a fresh branch. */
|
|
65
|
+
let activeKind: 'pending' | 'then' | 'catch' | undefined
|
|
55
66
|
|
|
56
67
|
const detach = (): void => {
|
|
57
68
|
if (active !== undefined) {
|
|
@@ -81,33 +92,61 @@ export function awaitBlock(
|
|
|
81
92
|
active = { nodes, dispose }
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
/* Settle to a resolved value. then→then updates the cell in place — the branch and its
|
|
96
|
+
inner each survive (no flash); any other prior kind builds a fresh then-branch around
|
|
97
|
+
a new cell. renderThen receives the CELL (not the raw value), so the branch reads it
|
|
98
|
+
reactively and re-runs its own effects when a later settle sets it. */
|
|
99
|
+
const settleThen = (value: unknown): void => {
|
|
100
|
+
if (activeKind === 'then' && valueCell !== undefined) {
|
|
101
|
+
valueCell.value = value
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const cell = state(value)
|
|
105
|
+
valueCell = cell
|
|
106
|
+
place((host) => renderThen(host, cell))
|
|
107
|
+
activeKind = 'then'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Settle to a rejection: surface it with no catch branch, else swap to the catch branch. */
|
|
111
|
+
const settleError = (error: unknown): void => {
|
|
112
|
+
if (renderCatch === undefined) {
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
valueCell = undefined
|
|
116
|
+
place((host) => renderCatch(host, error))
|
|
117
|
+
activeKind = 'catch'
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
/* Render a settled-or-pending result into the current generation. */
|
|
85
121
|
const render = (result: unknown): void => {
|
|
86
122
|
const gen = generation
|
|
87
123
|
if (!isThenable(result)) {
|
|
88
|
-
|
|
124
|
+
settleThen(result) // warm-sync → resolved now, no flash
|
|
89
125
|
return
|
|
90
126
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
/* A then-branch is already mounted (a revalidation): keep it visible and update in
|
|
128
|
+
place when the new value settles, instead of blanking to pending and rebuilding —
|
|
129
|
+
this is the no-flash live-update path. A first load (or a prior pending/catch)
|
|
130
|
+
shows the pending branch (or detaches) while the promise is in flight. */
|
|
131
|
+
if (activeKind !== 'then') {
|
|
132
|
+
if (renderPending !== undefined) {
|
|
133
|
+
place((host) => renderPending(host))
|
|
134
|
+
activeKind = 'pending'
|
|
135
|
+
} else {
|
|
136
|
+
detach()
|
|
137
|
+
activeKind = undefined
|
|
138
|
+
}
|
|
95
139
|
}
|
|
96
140
|
result.then(
|
|
97
141
|
(value) => {
|
|
98
142
|
if (gen === generation) {
|
|
99
|
-
|
|
143
|
+
settleThen(value)
|
|
100
144
|
}
|
|
101
145
|
},
|
|
102
146
|
(error) => {
|
|
103
|
-
if (gen
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
/* No catch branch → surface the rejection instead of an empty branch. */
|
|
107
|
-
if (renderCatch === undefined) {
|
|
108
|
-
throw error
|
|
147
|
+
if (gen === generation) {
|
|
148
|
+
settleError(error)
|
|
109
149
|
}
|
|
110
|
-
place((host) => renderCatch(host, error))
|
|
111
150
|
},
|
|
112
151
|
)
|
|
113
152
|
}
|
|
@@ -176,11 +215,21 @@ export function awaitBlock(
|
|
|
176
215
|
}
|
|
177
216
|
}
|
|
178
217
|
if (entry !== undefined) {
|
|
218
|
+
/* Build the adopted branch around a value CELL (then) so a later re-run updates
|
|
219
|
+
it in place, exactly like a fresh mount. The `throw` for a catch-less rejection
|
|
220
|
+
stays OUTSIDE the adopt try/catch so it surfaces rather than triggering the
|
|
221
|
+
cold-rebuild fallback. */
|
|
179
222
|
let build: (host: Node) => void
|
|
223
|
+
let cell: State<unknown> | undefined
|
|
224
|
+
let kind: 'then' | 'catch'
|
|
180
225
|
if (entry.ok) {
|
|
181
|
-
|
|
226
|
+
cell = state(entry.value)
|
|
227
|
+
const resolved = cell
|
|
228
|
+
build = (host) => renderThen(host, resolved)
|
|
229
|
+
kind = 'then'
|
|
182
230
|
} else if (renderCatch !== undefined) {
|
|
183
231
|
build = (host) => renderCatch(host, entry.error)
|
|
232
|
+
kind = 'catch'
|
|
184
233
|
} else {
|
|
185
234
|
/* A resumed rejection with no catch branch surfaces (mirrors the cold
|
|
186
235
|
path); in practice the server 500s such a block, so none resumes. */
|
|
@@ -188,14 +237,19 @@ export function awaitBlock(
|
|
|
188
237
|
}
|
|
189
238
|
try {
|
|
190
239
|
adopt(open, build)
|
|
240
|
+
valueCell = cell
|
|
241
|
+
activeKind = kind
|
|
191
242
|
} catch {
|
|
192
243
|
rebuildCold(open)
|
|
193
244
|
}
|
|
194
245
|
return
|
|
195
246
|
}
|
|
196
247
|
if (!isThenable(result)) {
|
|
248
|
+
const cell = state(result)
|
|
197
249
|
try {
|
|
198
|
-
adopt(open, (host) => renderThen(host,
|
|
250
|
+
adopt(open, (host) => renderThen(host, cell))
|
|
251
|
+
valueCell = cell
|
|
252
|
+
activeKind = 'then'
|
|
199
253
|
} catch {
|
|
200
254
|
rebuildCold(open)
|
|
201
255
|
}
|
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { claimExpected } from '../runtime/claimExpected.ts'
|
|
|
4
4
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
5
|
import { scope } from '../runtime/scope.ts'
|
|
6
6
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
7
|
+
import type { State } from '../runtime/types/State.ts'
|
|
8
|
+
import { state } from '../state.ts'
|
|
7
9
|
import { enterNamespace } from './enterNamespace.ts'
|
|
8
10
|
import { moveRange } from './moveRange.ts'
|
|
9
11
|
import { removeRange } from './removeRange.ts'
|
|
@@ -32,7 +34,11 @@ export function each<T>(
|
|
|
32
34
|
parent: Node,
|
|
33
35
|
items: () => Iterable<T>,
|
|
34
36
|
keyOf: (item: T) => string,
|
|
35
|
-
|
|
37
|
+
/* The row receives its item and its position as reactive cells (not raw snapshots): a
|
|
38
|
+
re-key with a changed value, or a reorder that shifts the row, sets the matching cell
|
|
39
|
+
and re-runs the row's effects in place. The compiler binds the `as`/`index` names to
|
|
40
|
+
read them; a direct caller reads `item.value` / `index.value`. */
|
|
41
|
+
render: (parent: Node, item: State<T>, index: State<number>) => void,
|
|
36
42
|
before: Node | null = null,
|
|
37
43
|
): void {
|
|
38
44
|
const rows = new Map<string, EachRow>()
|
|
@@ -49,15 +55,20 @@ export function each<T>(
|
|
|
49
55
|
(`scope` builds untracked), so a raw reactive read in the row content — e.g. a
|
|
50
56
|
nested `<script>` body — can't re-reconcile the whole list. Only `items()` drives
|
|
51
57
|
the each; each row's own interpolations track through their own effects. */
|
|
52
|
-
const buildRow = (item: T): EachRow => {
|
|
58
|
+
const buildRow = (item: T, position: number): EachRow => {
|
|
59
|
+
/* Item and position are held in reactive cells the row reads, so a later re-key with
|
|
60
|
+
a changed value, or a reorder that shifts the row, updates it in place (see the
|
|
61
|
+
reconcile below) instead of rebuilding it. */
|
|
62
|
+
const cell = state(item) as State<unknown>
|
|
63
|
+
const indexCell = state(position)
|
|
53
64
|
const hydration = RENDER.hydration
|
|
54
65
|
if (hydration !== undefined) {
|
|
55
66
|
const start = claimExpected(hydration, parent, 'each row start marker')
|
|
56
67
|
hydration.next.set(parent, start.nextSibling)
|
|
57
|
-
const dispose = group.track(scope(() => render(parent,
|
|
68
|
+
const dispose = group.track(scope(() => render(parent, cell as State<T>, indexCell)))
|
|
58
69
|
const end = claimExpected(hydration, parent, 'each row end marker')
|
|
59
70
|
hydration.next.set(parent, end.nextSibling)
|
|
60
|
-
return { start, end, dispose }
|
|
71
|
+
return { start, end, dispose, cell, indexCell }
|
|
61
72
|
}
|
|
62
73
|
const start = document.createComment('[')
|
|
63
74
|
const end = document.createComment(']')
|
|
@@ -66,10 +77,10 @@ export function each<T>(
|
|
|
66
77
|
/* Build under `parent`'s foreign namespace so foreign row elements (svg/math)
|
|
67
78
|
built into the detached fragment are namespaced, not built as HTML. */
|
|
68
79
|
const dispose = group.track(
|
|
69
|
-
enterNamespace(parent, () => scope(() => render(pending,
|
|
80
|
+
enterNamespace(parent, () => scope(() => render(pending, cell as State<T>, indexCell))),
|
|
70
81
|
)
|
|
71
82
|
pending.appendChild(end)
|
|
72
|
-
return { start, end, dispose, pending }
|
|
83
|
+
return { start, end, dispose, cell, indexCell, pending }
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/* Place a row so its range ends just before `cursor`: insert a fresh row's
|
|
@@ -94,8 +105,10 @@ export function each<T>(
|
|
|
94
105
|
let adopting = false
|
|
95
106
|
const hydration = RENDER.hydration
|
|
96
107
|
if (hydration !== undefined) {
|
|
108
|
+
let position = 0
|
|
97
109
|
for (const item of items()) {
|
|
98
|
-
rows.set(keyOf(item), buildRow(item)) // claims the SSR row where it sits
|
|
110
|
+
rows.set(keyOf(item), buildRow(item, position)) // claims the SSR row where it sits
|
|
111
|
+
position += 1
|
|
99
112
|
}
|
|
100
113
|
anchor = document.createTextNode('')
|
|
101
114
|
parent.insertBefore(anchor, claimChild(hydration, parent))
|
|
@@ -145,8 +158,15 @@ export function each<T>(
|
|
|
145
158
|
const key = keys[index] as string
|
|
146
159
|
let row = rows.get(key)
|
|
147
160
|
if (row === undefined) {
|
|
148
|
-
row = buildRow(list[index] as T)
|
|
161
|
+
row = buildRow(list[index] as T, index)
|
|
149
162
|
rows.set(key, row)
|
|
163
|
+
} else {
|
|
164
|
+
/* Surviving key: push the (possibly new) item and position into the row's
|
|
165
|
+
cells. Both write through `Object.is`, so an unchanged item/position is a
|
|
166
|
+
no-op and a changed one re-runs only the row's own effects — the row's DOM
|
|
167
|
+
is never rebuilt. A reorder thus repaints only the moved rows' `index`. */
|
|
168
|
+
row.cell.value = list[index]
|
|
169
|
+
row.indexCell.value = index
|
|
150
170
|
}
|
|
151
171
|
placeBefore(row, cursor)
|
|
152
172
|
cursor = row.start
|
|
@@ -4,6 +4,8 @@ import { OWNER } from '../runtime/OWNER.ts'
|
|
|
4
4
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
5
|
import { scope } from '../runtime/scope.ts'
|
|
6
6
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
7
|
+
import type { State } from '../runtime/types/State.ts'
|
|
8
|
+
import { state } from '../state.ts'
|
|
7
9
|
import { enterNamespace } from './enterNamespace.ts'
|
|
8
10
|
import { removeRange } from './removeRange.ts'
|
|
9
11
|
import type { EachRow } from './types/EachRow.ts'
|
|
@@ -27,7 +29,10 @@ export function eachAsync<T>(
|
|
|
27
29
|
parent: Node,
|
|
28
30
|
items: () => AsyncIterable<T>,
|
|
29
31
|
keyOf: (item: T) => string,
|
|
30
|
-
|
|
32
|
+
/* The row receives its item and position as reactive cells — same contract as sync
|
|
33
|
+
`each`; the streaming runtime rebuilds the row on a re-yield rather than patching, and
|
|
34
|
+
the position is the stream arrival ordinal (a stream only appends, never reorders). */
|
|
35
|
+
render: (parent: Node, item: State<T>, index: State<number>) => void,
|
|
31
36
|
/* Absent → an iterator rejection surfaces instead of rendering a catch branch. */
|
|
32
37
|
renderCatch: ((parent: Node, error: unknown) => void) | undefined,
|
|
33
38
|
before: Node | null = null,
|
|
@@ -44,8 +49,11 @@ export function eachAsync<T>(
|
|
|
44
49
|
parent.insertBefore(anchor, before) // `before` places rows before a static suffix
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
/* Build a content range and insert it just before the anchor (arrival order).
|
|
48
|
-
|
|
52
|
+
/* Build a content range and insert it just before the anchor (arrival order). A bare
|
|
53
|
+
range (no item cell) — the data-row site adds the cell, the error branch needs none. */
|
|
54
|
+
const insertRange = (
|
|
55
|
+
build: (into: Node) => void,
|
|
56
|
+
): { start: Node; end: Node; dispose: () => void } => {
|
|
49
57
|
const start = document.createComment('[')
|
|
50
58
|
const end = document.createComment(']')
|
|
51
59
|
const fragment = document.createDocumentFragment()
|
|
@@ -62,7 +70,7 @@ export function eachAsync<T>(
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
/* The mounted `<template catch>` range, disposed when a fresh run re-streams. */
|
|
65
|
-
let errorRange:
|
|
73
|
+
let errorRange: { start: Node; end: Node; dispose: () => void } | undefined
|
|
66
74
|
const clearError = (): void => {
|
|
67
75
|
if (errorRange !== undefined) {
|
|
68
76
|
errorRange.dispose()
|
|
@@ -82,6 +90,7 @@ export function eachAsync<T>(
|
|
|
82
90
|
clearError() // a fresh run drops a prior error branch
|
|
83
91
|
const iterable = items() // read (subscribe) synchronously
|
|
84
92
|
const present = new Set<string>()
|
|
93
|
+
let arrivals = 0 // stream arrival ordinal → each row's index
|
|
85
94
|
const drain = async (): Promise<void> => {
|
|
86
95
|
const active = iterable[Symbol.asyncIterator]()
|
|
87
96
|
iterator = active
|
|
@@ -96,14 +105,18 @@ export function eachAsync<T>(
|
|
|
96
105
|
}
|
|
97
106
|
const key = keyOf(result.value)
|
|
98
107
|
present.add(key)
|
|
99
|
-
/* A re-yielded key rebuilds the row from the new value, swapping
|
|
100
|
-
range out
|
|
108
|
+
/* A re-yielded key rebuilds the row from the new value (arrival order), swapping
|
|
109
|
+
the old range out. The item rides in a cell — the row reads it reactively, same
|
|
110
|
+
contract as sync `each` — but the streaming runtime rebuilds rather than patches. */
|
|
101
111
|
const stale = rows.get(key)
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const cell = state(result.value) as State<unknown>
|
|
113
|
+
const indexCell = state(arrivals)
|
|
114
|
+
arrivals += 1
|
|
115
|
+
rows.set(key, {
|
|
116
|
+
...insertRange((host) => render(host, cell as State<T>, indexCell)),
|
|
117
|
+
cell,
|
|
118
|
+
indexCell,
|
|
119
|
+
})
|
|
107
120
|
if (stale !== undefined) {
|
|
108
121
|
stale.dispose()
|
|
109
122
|
removeRange(stale.start, stale.end)
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import type { State } from '../../runtime/types/State.ts'
|
|
2
|
+
|
|
1
3
|
/* A live row in a keyed list: a content RANGE bounded by two comment markers (so a
|
|
2
4
|
row holds any content, not just one node), plus the disposer for the bindings
|
|
3
|
-
created in its ownership scope. `
|
|
4
|
-
|
|
5
|
+
created in its ownership scope. `cell` holds the row's item as a reactive value, so a
|
|
6
|
+
re-key with a changed value (same key, new object) updates the row in place instead of
|
|
7
|
+
leaving it frozen. `indexCell` holds the row's reactive position, so a reorder repaints
|
|
8
|
+
its `index` binding in place without a rebuild. `pending` holds a freshly built row's
|
|
9
|
+
nodes in a fragment until first placement inserts them. */
|
|
5
10
|
export type EachRow = {
|
|
6
11
|
start: Node
|
|
7
12
|
end: Node
|
|
8
13
|
dispose: () => void
|
|
14
|
+
cell: State<unknown>
|
|
15
|
+
indexCell: State<number>
|
|
9
16
|
pending?: DocumentFragment
|
|
10
17
|
}
|