@b9g/crank 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -138
- package/_css.cjs +80 -0
- package/_css.cjs.map +1 -0
- package/_css.d.ts +21 -0
- package/_css.js +76 -0
- package/_css.js.map +1 -0
- package/_utils.cjs +106 -0
- package/_utils.cjs.map +1 -0
- package/_utils.js +99 -0
- package/_utils.js.map +1 -0
- package/async.cjs +42 -39
- package/async.cjs.map +1 -1
- package/async.d.ts +10 -7
- package/async.js +42 -39
- package/async.js.map +1 -1
- package/crank.cjs +87 -141
- package/crank.cjs.map +1 -1
- package/crank.d.ts +255 -3
- package/crank.js +51 -105
- package/crank.js.map +1 -1
- package/dom.cjs +33 -19
- package/dom.cjs.map +1 -1
- package/dom.js +33 -19
- package/dom.js.map +1 -1
- package/html.cjs +5 -3
- package/html.cjs.map +1 -1
- package/html.js +5 -3
- package/html.js.map +1 -1
- package/jsx-runtime.cjs +0 -1
- package/jsx-runtime.cjs.map +1 -1
- package/jsx-runtime.js +0 -1
- package/jsx-runtime.js.map +1 -1
- package/jsx-tag.cjs +0 -1
- package/jsx-tag.cjs.map +1 -1
- package/jsx-tag.js +0 -1
- package/jsx-tag.js.map +1 -1
- package/package.json +3 -2
- package/standalone.cjs +0 -1
- package/standalone.cjs.map +1 -1
- package/standalone.js +0 -1
- package/standalone.js.map +1 -1
- package/umd.js +160 -28
- package/umd.js.map +1 -1
package/crank.d.ts
CHANGED
|
@@ -193,17 +193,81 @@ declare class Retainer<TNode, TScope = unknown> {
|
|
|
193
193
|
constructor(el: Element);
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
|
-
* Interface for adapting the rendering process to a specific target
|
|
197
|
-
*
|
|
198
|
-
* to
|
|
196
|
+
* Interface for adapting the rendering process to a specific target environment.
|
|
197
|
+
*
|
|
198
|
+
* The RenderAdapter defines how Crank elements are mapped to nodes in your target
|
|
199
|
+
* rendering environment (DOM, Canvas, WebGL, Terminal, etc.). Each method handles
|
|
200
|
+
* a specific part of the element lifecycle, from creation to removal.
|
|
201
|
+
*
|
|
202
|
+
* @template TNode - The type representing a node in your target environment
|
|
203
|
+
* @template TScope - Additional context data passed down the component tree
|
|
204
|
+
* @template TRoot - The type of the root container (defaults to TNode)
|
|
205
|
+
* @template TResult - The type returned when reading element values (defaults to ElementValue<TNode>)
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const adapter: RenderAdapter<MyNode, MyScope> = {
|
|
210
|
+
* create: ({ tag, props }) => new MyNode(tag, props),
|
|
211
|
+
* patch: ({ node, props }) => node.update(props),
|
|
212
|
+
* arrange: ({ node, children }) => node.replaceChildren(children),
|
|
213
|
+
* // ... other methods
|
|
214
|
+
* };
|
|
215
|
+
* ```
|
|
199
216
|
*/
|
|
200
217
|
export interface RenderAdapter<TNode, TScope, TRoot extends TNode | undefined = TNode, TResult = ElementValue<TNode>> {
|
|
218
|
+
/**
|
|
219
|
+
* Creates a new node for the given element tag and props.
|
|
220
|
+
*
|
|
221
|
+
* This method is called when Crank encounters a new element that needs to be
|
|
222
|
+
* rendered for the first time. You should create and return a node appropriate
|
|
223
|
+
* for your target environment.
|
|
224
|
+
*
|
|
225
|
+
* @param data.tag - The element tag (e.g., "div", "sprite", or a symbol)
|
|
226
|
+
* @param data.tagName - String representation of the tag for debugging
|
|
227
|
+
* @param data.props - The element's props object
|
|
228
|
+
* @param data.scope - Current scope context (can be undefined)
|
|
229
|
+
* @returns A new node instance
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* create: ({ tag, props, scope }) => {
|
|
234
|
+
* if (tag === "sprite") {
|
|
235
|
+
* return new PIXI.Sprite(props.texture);
|
|
236
|
+
* }
|
|
237
|
+
* throw new Error(`Unknown tag: ${tag}`);
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
201
241
|
create(data: {
|
|
202
242
|
tag: string | symbol;
|
|
203
243
|
tagName: string;
|
|
204
244
|
props: Record<string, any>;
|
|
205
245
|
scope: TScope | undefined;
|
|
206
246
|
}): TNode;
|
|
247
|
+
/**
|
|
248
|
+
* Adopts existing nodes during hydration.
|
|
249
|
+
*
|
|
250
|
+
* Called when hydrating server-rendered content or reusing existing nodes.
|
|
251
|
+
* Should return an array of child nodes if the provided node matches the
|
|
252
|
+
* expected tag, or undefined if hydration should fail.
|
|
253
|
+
*
|
|
254
|
+
* @param data.tag - The element tag being hydrated
|
|
255
|
+
* @param data.tagName - String representation of the tag
|
|
256
|
+
* @param data.props - The element's props
|
|
257
|
+
* @param data.node - The existing node to potentially adopt
|
|
258
|
+
* @param data.scope - Current scope context
|
|
259
|
+
* @returns Array of child nodes to hydrate, or undefined if adoption fails
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* adopt: ({ tag, node }) => {
|
|
264
|
+
* if (node && node.tagName.toLowerCase() === tag) {
|
|
265
|
+
* return Array.from(node.children);
|
|
266
|
+
* }
|
|
267
|
+
* return undefined; // Hydration mismatch
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
207
271
|
adopt(data: {
|
|
208
272
|
tag: string | symbol;
|
|
209
273
|
tagName: string;
|
|
@@ -211,23 +275,124 @@ export interface RenderAdapter<TNode, TScope, TRoot extends TNode | undefined =
|
|
|
211
275
|
node: TNode | undefined;
|
|
212
276
|
scope: TScope | undefined;
|
|
213
277
|
}): Array<TNode> | undefined;
|
|
278
|
+
/**
|
|
279
|
+
* Creates or updates a text node.
|
|
280
|
+
*
|
|
281
|
+
* Called when rendering text content. Should create a new text node or
|
|
282
|
+
* update an existing one with the provided value.
|
|
283
|
+
*
|
|
284
|
+
* @param data.value - The text content to render
|
|
285
|
+
* @param data.scope - Current scope context
|
|
286
|
+
* @param data.oldNode - Previous text node to potentially reuse
|
|
287
|
+
* @param data.hydrationNodes - Nodes available during hydration
|
|
288
|
+
* @returns A text node containing the given value
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* text: ({ value, oldNode }) => {
|
|
293
|
+
* if (oldNode && oldNode.text !== value) {
|
|
294
|
+
* oldNode.text = value;
|
|
295
|
+
* return oldNode;
|
|
296
|
+
* }
|
|
297
|
+
* return new TextNode(value);
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
214
301
|
text(data: {
|
|
215
302
|
value: string;
|
|
216
303
|
scope: TScope | undefined;
|
|
217
304
|
oldNode: TNode | undefined;
|
|
218
305
|
hydrationNodes: Array<TNode> | undefined;
|
|
219
306
|
}): TNode;
|
|
307
|
+
/**
|
|
308
|
+
* Computes scope context for child elements.
|
|
309
|
+
*
|
|
310
|
+
* Called to determine what scope context should be passed to child elements.
|
|
311
|
+
* The scope can be used to pass rendering context like theme, coordinate systems,
|
|
312
|
+
* or namespaces down the component tree.
|
|
313
|
+
*
|
|
314
|
+
* @param data.tag - The element tag
|
|
315
|
+
* @param data.tagName - String representation of the tag
|
|
316
|
+
* @param data.props - The element's props
|
|
317
|
+
* @param data.scope - Current scope context
|
|
318
|
+
* @returns New scope for children, or undefined to inherit current scope
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* scope: ({ tag, props, scope }) => {
|
|
323
|
+
* if (tag === "svg") {
|
|
324
|
+
* return { ...scope, namespace: "http://www.w3.org/2000/svg" };
|
|
325
|
+
* }
|
|
326
|
+
* return scope;
|
|
327
|
+
* }
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
220
330
|
scope(data: {
|
|
221
331
|
tag: string | symbol;
|
|
222
332
|
tagName: string;
|
|
223
333
|
props: Record<string, any>;
|
|
224
334
|
scope: TScope | undefined;
|
|
225
335
|
}): TScope | undefined;
|
|
336
|
+
/**
|
|
337
|
+
* Handles raw values (strings or nodes) that bypass normal element processing.
|
|
338
|
+
*
|
|
339
|
+
* Called when rendering Raw elements or other direct node insertions.
|
|
340
|
+
* Should convert string values to appropriate nodes for your environment.
|
|
341
|
+
*
|
|
342
|
+
* @param data.value - Raw string or node value to render
|
|
343
|
+
* @param data.scope - Current scope context
|
|
344
|
+
* @param data.hydrationNodes - Nodes available during hydration
|
|
345
|
+
* @returns ElementValue that can be handled by arrange()
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* raw: ({ value, scope }) => {
|
|
350
|
+
* if (typeof value === "string") {
|
|
351
|
+
* const container = new Container();
|
|
352
|
+
* container.innerHTML = value;
|
|
353
|
+
* return Array.from(container.children);
|
|
354
|
+
* }
|
|
355
|
+
* return value;
|
|
356
|
+
* }
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
226
359
|
raw(data: {
|
|
227
360
|
value: string | TNode;
|
|
228
361
|
scope: TScope | undefined;
|
|
229
362
|
hydrationNodes: Array<TNode> | undefined;
|
|
230
363
|
}): ElementValue<TNode>;
|
|
364
|
+
/**
|
|
365
|
+
* Updates a node's properties.
|
|
366
|
+
*
|
|
367
|
+
* Called when element props change. Should efficiently update only the
|
|
368
|
+
* properties that have changed. This is where you implement prop-to-attribute
|
|
369
|
+
* mapping, event listener binding, and other property synchronization.
|
|
370
|
+
*
|
|
371
|
+
* @param data.tag - The element tag
|
|
372
|
+
* @param data.tagName - String representation of the tag
|
|
373
|
+
* @param data.node - The node to update
|
|
374
|
+
* @param data.props - New props object
|
|
375
|
+
* @param data.oldProps - Previous props object (undefined for initial render)
|
|
376
|
+
* @param data.scope - Current scope context
|
|
377
|
+
* @param data.copyProps - Props to skip (used for copying between renderers)
|
|
378
|
+
* @param data.isHydrating - Whether currently hydrating
|
|
379
|
+
* @param data.quietProps - Props to not warn about during hydration
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```typescript
|
|
383
|
+
* patch: ({ node, props, oldProps }) => {
|
|
384
|
+
* for (const [key, value] of Object.entries(props)) {
|
|
385
|
+
* if (oldProps?.[key] !== value) {
|
|
386
|
+
* if (key.startsWith("on")) {
|
|
387
|
+
* node.addEventListener(key.slice(2), value);
|
|
388
|
+
* } else {
|
|
389
|
+
* node[key] = value;
|
|
390
|
+
* }
|
|
391
|
+
* }
|
|
392
|
+
* }
|
|
393
|
+
* }
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
231
396
|
patch(data: {
|
|
232
397
|
tag: string | symbol;
|
|
233
398
|
tagName: string;
|
|
@@ -239,6 +404,32 @@ export interface RenderAdapter<TNode, TScope, TRoot extends TNode | undefined =
|
|
|
239
404
|
isHydrating: boolean;
|
|
240
405
|
quietProps: Set<string> | undefined;
|
|
241
406
|
}): void;
|
|
407
|
+
/**
|
|
408
|
+
* Arranges child nodes within their parent.
|
|
409
|
+
*
|
|
410
|
+
* Called after child elements are rendered to organize them within their
|
|
411
|
+
* parent node. Should efficiently insert, move, or remove child nodes to
|
|
412
|
+
* match the provided children array.
|
|
413
|
+
*
|
|
414
|
+
* @param data.tag - The parent element tag
|
|
415
|
+
* @param data.tagName - String representation of the tag
|
|
416
|
+
* @param data.node - The parent node
|
|
417
|
+
* @param data.props - The parent element's props
|
|
418
|
+
* @param data.children - Array of child nodes in correct order
|
|
419
|
+
* @param data.oldProps - Previous props (for reference)
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```typescript
|
|
423
|
+
* arrange: ({ node, children }) => {
|
|
424
|
+
* // Remove existing children
|
|
425
|
+
* node.removeChildren();
|
|
426
|
+
* // Add new children in order
|
|
427
|
+
* for (const child of children) {
|
|
428
|
+
* node.addChild(child);
|
|
429
|
+
* }
|
|
430
|
+
* }
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
242
433
|
arrange(data: {
|
|
243
434
|
tag: string | symbol;
|
|
244
435
|
tagName: string;
|
|
@@ -247,12 +438,73 @@ export interface RenderAdapter<TNode, TScope, TRoot extends TNode | undefined =
|
|
|
247
438
|
children: Array<TNode>;
|
|
248
439
|
oldProps: Record<string, any> | undefined;
|
|
249
440
|
}): void;
|
|
441
|
+
/**
|
|
442
|
+
* Removes a node from its parent.
|
|
443
|
+
*
|
|
444
|
+
* Called when an element is being unmounted. Should clean up the node
|
|
445
|
+
* and remove it from its parent if appropriate.
|
|
446
|
+
*
|
|
447
|
+
* @param data.node - The node to remove
|
|
448
|
+
* @param data.parentNode - The parent node
|
|
449
|
+
* @param data.isNested - Whether this is a nested removal (child of removed element)
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* remove: ({ node, parentNode, isNested }) => {
|
|
454
|
+
* // Clean up event listeners, resources, etc.
|
|
455
|
+
* node.cleanup?.();
|
|
456
|
+
* // Remove from parent unless it's a nested removal
|
|
457
|
+
* if (!isNested && parentNode.contains(node)) {
|
|
458
|
+
* parentNode.removeChild(node);
|
|
459
|
+
* }
|
|
460
|
+
* }
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
250
463
|
remove(data: {
|
|
251
464
|
node: TNode;
|
|
252
465
|
parentNode: TNode;
|
|
253
466
|
isNested: boolean;
|
|
254
467
|
}): void;
|
|
468
|
+
/**
|
|
469
|
+
* Reads the final rendered value from an ElementValue.
|
|
470
|
+
*
|
|
471
|
+
* Called to extract the final result from rendered elements. This allows
|
|
472
|
+
* you to transform the internal node representation into the public API
|
|
473
|
+
* that users of your renderer will see.
|
|
474
|
+
*
|
|
475
|
+
* @param value - The ElementValue to read (array, single node, or undefined)
|
|
476
|
+
* @returns The public representation of the rendered value
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* read: (value) => {
|
|
481
|
+
* if (Array.isArray(value)) {
|
|
482
|
+
* return value.map(node => node.publicAPI);
|
|
483
|
+
* }
|
|
484
|
+
* return value?.publicAPI;
|
|
485
|
+
* }
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
255
488
|
read(value: ElementValue<TNode>): TResult;
|
|
489
|
+
/**
|
|
490
|
+
* Performs final rendering to the root container.
|
|
491
|
+
*
|
|
492
|
+
* Called after the entire render cycle is complete. This is where you
|
|
493
|
+
* trigger the actual rendering/presentation in your target environment
|
|
494
|
+
* (e.g., calling render() on a canvas, flushing to the screen, etc.).
|
|
495
|
+
*
|
|
496
|
+
* @param root - The root container
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```typescript
|
|
500
|
+
* finalize: (root) => {
|
|
501
|
+
* // Trigger actual rendering
|
|
502
|
+
* if (root instanceof PIXIApplication) {
|
|
503
|
+
* root.render();
|
|
504
|
+
* }
|
|
505
|
+
* }
|
|
506
|
+
* ```
|
|
507
|
+
*/
|
|
256
508
|
finalize(root: TRoot): void;
|
|
257
509
|
}
|
|
258
510
|
/**
|
package/crank.js
CHANGED
|
@@ -1,102 +1,6 @@
|
|
|
1
1
|
/// <reference types="crank.d.ts" />
|
|
2
2
|
import { removeEventTargetDelegates, addEventTargetDelegates, clearEventListeners, CustomEventTarget } from './event-target.js';
|
|
3
|
-
|
|
4
|
-
function wrap(value) {
|
|
5
|
-
return value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
6
|
-
}
|
|
7
|
-
function unwrap(arr) {
|
|
8
|
-
return arr.length === 0 ? undefined : arr.length === 1 ? arr[0] : arr;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Ensures a value is an array.
|
|
12
|
-
*
|
|
13
|
-
* This function does the same thing as wrap() above except it handles nulls
|
|
14
|
-
* and iterables, so it is appropriate for wrapping user-provided element
|
|
15
|
-
* children.
|
|
16
|
-
*/
|
|
17
|
-
function arrayify(value) {
|
|
18
|
-
return value == null
|
|
19
|
-
? []
|
|
20
|
-
: Array.isArray(value)
|
|
21
|
-
? value
|
|
22
|
-
: typeof value === "string" ||
|
|
23
|
-
typeof value[Symbol.iterator] !== "function"
|
|
24
|
-
? [value]
|
|
25
|
-
: [...value];
|
|
26
|
-
}
|
|
27
|
-
function isIteratorLike(value) {
|
|
28
|
-
return value != null && typeof value.next === "function";
|
|
29
|
-
}
|
|
30
|
-
function isPromiseLike(value) {
|
|
31
|
-
return value != null && typeof value.then === "function";
|
|
32
|
-
}
|
|
33
|
-
function createRaceRecord(contender) {
|
|
34
|
-
const deferreds = new Set();
|
|
35
|
-
const record = { deferreds, settled: false };
|
|
36
|
-
// This call to `then` happens once for the lifetime of the value.
|
|
37
|
-
Promise.resolve(contender).then((value) => {
|
|
38
|
-
for (const { resolve } of deferreds) {
|
|
39
|
-
resolve(value);
|
|
40
|
-
}
|
|
41
|
-
deferreds.clear();
|
|
42
|
-
record.settled = true;
|
|
43
|
-
}, (err) => {
|
|
44
|
-
for (const { reject } of deferreds) {
|
|
45
|
-
reject(err);
|
|
46
|
-
}
|
|
47
|
-
deferreds.clear();
|
|
48
|
-
record.settled = true;
|
|
49
|
-
});
|
|
50
|
-
return record;
|
|
51
|
-
}
|
|
52
|
-
// Promise.race is memory unsafe. This is alternative which is. See:
|
|
53
|
-
// https://github.com/nodejs/node/issues/17469#issuecomment-685235106
|
|
54
|
-
// Keys are the values passed to race.
|
|
55
|
-
// Values are a record of data containing a set of deferreds and whether the
|
|
56
|
-
// value has settled.
|
|
57
|
-
const wm = new WeakMap();
|
|
58
|
-
function safeRace(contenders) {
|
|
59
|
-
let deferred;
|
|
60
|
-
const result = new Promise((resolve, reject) => {
|
|
61
|
-
deferred = { resolve, reject };
|
|
62
|
-
for (const contender of contenders) {
|
|
63
|
-
if (!isPromiseLike(contender)) {
|
|
64
|
-
// If the contender is a not a then-able, attempting to use it as a key
|
|
65
|
-
// in the weakmap would throw an error. Luckily, it is safe to call
|
|
66
|
-
// `Promise.resolve(contender).then` on regular values multiple
|
|
67
|
-
// times because the promise fulfills immediately.
|
|
68
|
-
Promise.resolve(contender).then(resolve, reject);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
let record = wm.get(contender);
|
|
72
|
-
if (record === undefined) {
|
|
73
|
-
record = createRaceRecord(contender);
|
|
74
|
-
record.deferreds.add(deferred);
|
|
75
|
-
wm.set(contender, record);
|
|
76
|
-
}
|
|
77
|
-
else if (record.settled) {
|
|
78
|
-
// If the value has settled, it is safe to call
|
|
79
|
-
// `Promise.resolve(contender).then` on it.
|
|
80
|
-
Promise.resolve(contender).then(resolve, reject);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
record.deferreds.add(deferred);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
// The finally callback executes when any value settles, preventing any of
|
|
88
|
-
// the unresolved values from retaining a reference to the resolved value.
|
|
89
|
-
return result.finally(() => {
|
|
90
|
-
for (const contender of contenders) {
|
|
91
|
-
if (isPromiseLike(contender)) {
|
|
92
|
-
const record = wm.get(contender);
|
|
93
|
-
if (record) {
|
|
94
|
-
record.deferreds.delete(deferred);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
3
|
+
import { isPromiseLike, unwrap, wrap, arrayify, safeRace, isIteratorLike } from './_utils.js';
|
|
100
4
|
|
|
101
5
|
const NOOP = () => { };
|
|
102
6
|
function getTagName(tag) {
|
|
@@ -261,6 +165,7 @@ const IsInForOfLoop = 1 << 13;
|
|
|
261
165
|
const IsInForAwaitOfLoop = 1 << 14;
|
|
262
166
|
const NeedsToYield = 1 << 15;
|
|
263
167
|
const PropsAvailable = 1 << 16;
|
|
168
|
+
const IsSchedulingRefresh = 1 << 17;
|
|
264
169
|
function getFlag(ret, flag) {
|
|
265
170
|
return !!(ret.f & flag);
|
|
266
171
|
}
|
|
@@ -614,10 +519,10 @@ function diffChildren(adapter, root, host, ctx, scope, parent, newChildren) {
|
|
|
614
519
|
}
|
|
615
520
|
}
|
|
616
521
|
else if (ret) {
|
|
522
|
+
let candidateFound = false;
|
|
617
523
|
// we do not need to add the retainer to the graveyard if it is the
|
|
618
524
|
// fallback of another retainer
|
|
619
525
|
// search for the tag in fallback chain
|
|
620
|
-
let candidateFound = false;
|
|
621
526
|
for (let predecessor = ret, candidate = ret.fallback; candidate; predecessor = candidate, candidate = candidate.fallback) {
|
|
622
527
|
if (candidate.el.tag === child.tag) {
|
|
623
528
|
// If we find a retainer in the fallback chain with the same tag,
|
|
@@ -1452,6 +1357,9 @@ class Context extends CustomEventTarget {
|
|
|
1452
1357
|
});
|
|
1453
1358
|
}
|
|
1454
1359
|
}
|
|
1360
|
+
if (getFlag(ctx.ret, IsScheduling)) {
|
|
1361
|
+
setFlag(ctx.ret, IsSchedulingRefresh);
|
|
1362
|
+
}
|
|
1455
1363
|
let diff;
|
|
1456
1364
|
const schedulePromises = [];
|
|
1457
1365
|
try {
|
|
@@ -1797,10 +1705,11 @@ function runComponent(ctx) {
|
|
|
1797
1705
|
if (getFlag(ctx.ret, IsInForOfLoop) &&
|
|
1798
1706
|
!getFlag(ctx.ret, NeedsToYield) &&
|
|
1799
1707
|
!getFlag(ctx.ret, IsUnmounted) &&
|
|
1800
|
-
!getFlag(ctx.ret,
|
|
1708
|
+
!getFlag(ctx.ret, IsSchedulingRefresh)) {
|
|
1801
1709
|
console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
|
|
1802
1710
|
}
|
|
1803
1711
|
setFlag(ctx.ret, NeedsToYield, false);
|
|
1712
|
+
setFlag(ctx.ret, IsSchedulingRefresh, false);
|
|
1804
1713
|
if (iteration.done) {
|
|
1805
1714
|
setFlag(ctx.ret, IsSyncGen, false);
|
|
1806
1715
|
ctx.iterator = undefined;
|
|
@@ -1846,11 +1755,12 @@ function runComponent(ctx) {
|
|
|
1846
1755
|
if (getFlag(ctx.ret, IsInForOfLoop) &&
|
|
1847
1756
|
!getFlag(ctx.ret, NeedsToYield) &&
|
|
1848
1757
|
!getFlag(ctx.ret, IsUnmounted) &&
|
|
1849
|
-
!getFlag(ctx.ret,
|
|
1758
|
+
!getFlag(ctx.ret, IsSchedulingRefresh)) {
|
|
1850
1759
|
console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
|
|
1851
1760
|
}
|
|
1852
1761
|
}
|
|
1853
1762
|
setFlag(ctx.ret, NeedsToYield, false);
|
|
1763
|
+
setFlag(ctx.ret, IsSchedulingRefresh, false);
|
|
1854
1764
|
if (iteration.done) {
|
|
1855
1765
|
setFlag(ctx.ret, IsAsyncGen, false);
|
|
1856
1766
|
ctx.iterator = undefined;
|
|
@@ -2082,18 +1992,17 @@ function commitComponent(ctx, schedulePromises, hydrationNodes) {
|
|
|
2082
1992
|
});
|
|
2083
1993
|
return getValue(ctx.ret);
|
|
2084
1994
|
}
|
|
2085
|
-
const wasScheduling = getFlag(ctx.ret, IsScheduling);
|
|
2086
1995
|
const values = commitChildren(ctx.adapter, ctx.host, ctx, ctx.scope, ctx.ret, ctx.index, schedulePromises, hydrationNodes);
|
|
2087
1996
|
if (getFlag(ctx.ret, IsUnmounted)) {
|
|
2088
1997
|
return;
|
|
2089
1998
|
}
|
|
2090
1999
|
addEventTargetDelegates(ctx.ctx, values);
|
|
2091
2000
|
// Execute schedule callbacks early to check for async deferral
|
|
2092
|
-
const
|
|
2001
|
+
const wasScheduling = getFlag(ctx.ret, IsScheduling);
|
|
2093
2002
|
let schedulePromises1;
|
|
2003
|
+
const callbacks = scheduleMap.get(ctx);
|
|
2094
2004
|
if (callbacks) {
|
|
2095
2005
|
scheduleMap.delete(ctx);
|
|
2096
|
-
// TODO: think about error handling for schedule callbacks
|
|
2097
2006
|
setFlag(ctx.ret, IsScheduling);
|
|
2098
2007
|
const result = ctx.adapter.read(unwrap(values));
|
|
2099
2008
|
for (const callback of callbacks) {
|
|
@@ -2104,7 +2013,7 @@ function commitComponent(ctx, schedulePromises, hydrationNodes) {
|
|
|
2104
2013
|
}
|
|
2105
2014
|
if (schedulePromises1 && !getFlag(ctx.ret, DidCommit)) {
|
|
2106
2015
|
const scheduleCallbacksP = Promise.all(schedulePromises1).then(() => {
|
|
2107
|
-
setFlag(ctx.ret, IsScheduling,
|
|
2016
|
+
setFlag(ctx.ret, IsScheduling, wasScheduling);
|
|
2108
2017
|
propagateComponent(ctx);
|
|
2109
2018
|
if (ctx.ret.fallback) {
|
|
2110
2019
|
unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
|
|
@@ -2144,6 +2053,37 @@ function commitComponent(ctx, schedulePromises, hydrationNodes) {
|
|
|
2144
2053
|
// if schedule callbacks call refresh() or async mounting is happening.
|
|
2145
2054
|
return getValue(ctx.ret, true);
|
|
2146
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Checks if a target retainer is active (contributing) in the host's retainer tree.
|
|
2058
|
+
* Performs a downward traversal from host to find if target is in the active path.
|
|
2059
|
+
*/
|
|
2060
|
+
function isRetainerActive(target, host) {
|
|
2061
|
+
const stack = [host];
|
|
2062
|
+
while (stack.length > 0) {
|
|
2063
|
+
const current = stack.pop();
|
|
2064
|
+
if (current === target) {
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
// Add direct children to stack (skip if this is a host boundary)
|
|
2068
|
+
// Host boundaries are: DOM elements (string tags) or Portal, but NOT Fragment
|
|
2069
|
+
const isHostBoundary = current !== host &&
|
|
2070
|
+
((typeof current.el.tag === "string" && current.el.tag !== Fragment) ||
|
|
2071
|
+
current.el.tag === Portal);
|
|
2072
|
+
if (current.children && !isHostBoundary) {
|
|
2073
|
+
const children = wrap(current.children);
|
|
2074
|
+
for (const child of children) {
|
|
2075
|
+
if (child) {
|
|
2076
|
+
stack.push(child);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
// Add fallback chains (only if current retainer is using fallback)
|
|
2081
|
+
if (current.fallback && !getFlag(current, DidDiff)) {
|
|
2082
|
+
stack.push(current.fallback);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2147
2087
|
/**
|
|
2148
2088
|
* Propagates component changes up to ancestors when rendering starts from a
|
|
2149
2089
|
* component via refresh() or multiple for await...of renders. This handles
|
|
@@ -2154,14 +2094,20 @@ function propagateComponent(ctx) {
|
|
|
2154
2094
|
const values = getChildValues(ctx.ret, ctx.index);
|
|
2155
2095
|
addEventTargetDelegates(ctx.ctx, values, (ctx1) => ctx1[_ContextState].host === ctx.host);
|
|
2156
2096
|
const host = ctx.host;
|
|
2097
|
+
const initiator = ctx.ret;
|
|
2098
|
+
// Check if initiator is active in the host's tree
|
|
2099
|
+
if (!isRetainerActive(initiator, host)) {
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2157
2102
|
const props = stripSpecialProps(host.el.props);
|
|
2103
|
+
const hostChildren = getChildValues(host, 0);
|
|
2158
2104
|
ctx.adapter.arrange({
|
|
2159
2105
|
tag: host.el.tag,
|
|
2160
2106
|
tagName: getTagName(host.el.tag),
|
|
2161
2107
|
node: host.value,
|
|
2162
2108
|
props,
|
|
2163
2109
|
oldProps: props,
|
|
2164
|
-
children:
|
|
2110
|
+
children: hostChildren,
|
|
2165
2111
|
});
|
|
2166
2112
|
flush(ctx.adapter, ctx.root, ctx);
|
|
2167
2113
|
}
|