@uniflowed/router 0.0.0-alpha.9 → 0.2.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.
Files changed (51) hide show
  1. package/action.js +344 -0
  2. package/client.js +263 -7
  3. package/handler.js +113 -99
  4. package/http-client.js +104 -0
  5. package/index.js +49 -6
  6. package/instrumentation.js +92 -0
  7. package/internal/action-endpoint.js +646 -0
  8. package/internal/action-wire.js +608 -0
  9. package/internal/base-path.js +175 -0
  10. package/internal/boundaries.js +481 -0
  11. package/internal/boundary-data.js +88 -0
  12. package/internal/compose.js +490 -0
  13. package/internal/deployment.js +160 -0
  14. package/internal/devtools.js +131 -0
  15. package/internal/diagnostics.js +169 -0
  16. package/internal/error-view.js +193 -0
  17. package/internal/flight-browser.js +242 -0
  18. package/internal/flight-chunks.js +205 -0
  19. package/internal/flight-rows.js +135 -0
  20. package/internal/flight-ssr.js +91 -0
  21. package/internal/flight.js +192 -0
  22. package/internal/form-action.js +243 -0
  23. package/internal/head.js +219 -0
  24. package/internal/hydrate-options.js +38 -0
  25. package/internal/hydration.js +1085 -0
  26. package/internal/inspector.js +626 -0
  27. package/internal/native-links.js +67 -0
  28. package/internal/native-tree.js +89 -0
  29. package/internal/navigation-cache.js +181 -0
  30. package/internal/payload-rows.js +270 -0
  31. package/internal/payload.js +685 -0
  32. package/internal/prepare-document.js +54 -0
  33. package/internal/react-version.js +77 -0
  34. package/internal/resolve.js +1617 -0
  35. package/internal/resolved-summary.js +199 -0
  36. package/internal/routing.js +478 -0
  37. package/internal/runtime.js +1593 -1341
  38. package/internal/server-instrumentation.js +12 -0
  39. package/internal/server-route.js +58 -0
  40. package/internal/shell.js +132 -0
  41. package/internal/stream.js +766 -21
  42. package/middleware.js +274 -22
  43. package/native-navigation.js +217 -0
  44. package/native.js +416 -0
  45. package/package.json +48 -7
  46. package/routing.js +51 -0
  47. package/rsc-client.js +122 -0
  48. package/rsc-ssr.js +641 -0
  49. package/rsc.js +402 -0
  50. package/server-components.js +159 -0
  51. package/server.js +263 -106
@@ -0,0 +1,685 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/router`: the payload, and what may be in it.
4
+ //
5
+ // A document uf streams has always carried one thing the browser reads back:
6
+ // the loader's answer, in `<script id="__uf_data" type="application/json">`.
7
+ // That element is written once, from a value that is complete by the time it
8
+ // is written, so everything in it had to have resolved before the byte after
9
+ // it could be sent. A page whose data is one slow thing and four fast ones
10
+ // therefore waited for the slow one to say anything about the other four, and
11
+ // a promise anywhere in that value was `JSON.stringify`'d to `{}` — silently,
12
+ // which is the worse half.
13
+ //
14
+ // This module is the format that stops it waiting. It is **Flight-shaped** in
15
+ // the one sense that matters: a payload is not a value, it is a sequence of
16
+ // numbered *rows*, the first of which may name rows that have not been written
17
+ // yet. Row 0 is the model — the loader's answer with each unresolved value
18
+ // replaced by a reference — and each later row is one of those values,
19
+ // arriving in whatever order it resolved in, in a `<script>` React streams
20
+ // into the document at the moment it settles.
21
+ //
22
+ // 0 {"user":{"name":"ada"},"comments":"$P1","related":"$P2"}
23
+ // 2 {"value":[{"id":7}]}
24
+ // 1 {"value":[{"body":"…"}]}
25
+ //
26
+ // Two rows out of order, because row 2 resolved first, and that is the whole
27
+ // point: the reader has the page and the user's name while the comments are
28
+ // still being fetched, and the boundary around the comments resolves on its
29
+ // own rather than behind the slowest thing on the page.
30
+ //
31
+ // # One reference kind, and the reason there is only one
32
+ //
33
+ // `./action-wire.js` says, under "What is deliberately absent", that React's
34
+ // Flight payload can carry a reference to a client module, a promise, or an
35
+ // element, and that "a decoder that reconstructs those is a decoder that
36
+ // constructs attacker-chosen objects" — so the action grammar has no reference
37
+ // format and "this grammar is not the place to grow one quietly". This is the
38
+ // place, and it grows exactly one:
39
+ //
40
+ // `"$P<n>"` the value written by row `n` of this same payload.
41
+ //
42
+ // A row reference names a *position in this payload*, and it is resolved by
43
+ // this module's own reader with a promise this module made. Nothing in a
44
+ // payload names a constructor, a module, a function, an export or a class, and
45
+ // no string in one is looked up in any registry. That is the difference
46
+ // between the reference this format has and the two it does not: a module
47
+ // reference and an element reference are decoded by *calling* something the
48
+ // payload named, and the payload the client re-renders a *tree* from needs
49
+ // both. Those wait for ubugeeei-prod/uf#519's other half, which needs a second
50
+ // module graph before it needs a format. This half is the framing and the
51
+ // streaming, and it is written so that adding a tag later is a change to a
52
+ // closed list rather than to a decoder that already passes strings through:
53
+ // an unrecognised `$` string is **refused**, because a decoder that ignored
54
+ // `"$X1"` today would accept it silently on the day `$X` means something.
55
+ //
56
+ // # What the model may hold is unchanged, and that is deliberate
57
+ //
58
+ // Everything else in a payload is what `JSON.stringify` carries, because that
59
+ // is what the loader data element has always been. Narrowing it to the closed
60
+ // grammar `./action-wire.js` applies to a server action would be a change to a
61
+ // contract this format is not about — and it would be a change made in a
62
+ // throw, inside a loader, on somebody whose page worked yesterday. An action's
63
+ // arguments arrive from the network and are somebody else's bytes; a loader's
64
+ // answer is the application's own value on its way out, and the two do not
65
+ // need the same answer.
66
+ //
67
+ // So the encoder does exactly two things: it replaces promises with references
68
+ // and it escapes strings that would read as one. A `Date`, a class with a
69
+ // `toJSON`, an `undefined` property, a `NaN` — each crosses exactly as it did
70
+ // before this file existed, which is to say as `JSON.stringify` renders it.
71
+ // The one shape that is outside this and says so is a cycle: the walk is
72
+ // bounded by [`MAX_PAYLOAD_DEPTH`], and `JSON.stringify` threw on one anyway.
73
+ //
74
+ // The escape is applied to every string the walk *reaches*, which is every
75
+ // string inside a plain object or an array. A string that only appears because
76
+ // something's `toJSON` produced it is not reached and not escaped — so a
77
+ // `toJSON` returning a string that begins with `$` is the one value this
78
+ // format cannot round-trip, and it is named here rather than left to be found.
79
+ //
80
+ // # Nothing changes for a payload with nothing deferred
81
+ //
82
+ // Both walks answer with the value they were handed when there is nothing to
83
+ // do — no promise anywhere, no string starting with `$`. So a page whose
84
+ // loader returns ordinary data produces the same bytes it produced before this
85
+ // module existed, and the browser hands the application the object
86
+ // `JSON.parse` made rather than a copy of it. That is worth more than the
87
+ // walk it saves: it is what makes "the payload changed nothing here" a
88
+ // property rather than a hope.
89
+ //
90
+ // # Why both sides run this file
91
+ //
92
+ // The same reason `./action-wire.js` gives: two implementations of one grammar
93
+ // is how the two come to disagree. Here it is load-bearing beyond that. The
94
+ // row `<script>` is rendered by React *on both sides* — the server writes it
95
+ // from the promise it resolved, the browser writes it from the value it read
96
+ // out of that same element — so the two renders have to produce the same bytes
97
+ // or the page is a hydration mismatch. They do, because both call
98
+ // [`payloadJson`] on a value that has been through one `JSON.parse`, and
99
+ // `JSON.stringify` is stable over its own output.
100
+ //
101
+ // Pure: no imports, no platform APIs beyond `JSON`, so the browser's half of
102
+ // `@uniflowed/router` can reach it without reaching anything server-only. The
103
+ // document half — finding the row elements and noticing the ones that have not
104
+ // arrived yet — is `./payload-rows.js`, which is the only part that needs a DOM.
105
+
106
+ /** The attribute naming a late row's script element. */
107
+ export const PAYLOAD_ROW_ATTRIBUTE: string = "data-uf-row";
108
+
109
+ /**
110
+ * The prefix every reference in this format starts with.
111
+ *
112
+ * One character, so that escaping a string that starts with it costs one
113
+ * character too. React's own Flight payload uses the same one, and a format
114
+ * that reads as Flight does to somebody who knows Flight is worth more than a
115
+ * prefix nobody has ever seen.
116
+ */
117
+ const REFERENCE_PREFIX = "$";
118
+
119
+ /** The tag that makes a reference a row reference. `$P1` is "the value of row 1". */
120
+ const ROW_TAG = "P";
121
+
122
+ /**
123
+ * Most rows one payload may have.
124
+ *
125
+ * A ceiling on the number of `<script>` elements one document can be made to
126
+ * carry, and on the number of promises the browser holds open waiting for
127
+ * them. A loader that defers more than this is a loader that wants one request
128
+ * per thing rather than one page.
129
+ */
130
+ export const MAX_PAYLOAD_ROWS: number = 64;
131
+
132
+ /**
133
+ * Deepest nesting either walk will follow.
134
+ *
135
+ * A guard rather than a policy. `JSON.stringify` refuses a cycle by throwing,
136
+ * and the walks below would follow one forever, so the depth is what keeps the
137
+ * two failing the same way. Deep enough that no data shaped like data reaches
138
+ * it: `MAX_ACTION_DEPTH` is 24, and this is not the place to be stricter than
139
+ * the wire an application already has.
140
+ */
141
+ export const MAX_PAYLOAD_DEPTH: number = 64;
142
+
143
+ /** A value that could not be encoded, and where in the loader's answer it was. */
144
+ export class PayloadValueError extends Error {
145
+ /** Where the offending value sat, e.g. `data.filters`. */
146
+ path: string;
147
+
148
+ constructor(path: string, reason: string) {
149
+ super(`@uniflowed/router: ${path} ${reason}.`);
150
+ this.name = "PayloadValueError";
151
+ this.path = path;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * A row that said the value failed, carried back as a rejection.
157
+ *
158
+ * The class exists so the reading side can rewrite the row it read without
159
+ * having to guess. A row is rendered by React on *both* sides, so whatever the
160
+ * browser puts in the element has to be what the server put there — and the
161
+ * server's choice of words depends on whether it is a development build. This
162
+ * carries the row's own text past that decision: the browser echoes `wire`
163
+ * rather than deciding again, and the two agree whichever build wrote which.
164
+ */
165
+ export class PayloadRowError extends Error {
166
+ /** Exactly what the row's `error` said. */
167
+ wire: string;
168
+
169
+ constructor(wire: string) {
170
+ super(wire);
171
+ this.name = "PayloadRowError";
172
+ this.wire = wire;
173
+ }
174
+ }
175
+
176
+ /** One value the model referred to and that has not resolved yet. */
177
+ export type PendingRow = {|
178
+ /** Row number, 1-based; row 0 is the model itself. */
179
+ readonly id: number,
180
+ /** The promise whose settlement writes the row. */
181
+ readonly value: Promise<mixed>,
182
+ |};
183
+
184
+ /** The model, and every row it named. */
185
+ export type EncodedPayload = {|
186
+ /** Row 0: the value with each promise replaced by a `"$P<n>"` reference. */
187
+ readonly model: mixed,
188
+ /** The promises those references name, in ascending id order. */
189
+ readonly rows: $ReadOnlyArray<PendingRow>,
190
+ |};
191
+
192
+ /**
193
+ * What one late row's script says.
194
+ *
195
+ * Two shapes rather than one so that a rejected promise is a rejected promise
196
+ * on the other side too, instead of a value that never arrives and a boundary
197
+ * that spins. What is in the message is decided by the caller rather than here
198
+ * — see `internal/runtime.js`, which sends the server's own words only where
199
+ * `import.meta.hot` says a developer is reading them.
200
+ *
201
+ * One exact object with two optional fields rather than a union of two, and
202
+ * the reason is `JSON.stringify`: the value that gets written has to have
203
+ * exactly one key in it, so the type the writer holds has to be the one that
204
+ * can express either. [`parseRowMessage`] enforces "exactly one" on the way
205
+ * back in, which is the direction where it is a claim about somebody else's
206
+ * bytes rather than about uf's own.
207
+ */
208
+ export type PayloadRowMessage = {|
209
+ readonly value?: mixed,
210
+ readonly error?: string,
211
+ |};
212
+
213
+ /**
214
+ * The prototype every object literal has, captured rather than named.
215
+ *
216
+ * `./action-wire.js` explains the choice; the same one is made here so the two
217
+ * agree about what a plain object is. Only a plain object and an array are
218
+ * walked into — everything else is `JSON.stringify`'s business, as it was.
219
+ */
220
+ const PLAIN_PROTOTYPE: mixed = Object.getPrototypeOf({});
221
+
222
+ function isPlainObject(value: mixed): boolean {
223
+ if (value == null || typeof value !== "object") {
224
+ return false;
225
+ }
226
+ const prototype: mixed = Object.getPrototypeOf(value);
227
+ return prototype === PLAIN_PROTOTYPE || prototype === null;
228
+ }
229
+
230
+ /**
231
+ * The value as a promise, or `null`.
232
+ *
233
+ * A `then` method and nothing else, which is the one duck-type this file makes
234
+ * and the one it has to: a loader's promise may come from a different realm
235
+ * than the one this module was loaded in, and `instanceof Promise` is false
236
+ * across realms. It is safe in a way it would not be in `./action-wire.js`
237
+ * because a thenable here is never *reconstructed* — it is awaited, and what
238
+ * it produces is written by `JSON.stringify` like any other value.
239
+ */
240
+ function asThenable(value: mixed): Promise<mixed> | null {
241
+ if (value == null || (typeof value !== "object" && typeof value !== "function")) {
242
+ return null;
243
+ }
244
+ const then: mixed = (value: $FlowFixMe).then;
245
+ return typeof then === "function" ? (value: $FlowFixMe) : null;
246
+ }
247
+
248
+ /** Whether a string would be read as a reference and so has to be escaped. */
249
+ function isReferenceLike(value: mixed): boolean {
250
+ return typeof value === "string" && value.startsWith(REFERENCE_PREFIX);
251
+ }
252
+
253
+ /**
254
+ * Write a key without letting `__proto__` mean what assignment makes it mean.
255
+ *
256
+ * `JSON.parse` gives `__proto__` an *own* data property; `target[key] = value`
257
+ * calls the setter on `Object.prototype` and changes the object's prototype
258
+ * instead. Rebuilding a parsed object with plain assignment would therefore
259
+ * turn a payload that was inert into prototype pollution — a hazard these
260
+ * walks would introduce rather than inherit. `defineProperty` is what
261
+ * `JSON.parse` does, spelled out.
262
+ */
263
+ function setKey(target: { [string]: mixed }, key: string, value: mixed): void {
264
+ if (key === "__proto__") {
265
+ Object.defineProperty(target, key, {
266
+ value,
267
+ writable: true,
268
+ enumerable: true,
269
+ configurable: true,
270
+ });
271
+ return;
272
+ }
273
+ target[key] = value;
274
+ }
275
+
276
+ /** One entry of an explicit walk stack, with the slot to write the result into. */
277
+ type Frame = {|
278
+ readonly value: mixed,
279
+ readonly path: string,
280
+ readonly depth: number,
281
+ readonly emit: (encoded: mixed) => void,
282
+ |};
283
+
284
+ /**
285
+ * Whether anything in `root` needs encoding at all.
286
+ *
287
+ * A promise, or a string that would read as a reference. Answering `false`
288
+ * here is what lets [`encodePayload`] hand back the value it was given — see
289
+ * the header on why that matters more than the walk it saves.
290
+ *
291
+ * An explicit stack rather than recursion, for the reason `checkActionValue`
292
+ * gives: nesting is the application's choice, and a recursive walk over it is
293
+ * a stack overflow with somebody's hand on the depth.
294
+ */
295
+ function needsEncoding(root: mixed, label: string): boolean {
296
+ const stack: Array<{| value: mixed, path: string, depth: number |}> = [
297
+ { value: root, path: label, depth: 0 },
298
+ ];
299
+ while (stack.length > 0) {
300
+ const frame = stack.pop();
301
+ if (frame == null) {
302
+ break;
303
+ }
304
+ const { value, path, depth } = frame;
305
+ if (depth > MAX_PAYLOAD_DEPTH) {
306
+ throw new PayloadValueError(path, `is nested deeper than ${String(MAX_PAYLOAD_DEPTH)}`);
307
+ }
308
+ if (isReferenceLike(value) || asThenable(value) != null) {
309
+ return true;
310
+ }
311
+ if (Array.isArray(value)) {
312
+ for (let index = 0; index < value.length; index += 1) {
313
+ stack.push({ value: value[index], path: `${path}[${String(index)}]`, depth: depth + 1 });
314
+ }
315
+ continue;
316
+ }
317
+ if (isPlainObject(value)) {
318
+ const source: { +[string]: mixed } = (value: $FlowFixMe);
319
+ for (const key of Object.keys(source)) {
320
+ stack.push({ value: source[key], path: `${path}.${key}`, depth: depth + 1 });
321
+ }
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+
327
+ /**
328
+ * Split a value into the model that goes out now and the rows that follow.
329
+ *
330
+ * The walk is **ordered**: children are visited in array order and in
331
+ * `Object.keys` order, so the id a promise gets is a function of where it sits
332
+ * in the model and of nothing else.
333
+ *
334
+ * That determinism is what lets the browser encode the same model to the same
335
+ * bytes, which is what lets both sides render the row `<script>`. It holds
336
+ * across a `JSON.parse` because parsing preserves both orders, and it is why
337
+ * ids come from this walk rather than from the order the promises were created
338
+ * in — which is a fact about the loader, not about the data, and would differ
339
+ * between the two sides.
340
+ */
341
+ export function encodePayload(root: mixed, label: string): EncodedPayload {
342
+ if (!needsEncoding(root, label)) {
343
+ return { model: root, rows: [] };
344
+ }
345
+
346
+ const rows: Array<PendingRow> = [];
347
+ let model: mixed = null;
348
+ const stack: Array<Frame> = [
349
+ {
350
+ value: root,
351
+ path: label,
352
+ depth: 0,
353
+ emit: (encoded) => {
354
+ model = encoded;
355
+ },
356
+ },
357
+ ];
358
+
359
+ while (stack.length > 0) {
360
+ const frame = stack.pop();
361
+ if (frame == null) {
362
+ break;
363
+ }
364
+ const { value, path, depth, emit } = frame;
365
+ if (depth > MAX_PAYLOAD_DEPTH) {
366
+ throw new PayloadValueError(path, `is nested deeper than ${String(MAX_PAYLOAD_DEPTH)}`);
367
+ }
368
+
369
+ const thenable = asThenable(value);
370
+ if (thenable != null) {
371
+ if (rows.length >= MAX_PAYLOAD_ROWS) {
372
+ throw new PayloadValueError(label, `defers more than ${String(MAX_PAYLOAD_ROWS)} values`);
373
+ }
374
+ const id = rows.length + 1;
375
+ rows.push({ id, value: thenable });
376
+ emit(`${REFERENCE_PREFIX}${ROW_TAG}${String(id)}`);
377
+ continue;
378
+ }
379
+ if (isReferenceLike(value)) {
380
+ emit(REFERENCE_PREFIX + String(value));
381
+ continue;
382
+ }
383
+ if (Array.isArray(value)) {
384
+ const encoded: Array<mixed> = new Array(value.length).fill(null);
385
+ emit(encoded);
386
+ // Pushed in reverse so that popping visits index 0 first: the id a
387
+ // promise is given has to follow the order the payload reads in.
388
+ for (let index = value.length - 1; index >= 0; index -= 1) {
389
+ stack.push({
390
+ value: value[index],
391
+ path: `${path}[${String(index)}]`,
392
+ depth: depth + 1,
393
+ emit: (child) => {
394
+ encoded[index] = child;
395
+ },
396
+ });
397
+ }
398
+ continue;
399
+ }
400
+ if (!isPlainObject(value)) {
401
+ // A `Date`, a `Map`, a class instance, a number, `undefined`: whatever
402
+ // `JSON.stringify` would have made of it, unchanged. See the header.
403
+ emit(value);
404
+ continue;
405
+ }
406
+ const source: { +[string]: mixed } = (value: $FlowFixMe);
407
+ const encoded: { [string]: mixed } = {};
408
+ emit(encoded);
409
+ const keys = Object.keys(source);
410
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
411
+ const key = keys[index];
412
+ stack.push({
413
+ value: source[key],
414
+ path: `${path}.${key}`,
415
+ depth: depth + 1,
416
+ emit: (child) => {
417
+ setKey(encoded, key, child);
418
+ },
419
+ });
420
+ }
421
+ }
422
+
423
+ return { model, rows };
424
+ }
425
+
426
+ /**
427
+ * Encode what one row resolved to.
428
+ *
429
+ * The same walk, and then the one rule a row has that the model does not: it
430
+ * may not itself defer. Row ids are handed out by a single walk of the model,
431
+ * which is the only structure both sides hold before anything has resolved, so
432
+ * a row that could add ids as it settled would be a numbering that depends on
433
+ * the order things finished in — and the browser, which re-renders the row
434
+ * element from the value it read, would number them differently.
435
+ *
436
+ * Reusing the encoder and refusing the rows it found is the shortest honest
437
+ * spelling: one walk, one escape rule, and no second implementation to drift
438
+ * from the first.
439
+ */
440
+ export function encodeRowValue(root: mixed, label: string): mixed {
441
+ const { model, rows } = encodePayload(root, label);
442
+ if (rows.length > 0) {
443
+ throw new PayloadValueError(label, "resolved to a value that is itself deferred");
444
+ }
445
+ return model;
446
+ }
447
+
448
+ /** How a decoder is told what a reference stands for. */
449
+ export type RowResolver = (id: number) => mixed;
450
+
451
+ /**
452
+ * Rebuild a value from a model, with every reference replaced.
453
+ *
454
+ * `resolve` is handed a row id and answers with whatever should stand in the
455
+ * slot — a promise, on the reading side, which is the whole point. A callback
456
+ * rather than a map, so a reader can create the promise on first sight of a
457
+ * reference and never for a row nothing refers to.
458
+ *
459
+ * A model with no `$` string in it is handed straight back, so the application
460
+ * gets the object `JSON.parse` made. See the header.
461
+ */
462
+ export function decodePayload(root: mixed, resolve: RowResolver, label: string): mixed {
463
+ if (!hasReference(root, label)) {
464
+ return root;
465
+ }
466
+
467
+ let out: mixed = null;
468
+ const stack: Array<Frame> = [
469
+ {
470
+ value: root,
471
+ path: label,
472
+ depth: 0,
473
+ emit: (value) => {
474
+ out = value;
475
+ },
476
+ },
477
+ ];
478
+
479
+ while (stack.length > 0) {
480
+ const frame = stack.pop();
481
+ if (frame == null) {
482
+ break;
483
+ }
484
+ const { value, path, depth, emit } = frame;
485
+ if (depth > MAX_PAYLOAD_DEPTH) {
486
+ throw new PayloadValueError(path, `is nested deeper than ${String(MAX_PAYLOAD_DEPTH)}`);
487
+ }
488
+
489
+ if (isReferenceLike(value)) {
490
+ emit(decodeReference(String(value), path, resolve));
491
+ continue;
492
+ }
493
+ if (Array.isArray(value)) {
494
+ const rebuilt: Array<mixed> = new Array(value.length).fill(null);
495
+ emit(rebuilt);
496
+ for (let index = value.length - 1; index >= 0; index -= 1) {
497
+ stack.push({
498
+ value: value[index],
499
+ path: `${path}[${String(index)}]`,
500
+ depth: depth + 1,
501
+ emit: (child) => {
502
+ rebuilt[index] = child;
503
+ },
504
+ });
505
+ }
506
+ continue;
507
+ }
508
+ if (!isPlainObject(value)) {
509
+ emit(value);
510
+ continue;
511
+ }
512
+ const source: { +[string]: mixed } = (value: $FlowFixMe);
513
+ const rebuilt: { [string]: mixed } = {};
514
+ emit(rebuilt);
515
+ const keys = Object.keys(source);
516
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
517
+ const key = keys[index];
518
+ stack.push({
519
+ value: source[key],
520
+ path: `${path}.${key}`,
521
+ depth: depth + 1,
522
+ emit: (child) => {
523
+ setKey(rebuilt, key, child);
524
+ },
525
+ });
526
+ }
527
+ }
528
+
529
+ return out;
530
+ }
531
+
532
+ /** Whether a model holds anything the decoder has to look at. */
533
+ function hasReference(root: mixed, label: string): boolean {
534
+ const stack: Array<{| value: mixed, path: string, depth: number |}> = [
535
+ { value: root, path: label, depth: 0 },
536
+ ];
537
+ while (stack.length > 0) {
538
+ const frame = stack.pop();
539
+ if (frame == null) {
540
+ break;
541
+ }
542
+ const { value, path, depth } = frame;
543
+ if (depth > MAX_PAYLOAD_DEPTH) {
544
+ throw new PayloadValueError(path, `is nested deeper than ${String(MAX_PAYLOAD_DEPTH)}`);
545
+ }
546
+ if (isReferenceLike(value)) {
547
+ return true;
548
+ }
549
+ if (Array.isArray(value)) {
550
+ for (let index = 0; index < value.length; index += 1) {
551
+ stack.push({ value: value[index], path: `${path}[${String(index)}]`, depth: depth + 1 });
552
+ }
553
+ continue;
554
+ }
555
+ if (isPlainObject(value)) {
556
+ const source: { +[string]: mixed } = (value: $FlowFixMe);
557
+ for (const key of Object.keys(source)) {
558
+ stack.push({ value: source[key], path: `${path}.${key}`, depth: depth + 1 });
559
+ }
560
+ }
561
+ }
562
+ return false;
563
+ }
564
+
565
+ /**
566
+ * One `$` string: an escaped literal, or a row reference.
567
+ *
568
+ * The unrecognised case throws. See the header: a decoder that passed `"$X1"`
569
+ * through would be one that accepts a tag it does not implement, from a
570
+ * payload it did not write.
571
+ */
572
+ function decodeReference(value: string, path: string, resolve: RowResolver): mixed {
573
+ if (value.startsWith(REFERENCE_PREFIX + REFERENCE_PREFIX)) {
574
+ return value.slice(1);
575
+ }
576
+ const id = rowId(value);
577
+ if (id == null) {
578
+ throw new PayloadValueError(path, "names a reference this payload format has no tag for");
579
+ }
580
+ return resolve(id);
581
+ }
582
+
583
+ /**
584
+ * The row id carried by a streamed row element, or `null` when the attribute
585
+ * is not one.
586
+ *
587
+ * This is the same grammar as the `$P<n>` reference without the `$P` tag:
588
+ * digits only, in range. The browser reads row elements from a live document,
589
+ * so accepting `Number.parseInt`'s looser spellings would let `1x` satisfy the
590
+ * row the model named as `$P1`.
591
+ */
592
+ export function payloadRowId(value: string): number | null {
593
+ return parsePayloadRowId(value);
594
+ }
595
+
596
+ /**
597
+ * The row a reference names, or `null` when the string is not one.
598
+ *
599
+ * Digits only, and read by hand rather than with `Number`, which accepts
600
+ * `"1e3"`, `" 1"`, `"0x1"` and `"Infinity"` — four spellings of a row id this
601
+ * format does not have.
602
+ */
603
+ function rowId(value: string): number | null {
604
+ if (!value.startsWith(REFERENCE_PREFIX + ROW_TAG)) {
605
+ return null;
606
+ }
607
+ return parsePayloadRowId(value.slice(2));
608
+ }
609
+
610
+ function parsePayloadRowId(digits: string): number | null {
611
+ if (digits.length === 0 || digits.length > 3) {
612
+ return null;
613
+ }
614
+ for (let index = 0; index < digits.length; index += 1) {
615
+ const code = digits.charCodeAt(index);
616
+ if (code < 48 || code > 57) {
617
+ return null;
618
+ }
619
+ }
620
+ const id = Number.parseInt(digits, 10);
621
+ return id >= 1 && id <= MAX_PAYLOAD_ROWS ? id : null;
622
+ }
623
+
624
+ /**
625
+ * A payload row, or the model, as the text of a `<script type="application/json">`.
626
+ *
627
+ * `<` is escaped so a string holding `</script>` cannot end the element early,
628
+ * and U+2028 and U+2029 because a JSON document is not JavaScript source but is
629
+ * sometimes read as if it were. This is the escape `internal/runtime.js`
630
+ * applied inline before there was a payload; it lives here now because the
631
+ * model and every row have to be written the same way and there is no longer
632
+ * only one of them.
633
+ */
634
+ export function payloadJson(value: mixed): string {
635
+ return JSON.stringify(value)
636
+ .replace(/</g, "\\u003c")
637
+ .replace(/\u2028/g, "\\u2028")
638
+ .replace(/\u2029/g, "\\u2029");
639
+ }
640
+
641
+ /**
642
+ * Read one late row's script text.
643
+ *
644
+ * Refuses anything that is not exactly one of the two shapes, and refuses a
645
+ * message carrying both — a row is a value or a failure, never a choice the
646
+ * reader has to make. The value goes through [`decodePayload`] with a resolver
647
+ * that refuses every reference, which is where "a row may not itself be
648
+ * deferred" is enforced on the reading side: ids are handed out by one walk of
649
+ * the model, and a row that could add more would be a numbering that depends
650
+ * on the order things finished in.
651
+ */
652
+ export function parseRowMessage(text: string, id: number): PayloadRowMessage {
653
+ const label = `row ${String(id)}`;
654
+ let parsed: mixed;
655
+ try {
656
+ parsed = JSON.parse(text);
657
+ } catch {
658
+ throw new PayloadValueError(label, "is not JSON");
659
+ }
660
+ if (!isPlainObject(parsed)) {
661
+ throw new PayloadValueError(label, "is not a row message");
662
+ }
663
+ const message: { +[string]: mixed } = (parsed: $FlowFixMe);
664
+ const hasValue = Object.prototype.hasOwnProperty.call(message, "value");
665
+ const hasError = Object.prototype.hasOwnProperty.call(message, "error");
666
+ if (hasValue === hasError) {
667
+ throw new PayloadValueError(label, "must carry exactly one of `value` and `error`");
668
+ }
669
+ if (hasError) {
670
+ const error = message.error;
671
+ if (typeof error !== "string") {
672
+ throw new PayloadValueError(label, "carries an `error` that is not a string");
673
+ }
674
+ return { error };
675
+ }
676
+ return {
677
+ value: decodePayload(
678
+ message.value,
679
+ () => {
680
+ throw new PayloadValueError(label, "refers to another row");
681
+ },
682
+ label,
683
+ ),
684
+ };
685
+ }