1
+/* eslint-disable dot-notation */
2
+
3
+const COMMENT_NODE = 8;
4
+const SUSPENSE_START_DATA = '$';
5
+const SUSPENSE_END_DATA = '/$';
6
+const SUSPENSE_PENDING_START_DATA = '$?';
7
+const SUSPENSE_FALLBACK_START_DATA = '$!';
8
+const LOADED = 'l';
9
+const ERRORED = 'e';
10
+
11
+// TODO: Symbols that are referenced outside this module use dynamic accessor
12
+// notation instead of dot notation to prevent Closure's advanced compilation
13
+// mode from renaming. We could use extern files instead, but I couldn't get it
14
+// working. Closure converts it to a dot access anyway, though, so it's not an
15
+// urgent issue.
16
+
17
+export function clientRenderBoundary(
18
+ suspenseBoundaryID,
19
+ errorDigest,
20
+ errorMsg,
21
+ errorComponentStack,
22
+) {
23
+ // Find the fallback's first element.
24
+ const suspenseIdNode = document.getElementById(suspenseBoundaryID);
25
+ if (!suspenseIdNode) {
26
+ // The user must have already navigated away from this tree.
27
+ // E.g. because the parent was hydrated.
28
+ return;
29
+ }
30
+ // Find the boundary around the fallback. This is always the previous node.
31
+ const suspenseNode = suspenseIdNode.previousSibling;
32
+ // Tag it to be client rendered.
33
+ suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
34
+ // assign error metadata to first sibling
35
+ const dataset = suspenseIdNode.dataset;
36
+ if (errorDigest) dataset['dgst'] = errorDigest;
37
+ if (errorMsg) dataset['msg'] = errorMsg;
38
+ if (errorComponentStack) dataset['stck'] = errorComponentStack;
39
+ // Tell React to retry it if the parent already hydrated.
40
+ if (suspenseNode['_reactRetry']) {
41
+ suspenseNode['_reactRetry']();
42
+ }
43
+}
44
+
45
+export function completeBoundaryWithStyles(
46
+ suspenseBoundaryID,
47
+ contentID,
48
+ styles,
49
+) {
50
+ // TODO: In the non-inline version of the runtime, these don't need to be read
51
+ // from the global scope.
52
+ const completeBoundaryImpl = window['$RC'];
53
+ const resourceMap = window['$RM'];
54
+
55
+ const precedences = new Map();
56
+ const thisDocument = document;
57
+ let lastResource, node;
58
+
59
+ // Seed the precedence list with existing resources
60
+ const nodes = thisDocument.querySelectorAll('link[data-rprec]');
61
+ for (let i = 0; (node = nodes[i++]); ) {
62
+ precedences.set(node.dataset['rprec'], (lastResource = node));
63
+ }
64
+
65
+ let i = 0;
66
+ const dependencies = [];
67
+ let style, href, precedence, attr, loadingState, resourceEl;
68
+
69
+ function setStatus(s) {
70
+ this['s'] = s;
71
+ }
72
+
73
+ while ((style = styles[i++])) {
74
+ let j = 0;
75
+ href = style[j++];
76
+ // We check if this resource is already in our resourceMap and reuse it if so.
77
+ // If it is already loaded we don't return it as a depenendency since there is nothing
78
+ // to wait for
79
+ loadingState = resourceMap.get(href);
80
+ if (loadingState) {
81
+ if (loadingState['s'] !== 'l') {
82
+ dependencies.push(loadingState);
83
+ }
84
+ continue;
85
+ }
86
+
87
+ // We construct our new resource element, looping over remaining attributes if any
88
+ // setting them to the Element.
89
+ resourceEl = thisDocument.createElement('link');
90
+ resourceEl.href = href;
91
+ resourceEl.rel = 'stylesheet';
92
+ resourceEl.dataset['rprec'] = precedence = style[j++];
93
+ while ((attr = style[j++])) {
94
+ resourceEl.setAttribute(attr, style[j++]);
95
+ }
96
+
97
+ // We stash a pending promise in our map by href which will resolve or reject
98
+ // when the underlying resource loads or errors. We add it to the dependencies
99
+ // array to be returned.
100
+ loadingState = resourceEl['_p'] = new Promise((re, rj) => {
101
+ resourceEl.onload = re;
102
+ resourceEl.onerror = rj;
103
+ });
104
+ loadingState.then(
105
+ setStatus.bind(loadingState, LOADED),
106
+ setStatus.bind(loadingState, ERRORED),
107
+ );
108
+ resourceMap.set(href, loadingState);
109
+ dependencies.push(loadingState);
110
+
111
+ // The prior style resource is the last one placed at a given
112
+ // precedence or the last resource itself which may be null.
113
+ // We grab this value and then update the last resource for this
114
+ // precedence to be the inserted element, updating the lastResource
115
+ // pointer if needed.
116
+ const prior = precedences.get(precedence) || lastResource;
117
+ if (prior === lastResource) {
118
+ lastResource = resourceEl;
119
+ }
120
+ precedences.set(precedence, resourceEl);
121
+
122
+ // Finally, we insert the newly constructed instance at an appropriate location
123
+ // in the Document.
124
+ if (prior) {
125
+ prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
126
+ } else {
127
+ const head = thisDocument.head;
128
+ head.insertBefore(resourceEl, head.firstChild);
129
+ }
130
+ }
131
+
132
+ Promise.all(dependencies).then(
133
+ completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''),
134
+ completeBoundaryImpl.bind(
135
+ null,
136
+ suspenseBoundaryID,
137
+ contentID,
138
+ 'Resource failed to load',
139
+ ),
140
+ );
141
+}
142
+
143
+export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
144
+ const contentNode = document.getElementById(contentID);
145
+ // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
146
+ // This might also help by not causing recalcing each time we move a child from here to the target.
147
+ contentNode.parentNode.removeChild(contentNode);
148
+
149
+ // Find the fallback's first element.
150
+ const suspenseIdNode = document.getElementById(suspenseBoundaryID);
151
+ if (!suspenseIdNode) {
152
+ // The user must have already navigated away from this tree.
153
+ // E.g. because the parent was hydrated. That's fine there's nothing to do
154
+ // but we have to make sure that we already deleted the container node.
155
+ return;
156
+ }
157
+ // Find the boundary around the fallback. This is always the previous node.
158
+ const suspenseNode = suspenseIdNode.previousSibling;
159
+
160
+ if (!errorDigest) {
161
+ // Clear all the existing children. This is complicated because
162
+ // there can be embedded Suspense boundaries in the fallback.
163
+ // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
164
+ // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
165
+ // They never hydrate anyway. However, currently we support incrementally loading the fallback.
166
+ const parentInstance = suspenseNode.parentNode;
167
+ let node = suspenseNode.nextSibling;
168
+ let depth = 0;
169
+ do {
170
+ if (node && node.nodeType === COMMENT_NODE) {
171
+ const data = node.data;
172
+ if (data === SUSPENSE_END_DATA) {
173
+ if (depth === 0) {
174
+ break;
175
+ } else {
176
+ depth--;
177
+ }
178
+ } else if (
179
+ data === SUSPENSE_START_DATA ||
180
+ data === SUSPENSE_PENDING_START_DATA ||
181
+ data === SUSPENSE_FALLBACK_START_DATA
182
+ ) {
183
+ depth++;
184
+ }
185
+ }
186
+
187
+ const nextNode = node.nextSibling;
188
+ parentInstance.removeChild(node);
189
+ node = nextNode;
190
+ } while (node);
191
+
192
+ const endOfBoundary = node;
193
+
194
+ // Insert all the children from the contentNode between the start and end of suspense boundary.
195
+ while (contentNode.firstChild) {
196
+ parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
197
+ }
198
+
199
+ suspenseNode.data = SUSPENSE_START_DATA;
200
+ } else {
201
+ suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
202
+ suspenseIdNode.setAttribute('data-dgst', errorDigest);
203
+ }
204
+
205
+ if (suspenseNode['_reactRetry']) {
206
+ suspenseNode['_reactRetry']();
207
+ }
208
+}
209
+
210
+export function completeSegment(containerID, placeholderID) {
211
+ const segmentContainer = document.getElementById(containerID);
212
+ const placeholderNode = document.getElementById(placeholderID);
213
+ // We always expect both nodes to exist here because, while we might
214
+ // have navigated away from the main tree, we still expect the detached
215
+ // tree to exist.
216
+ segmentContainer.parentNode.removeChild(segmentContainer);
217
+ while (segmentContainer.firstChild) {
218
+ placeholderNode.parentNode.insertBefore(
219
+ segmentContainer.firstChild,
220
+ placeholderNode,
221
+ );
222
+ }
223
+ placeholderNode.parentNode.removeChild(placeholderNode);
224
+}