1 | import { Component, toChildArray } from 'preact';
|
2 | import { suspended } from './suspense.js';
|
3 |
|
4 | // Indexes to linked list nodes (nodes are stored as arrays to save bytes).
|
5 | const SUSPENDED_COUNT = 0;
|
6 | const RESOLVED_COUNT = 1;
|
7 | const NEXT_NODE = 2;
|
8 |
|
9 | // Having custom inheritance instead of a class here saves a lot of bytes.
|
10 | export function SuspenseList() {
|
11 | this._next = null;
|
12 | this._map = null;
|
13 | }
|
14 |
|
15 | // Mark one of child's earlier suspensions as resolved.
|
16 | // Some pending callbacks may become callable due to this
|
17 | // (e.g. the last suspended descendant gets resolved when
|
18 | // revealOrder === 'together'). Process those callbacks as well.
|
19 | const resolve = (list, child, node) => {
|
20 | if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) {
|
21 | // The number a child (or any of its descendants) has been suspended
|
22 | // matches the number of times it's been resolved. Therefore we
|
23 | // mark the child as completely resolved by deleting it from ._map.
|
24 | // This is used to figure out when *all* children have been completely
|
25 | // resolved when revealOrder is 'together'.
|
26 | list._map.delete(child);
|
27 | }
|
28 |
|
29 | // If revealOrder is falsy then we can do an early exit, as the
|
30 | // callbacks won't get queued in the node anyway.
|
31 | // If revealOrder is 'together' then also do an early exit
|
32 | // if all suspended descendants have not yet been resolved.
|
33 | if (
|
34 | !list.props.revealOrder ||
|
35 | (list.props.revealOrder[0] === 't' && list._map.size)
|
36 | ) {
|
37 | return;
|
38 | }
|
39 |
|
40 | // Walk the currently suspended children in order, calling their
|
41 | // stored callbacks on the way. Stop if we encounter a child that
|
42 | // has not been completely resolved yet.
|
43 | node = list._next;
|
44 | while (node) {
|
45 | while (node.length > 3) {
|
46 | node.pop()();
|
47 | }
|
48 | if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) {
|
49 | break;
|
50 | }
|
51 | list._next = node = node[NEXT_NODE];
|
52 | }
|
53 | };
|
54 |
|
55 | // Things we do here to save some bytes but are not proper JS inheritance:
|
56 | // - call `new Component()` as the prototype
|
57 | // - do not set `Suspense.prototype.constructor` to `Suspense`
|
58 | SuspenseList.prototype = new Component();
|
59 |
|
60 | SuspenseList.prototype._suspended = function(child) {
|
61 | const list = this;
|
62 | const delegated = suspended(list._vnode);
|
63 |
|
64 | let node = list._map.get(child);
|
65 | node[SUSPENDED_COUNT]++;
|
66 |
|
67 | return unsuspend => {
|
68 | const wrappedUnsuspend = () => {
|
69 | if (!list.props.revealOrder) {
|
70 | // Special case the undefined (falsy) revealOrder, as there
|
71 | // is no need to coordinate a specific order or unsuspends.
|
72 | unsuspend();
|
73 | } else {
|
74 | node.push(unsuspend);
|
75 | resolve(list, child, node);
|
76 | }
|
77 | };
|
78 | if (delegated) {
|
79 | delegated(wrappedUnsuspend);
|
80 | } else {
|
81 | wrappedUnsuspend();
|
82 | }
|
83 | };
|
84 | };
|
85 |
|
86 | SuspenseList.prototype.render = function(props) {
|
87 | this._next = null;
|
88 | this._map = new Map();
|
89 |
|
90 | const children = toChildArray(props.children);
|
91 | if (props.revealOrder && props.revealOrder[0] === 'b') {
|
92 | // If order === 'backwards' (or, well, anything starting with a 'b')
|
93 | // then flip the child list around so that the last child will be
|
94 | // the first in the linked list.
|
95 | children.reverse();
|
96 | }
|
97 | // Build the linked list. Iterate through the children in reverse order
|
98 | // so that `_next` points to the first linked list node to be resolved.
|
99 | for (let i = children.length; i--; ) {
|
100 | // Create a new linked list node as an array of form:
|
101 | // [suspended_count, resolved_count, next_node]
|
102 | // where suspended_count and resolved_count are numeric counters for
|
103 | // keeping track how many times a node has been suspended and resolved.
|
104 | //
|
105 | // Note that suspended_count starts from 1 instead of 0, so we can block
|
106 | // processing callbacks until componentDidMount has been called. In a sense
|
107 | // node is suspended at least until componentDidMount gets called!
|
108 | //
|
109 | // Pending callbacks are added to the end of the node:
|
110 | // [suspended_count, resolved_count, next_node, callback_0, callback_1, ...]
|
111 | this._map.set(children[i], (this._next = [1, 0, this._next]));
|
112 | }
|
113 | return props.children;
|
114 | };
|
115 |
|
116 | SuspenseList.prototype.componentDidUpdate = SuspenseList.prototype.componentDidMount = function() {
|
117 | // Iterate through all children after mounting for two reasons:
|
118 | // 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases
|
119 | // each node[RELEASED_COUNT] by 1, therefore balancing the counters.
|
120 | // The nodes can now be completely consumed from the linked list.
|
121 | // 2. Handle nodes that might have gotten resolved between render and
|
122 | // componentDidMount.
|
123 | this._map.forEach((node, child) => {
|
124 | resolve(this, child, node);
|
125 | });
|
126 | };
|