@webjsdev/core 0.7.10 → 0.7.11
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/dist/webjs-core-browser.js +3 -3
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-lazy-loader.js +1 -1
- package/dist/webjs-core-lazy-loader.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +4 -0
- package/index.d.ts +9 -0
- package/index.js +4 -0
- package/package.json +1 -1
- package/src/lazy-loader.js +56 -0
- package/src/optimistic.js +64 -0
- package/src/render-client.js +176 -25
- package/src/router-client.js +763 -37
- package/src/webjs-frame.js +154 -6
- package/src/webjs-stream.js +170 -0
package/index-browser.js
CHANGED
|
@@ -44,10 +44,14 @@ export {
|
|
|
44
44
|
} from './src/serialize.js';
|
|
45
45
|
export { enableClientRouter, disableClientRouter, navigate, revalidate } from './src/router-client.js';
|
|
46
46
|
export { WebjsFrame } from './src/webjs-frame.js';
|
|
47
|
+
export { WebjsStream, renderStream } from './src/webjs-stream.js';
|
|
47
48
|
|
|
48
49
|
// Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
|
|
49
50
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
50
51
|
|
|
52
|
+
// Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
|
|
53
|
+
export { optimistic } from './src/optimistic.js';
|
|
54
|
+
|
|
51
55
|
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
52
56
|
// lit-html-parity set is re-exported here so the dist browser bundle (which
|
|
53
57
|
// the `@webjsdev/core/directives` subpath collapses onto in dist mode) carries
|
package/index.d.ts
CHANGED
|
@@ -77,3 +77,12 @@ export function navigate(url: Route, opts?: { replace?: boolean }): Promise<void
|
|
|
77
77
|
export { unsafeHTML, isUnsafeHTML, live, isLive } from './src/directives.js';
|
|
78
78
|
export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
|
|
79
79
|
export { Task, TaskStatus } from './src/task.js';
|
|
80
|
+
|
|
81
|
+
// Optimistic-mutation helper: set a signal to an expected value immediately,
|
|
82
|
+
// run the action, roll back on a thrown error or a `{ success: false }`
|
|
83
|
+
// ActionResult, keep the value on success. Returns the action's result.
|
|
84
|
+
export function optimistic<T, R>(
|
|
85
|
+
signal: { get(): T; set(v: T): void },
|
|
86
|
+
value: T,
|
|
87
|
+
action: () => Promise<R> | R,
|
|
88
|
+
): Promise<R>;
|
package/index.js
CHANGED
|
@@ -26,10 +26,14 @@ export {
|
|
|
26
26
|
} from './src/serialize.js';
|
|
27
27
|
export { enableClientRouter, disableClientRouter, navigate, revalidate } from './src/router-client.js';
|
|
28
28
|
export { WebjsFrame } from './src/webjs-frame.js';
|
|
29
|
+
export { WebjsStream, renderStream } from './src/webjs-stream.js';
|
|
29
30
|
|
|
30
31
|
// Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
|
|
31
32
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
32
33
|
|
|
34
|
+
// Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
|
|
35
|
+
export { optimistic } from './src/optimistic.js';
|
|
36
|
+
|
|
33
37
|
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
34
38
|
// lit-html-parity set is re-exported so the bare specifier exposes the same
|
|
35
39
|
// directive surface in Node as the browser bundle does.
|
package/package.json
CHANGED
package/src/lazy-loader.js
CHANGED
|
@@ -105,3 +105,59 @@ function onIntersect(entries) {
|
|
|
105
105
|
observer?.unobserve(entry.target);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/* ====================================================================
|
|
110
|
+
* Per-element viewport observation (shared with <webjs-frame loading="lazy">)
|
|
111
|
+
* ==================================================================== */
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A separate IntersectionObserver keyed by ELEMENT (not by tag) so a single
|
|
115
|
+
* element can fire a one-shot callback when it scrolls into view. Reuses the
|
|
116
|
+
* same `rootMargin: '200px'` warm-up budget as the tag-based lazy-component
|
|
117
|
+
* observer above, so a `<webjs-frame loading="lazy">` self-loads with the same
|
|
118
|
+
* timing as a `static lazy = true` component module.
|
|
119
|
+
*
|
|
120
|
+
* @type {IntersectionObserver | null}
|
|
121
|
+
*/
|
|
122
|
+
let elementObserver = null;
|
|
123
|
+
|
|
124
|
+
/** @type {WeakMap<Element, () => void>} element → its one-shot callback */
|
|
125
|
+
const elementCallbacks = new WeakMap();
|
|
126
|
+
|
|
127
|
+
/** @param {IntersectionObserverEntry[]} entries */
|
|
128
|
+
function onElementIntersect(entries) {
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.isIntersecting) continue;
|
|
131
|
+
const cb = elementCallbacks.get(entry.target);
|
|
132
|
+
elementCallbacks.delete(entry.target);
|
|
133
|
+
elementObserver?.unobserve(entry.target);
|
|
134
|
+
if (cb) cb();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fire `callback` once when `el` first enters the viewport (within the
|
|
140
|
+
* shared 200px margin). When IntersectionObserver is unavailable (very old
|
|
141
|
+
* browser, or a non-DOM test env) the callback fires immediately, so a lazy
|
|
142
|
+
* frame still self-loads rather than staying blank. Returns a teardown that
|
|
143
|
+
* stops observing (the frame calls it on disconnect / a `src` change).
|
|
144
|
+
*
|
|
145
|
+
* @param {Element} el
|
|
146
|
+
* @param {() => void} callback
|
|
147
|
+
* @returns {() => void} Teardown that unobserves `el`.
|
|
148
|
+
*/
|
|
149
|
+
export function observeViewportOnce(el, callback) {
|
|
150
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
151
|
+
callback();
|
|
152
|
+
return () => {};
|
|
153
|
+
}
|
|
154
|
+
if (!elementObserver) {
|
|
155
|
+
elementObserver = new IntersectionObserver(onElementIntersect, { rootMargin: '200px' });
|
|
156
|
+
}
|
|
157
|
+
elementCallbacks.set(el, callback);
|
|
158
|
+
elementObserver.observe(el);
|
|
159
|
+
return () => {
|
|
160
|
+
elementCallbacks.delete(el);
|
|
161
|
+
elementObserver?.unobserve(el);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `optimistic(signal, value, action)`, a thin optimistic-mutation helper.
|
|
3
|
+
*
|
|
4
|
+
* The optimistic-UI pattern: show the expected result of a mutation
|
|
5
|
+
* IMMEDIATELY (so the interface feels instant), run the real server action,
|
|
6
|
+
* and roll the optimistic value back if the action fails. This wrapper does
|
|
7
|
+
* exactly that and nothing more, no state machine, no extra reactivity, just
|
|
8
|
+
* the existing signal primitive plus a try / envelope check.
|
|
9
|
+
*
|
|
10
|
+
* import { signal, optimistic } from '@webjsdev/core';
|
|
11
|
+
* import { likePost } from '../actions/like-post.server.js';
|
|
12
|
+
*
|
|
13
|
+
* const liked = signal(false);
|
|
14
|
+
* // In an @click handler:
|
|
15
|
+
* await optimistic(liked, true, () => likePost(postId));
|
|
16
|
+
* // `liked` flips to true instantly; if likePost throws or returns
|
|
17
|
+
* // { success: false }, it rolls back to its prior value.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour:
|
|
20
|
+
* 1. Capture `prev = signal.get()`.
|
|
21
|
+
* 2. `signal.set(value)` so the UI updates before the round-trip.
|
|
22
|
+
* 3. `await action()`.
|
|
23
|
+
* 4a. The action THROWS -> `signal.set(prev)` (rollback) + re-throw.
|
|
24
|
+
* 4b. The action returns an ActionResult FAILURE envelope
|
|
25
|
+
* (`result && result.success === false`) -> `signal.set(prev)`
|
|
26
|
+
* (rollback) + return the result (the caller reads its
|
|
27
|
+
* `error` / `fieldErrors`).
|
|
28
|
+
* 4c. SUCCESS (anything else) -> keep the optimistic value + return the
|
|
29
|
+
* result. The caller can reconcile to the authoritative value from the
|
|
30
|
+
* returned result if it wants to (e.g. `signal.set(result.data.count)`).
|
|
31
|
+
*
|
|
32
|
+
* It rolls back on a thrown error OR a `{ success: false }` ActionResult, and
|
|
33
|
+
* never rolls back on success. Client-only: it mutates a signal (client work),
|
|
34
|
+
* so a component importing it is never elided as display-only.
|
|
35
|
+
*
|
|
36
|
+
* @template T
|
|
37
|
+
* @param {{ get: () => T, set: (v: T) => void }} signal A webjs signal.
|
|
38
|
+
* @param {T} value The optimistic value to show immediately.
|
|
39
|
+
* @param {() => Promise<any> | any} action The server-action call (or any
|
|
40
|
+
* thunk returning a Promise / value).
|
|
41
|
+
* @returns {Promise<any>} The action's result. On a `{ success: false }`
|
|
42
|
+
* envelope the rolled-back result is returned (not thrown); a thrown action
|
|
43
|
+
* re-throws after rollback.
|
|
44
|
+
*/
|
|
45
|
+
export async function optimistic(signal, value, action) {
|
|
46
|
+
const prev = signal.get();
|
|
47
|
+
signal.set(value);
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = await action();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// The action rejected: roll the optimistic value back and let the caller
|
|
53
|
+
// handle the error (re-throw, do not swallow).
|
|
54
|
+
signal.set(prev);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
// ActionResult FAILURE envelope: a `{ success: false }` is a handled
|
|
58
|
+
// failure, not a throw. Roll back and hand the result back so the caller
|
|
59
|
+
// can read its `error` / `fieldErrors`.
|
|
60
|
+
if (result && result.success === false) {
|
|
61
|
+
signal.set(prev);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
package/src/render-client.js
CHANGED
|
@@ -37,8 +37,12 @@ import {
|
|
|
37
37
|
* - Event listeners are attached once and retargeted when the handler
|
|
38
38
|
* reference changes (swap-in-place via a dispatch closure, so `addEventListener`
|
|
39
39
|
* isn't churned every render).
|
|
40
|
-
* -
|
|
41
|
-
*
|
|
40
|
+
* - A plain `.map()` array reconciles POSITIONALLY (non-keyed), matching
|
|
41
|
+
* lit-html: each index updates its item instance in place when the
|
|
42
|
+
* template shape is unchanged, so DOM node identity (and the focus,
|
|
43
|
+
* selection, scroll, and in-progress native drag it carries) survives an
|
|
44
|
+
* item-level update. See `reconcileArray`. Keyed reordering still needs
|
|
45
|
+
* the `repeat()` directive.
|
|
42
46
|
*/
|
|
43
47
|
|
|
44
48
|
/** @type {WeakMap<TemplateStringsArray | string[], { templateEl: HTMLTemplateElement, parts: PartDescriptor[] }>} */
|
|
@@ -911,12 +915,35 @@ function applyChildInner(part, value) {
|
|
|
911
915
|
return;
|
|
912
916
|
}
|
|
913
917
|
|
|
918
|
+
// Plain array (a `.map()` / list interpolation): positional, non-keyed
|
|
919
|
+
// reconciliation. Update each position's instance IN PLACE when its
|
|
920
|
+
// template shape is unchanged, so DOM node identity survives an item
|
|
921
|
+
// update (focus, selection, scroll, and an in-progress native drag all
|
|
922
|
+
// survive). Mirrors lit-html's non-keyed array child part. Without this,
|
|
923
|
+
// flipping one item's binding rebuilt the WHOLE list and detached every
|
|
924
|
+
// node, which cancels a native drag mid-gesture. Use `repeat()` for keyed
|
|
925
|
+
// reordering.
|
|
926
|
+
if (Array.isArray(value)) {
|
|
927
|
+
if (part.child && /** @type any */ (part.child).kind === 'array') {
|
|
928
|
+
reconcileArray(part, value);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
teardownChild(part);
|
|
932
|
+
const arrState = { kind: 'array', items: [] };
|
|
933
|
+
part.child = arrState;
|
|
934
|
+
applyArrayFresh(marker, /** @type any */ (arrState), value);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
914
938
|
// Remove previously rendered nodes between marker and its next sibling we own.
|
|
915
939
|
if (part.child) {
|
|
916
940
|
const c = /** @type any */ (part.child);
|
|
917
941
|
if (c.kind === 'repeat') {
|
|
918
942
|
teardownRepeat(c);
|
|
919
943
|
part.child = undefined;
|
|
944
|
+
} else if (c.kind === 'array') {
|
|
945
|
+
teardownArray(/** @type any */ (c));
|
|
946
|
+
part.child = undefined;
|
|
920
947
|
} else if (c.kind === 'async-stream') {
|
|
921
948
|
teardownAsyncStream(c);
|
|
922
949
|
part.child = undefined;
|
|
@@ -966,29 +993,6 @@ function applyChildInner(part, value) {
|
|
|
966
993
|
return;
|
|
967
994
|
}
|
|
968
995
|
|
|
969
|
-
if (Array.isArray(value)) {
|
|
970
|
-
const list = [];
|
|
971
|
-
for (const v of value) {
|
|
972
|
-
if (isTemplate(v)) {
|
|
973
|
-
// Create an inline instance. No keyed reconciliation yet.
|
|
974
|
-
const tr = /** @type any */ (v);
|
|
975
|
-
const { templateEl, parts } = compile(tr);
|
|
976
|
-
const frag = /** @type DocumentFragment */ (templateEl.content.cloneNode(true));
|
|
977
|
-
const bound = parts.map((p) => bindPart(p, frag));
|
|
978
|
-
for (let i = 0; i < tr.values.length; i++) {
|
|
979
|
-
applyPart(bound[i], tr.values[i], undefined, tr.values);
|
|
980
|
-
}
|
|
981
|
-
list.push(...frag.childNodes);
|
|
982
|
-
} else if (v != null && v !== false && v !== true) {
|
|
983
|
-
list.push(document.createTextNode(String(v)));
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
const frag = nodesToFrag(list);
|
|
987
|
-
marker.parentNode?.insertBefore(frag, marker);
|
|
988
|
-
part.child = list;
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
996
|
const node = document.createTextNode(String(value));
|
|
993
997
|
marker.parentNode?.insertBefore(node, marker);
|
|
994
998
|
part.child = [node];
|
|
@@ -1150,6 +1154,151 @@ function teardownRepeat(state) {
|
|
|
1150
1154
|
state.map.clear();
|
|
1151
1155
|
}
|
|
1152
1156
|
|
|
1157
|
+
/* ================================================================
|
|
1158
|
+
* Plain array (.map) support: positional, non-keyed reconciliation
|
|
1159
|
+
* ================================================================ */
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* One rendered slot of a plain array. A `tpl` carries a detached template
|
|
1163
|
+
* instance (bookended by its own markers), a `text` carries a single text
|
|
1164
|
+
* node, and an `empty` slot (a nullish / boolean element) renders nothing
|
|
1165
|
+
* but still holds the position so index-based reconciliation stays aligned.
|
|
1166
|
+
* @typedef {{ type: 'tpl', inst: TemplateInstance } | { type: 'text', node: Text } | { type: 'empty' }} ArrayItem
|
|
1167
|
+
*/
|
|
1168
|
+
|
|
1169
|
+
/** @param {ArrayItem} item @returns {ChildNode | null} */
|
|
1170
|
+
function arrayItemFirstNode(item) {
|
|
1171
|
+
if (item.type === 'tpl') return item.inst.startNode;
|
|
1172
|
+
if (item.type === 'text') return item.node;
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** @param {ArrayItem} item */
|
|
1177
|
+
function removeArrayItem(item) {
|
|
1178
|
+
if (item.type === 'tpl') {
|
|
1179
|
+
disposeInstance(item.inst);
|
|
1180
|
+
removeBetween(item.inst.startNode, item.inst.endNode);
|
|
1181
|
+
} else if (item.type === 'text') {
|
|
1182
|
+
item.node.parentNode?.removeChild(item.node);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Build the slot for one array element, plus the fragment to insert (null
|
|
1188
|
+
* for an empty slot). A TemplateResult becomes a detached instance, a
|
|
1189
|
+
* primitive becomes a text node, and nullish / boolean renders nothing.
|
|
1190
|
+
* @param {unknown} v
|
|
1191
|
+
* @returns {{ item: ArrayItem, frag: Node | null }}
|
|
1192
|
+
*/
|
|
1193
|
+
function buildArrayItem(v) {
|
|
1194
|
+
if (isTemplate(v)) {
|
|
1195
|
+
const { inst, frag } = buildDetached(/** @type any */ (v));
|
|
1196
|
+
return { item: { type: 'tpl', inst }, frag };
|
|
1197
|
+
}
|
|
1198
|
+
if (v != null && v !== false && v !== true) {
|
|
1199
|
+
const node = document.createTextNode(String(v));
|
|
1200
|
+
return { item: { type: 'text', node }, frag: node };
|
|
1201
|
+
}
|
|
1202
|
+
return { item: { type: 'empty' }, frag: null };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Initial render of a plain array: insert every slot's nodes before the marker.
|
|
1207
|
+
* @param {Comment} marker
|
|
1208
|
+
* @param {{ kind: 'array', items: ArrayItem[] }} state
|
|
1209
|
+
* @param {unknown[]} value
|
|
1210
|
+
*/
|
|
1211
|
+
function applyArrayFresh(marker, state, value) {
|
|
1212
|
+
const parent = marker.parentNode;
|
|
1213
|
+
if (!parent) return;
|
|
1214
|
+
const bulk = document.createDocumentFragment();
|
|
1215
|
+
/** @type {ArrayItem[]} */
|
|
1216
|
+
const items = [];
|
|
1217
|
+
for (const v of value) {
|
|
1218
|
+
const { item, frag } = buildArrayItem(v);
|
|
1219
|
+
if (frag) bulk.appendChild(frag);
|
|
1220
|
+
items.push(item);
|
|
1221
|
+
}
|
|
1222
|
+
parent.insertBefore(bulk, marker);
|
|
1223
|
+
state.items = items;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Positional (non-keyed) reconciliation. For each index, update the slot
|
|
1228
|
+
* in place when its shape is unchanged (preserving DOM node identity),
|
|
1229
|
+
* otherwise replace it; grow or shrink at the tail. There is no key
|
|
1230
|
+
* matching, so a reorder reduces to a series of in-place value updates;
|
|
1231
|
+
* reach for `repeat()` when element identity must follow a moved key.
|
|
1232
|
+
* @param {Extract<BoundPart, {kind:'child'}>} part
|
|
1233
|
+
* @param {unknown[]} value
|
|
1234
|
+
*/
|
|
1235
|
+
function reconcileArray(part, value) {
|
|
1236
|
+
const marker = part.marker;
|
|
1237
|
+
const parent = marker.parentNode;
|
|
1238
|
+
if (!parent) return;
|
|
1239
|
+
const state = /** @type {{ kind: 'array', items: ArrayItem[] }} */ (part.child);
|
|
1240
|
+
const old = state.items;
|
|
1241
|
+
/** @type {ArrayItem[]} */
|
|
1242
|
+
const next = [];
|
|
1243
|
+
|
|
1244
|
+
for (let i = 0; i < value.length; i++) {
|
|
1245
|
+
const v = value[i];
|
|
1246
|
+
const o = old[i];
|
|
1247
|
+
if (isTemplate(v)) {
|
|
1248
|
+
const tr = /** @type any */ (v);
|
|
1249
|
+
if (o && o.type === 'tpl' && o.inst.strings === tr.strings) {
|
|
1250
|
+
updateInstance(o.inst, tr.values);
|
|
1251
|
+
next.push(o);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
} else if (v != null && v !== false && v !== true) {
|
|
1255
|
+
if (o && o.type === 'text') {
|
|
1256
|
+
const str = String(v);
|
|
1257
|
+
if (o.node.data !== str) o.node.data = str;
|
|
1258
|
+
next.push(o);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
} else {
|
|
1262
|
+
// Empty slot: drop any prior nodes that occupied this position.
|
|
1263
|
+
if (o) removeArrayItem(o);
|
|
1264
|
+
next.push({ type: 'empty' });
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
// Shape changed, or the array grew past the old length. Build fresh,
|
|
1268
|
+
// insert at this position (before the current / next still-attached
|
|
1269
|
+
// old node, else the marker), then drop the old slot it replaced.
|
|
1270
|
+
const { item, frag } = buildArrayItem(v);
|
|
1271
|
+
if (frag) parent.insertBefore(frag, nextArrayAnchor(old, i, marker));
|
|
1272
|
+
if (o) removeArrayItem(o);
|
|
1273
|
+
next.push(item);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Shrink: remove slots beyond the new length.
|
|
1277
|
+
for (let i = value.length; i < old.length; i++) removeArrayItem(old[i]);
|
|
1278
|
+
state.items = next;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* The node a freshly-built slot at index `i` inserts before: the first
|
|
1283
|
+
* node of the current or next still-attached old slot, else the part
|
|
1284
|
+
* marker (a tail append).
|
|
1285
|
+
* @param {ArrayItem[]} old @param {number} i @param {Comment} marker
|
|
1286
|
+
* @returns {ChildNode}
|
|
1287
|
+
*/
|
|
1288
|
+
function nextArrayAnchor(old, i, marker) {
|
|
1289
|
+
for (let j = i; j < old.length; j++) {
|
|
1290
|
+
const f = arrayItemFirstNode(old[j]);
|
|
1291
|
+
if (f && f.parentNode) return f;
|
|
1292
|
+
}
|
|
1293
|
+
return marker;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** @param {{ kind: 'array', items: ArrayItem[] }} state */
|
|
1297
|
+
function teardownArray(state) {
|
|
1298
|
+
for (const it of state.items) removeArrayItem(it);
|
|
1299
|
+
state.items = [];
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1153
1302
|
/**
|
|
1154
1303
|
* Collect [start .. end] (inclusive) and insert immediately before `anchor`.
|
|
1155
1304
|
* Browsers treat insertBefore of an already-connected node as a move and
|
|
@@ -1208,6 +1357,8 @@ function teardownChild(part) {
|
|
|
1208
1357
|
const c = /** @type any */ (part.child);
|
|
1209
1358
|
if (c.kind === 'repeat') {
|
|
1210
1359
|
teardownRepeat(c);
|
|
1360
|
+
} else if (c.kind === 'array') {
|
|
1361
|
+
teardownArray(c);
|
|
1211
1362
|
} else if (c.kind === 'async-stream') {
|
|
1212
1363
|
teardownAsyncStream(c);
|
|
1213
1364
|
} else if ('strings' in c) {
|