main
js 1,521 lines 51.1 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 Thenable,
12 PendingThenable,
13 FulfilledThenable,
14 RejectedThenable,
15 ReactCustomFormAction,
16 ReactFunctionLocation,
17 } from 'shared/ReactTypes';
18 import type {LazyComponent} from 'react/src/ReactLazy';
19 import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
20
21 import {
22 REACT_ELEMENT_TYPE,
23 REACT_LAZY_TYPE,
24 REACT_CONTEXT_TYPE,
25 getIteratorFn,
26 ASYNC_ITERATOR,
27 } from 'shared/ReactSymbols';
28
29 import {
30 describeObjectForErrorMessage,
31 isSimpleObject,
32 objectName,
33 } from 'shared/ReactSerializationErrors';
34
35 import {writeTemporaryReference} from './ReactFlightTemporaryReferences';
36
37 import isArray from 'shared/isArray';
38 import getPrototypeOf from 'shared/getPrototypeOf';
39
40 const ObjectPrototype = Object.prototype;
41
42 // Passed to replyLifetimeController.abort(). Nothing reads the reason, but a
43 // call to abort() without one constructs an AbortError DOMException. Capturing
44 // the stack trace dominates that cost, and the cost grows with the depth of the
45 // stack.
46 const REPLY_ENDED = 'The reply ended.';
47
48 import {
49 usedWithSSR,
50 checkEvalAvailabilityOnceDev,
51 } from './ReactFlightClientConfig';
52
53 type ReactJSONValue =
54 | string
55 | boolean
56 | number
57 | null
58 | $ReadOnlyArray<ReactJSONValue>
59 | ReactServerObject;
60
61 export opaque type ServerReference<T> = T;
62
63 export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
64
65 export type EncodeFormActionCallback = <A>(
66 id: any,
67 args: Promise<A>,
68 ) => ReactCustomFormAction;
69
70 export type ServerReferenceId = any;
71
72 type ServerReferenceClosure = {
73 id: ServerReferenceId,
74 originalBind: Function,
75 bound: null | Thenable<Array<any>>,
76 };
77
78 const knownServerReferences: WeakMap<Function, ServerReferenceClosure> =
79 new WeakMap();
80
81 // Serializable values
82 export type ReactServerValue =
83 // References are passed by their value
84 | ServerReference<any>
85 // The rest are passed as is. Sub-types can be passed in but lose their
86 // subtype, so the receiver can only accept once of these.
87 | string
88 | boolean
89 | number
90 | null
91 | void
92 | bigint
93 | $AsyncIterable<ReactServerValue, ReactServerValue, void>
94 | $AsyncIterator<ReactServerValue, ReactServerValue, void>
95 | Iterable<ReactServerValue>
96 | Iterator<ReactServerValue>
97 | Array<ReactServerValue>
98 | Map<ReactServerValue, ReactServerValue>
99 | Set<ReactServerValue>
100 | FormData
101 | Date
102 | ReactServerObject
103 | Promise<ReactServerValue>; // Thenable<ReactServerValue>
104
105 type ReactServerObject = {+[key: string]: ReactServerValue};
106
107 const __PROTO__ = '__proto__';
108
109 function serializeByValueID(id: number): string {
110 return '$' + id.toString(16);
111 }
112
113 function serializePromiseID(id: number): string {
114 return '$@' + id.toString(16);
115 }
116
117 function serializeServerReferenceID(id: number): string {
118 return '$h' + id.toString(16);
119 }
120
121 function serializeTemporaryReferenceMarker(): string {
122 return '$T';
123 }
124
125 function serializeFormDataReference(id: number): string {
126 return '$K' + id.toString(16);
127 }
128
129 function serializeNumber(number: number): string | number {
130 if (Number.isFinite(number)) {
131 if (number === 0 && 1 / number === -Infinity) {
132 return '$-0';
133 } else {
134 return number;
135 }
136 } else {
137 if (number === Infinity) {
138 return '$Infinity';
139 } else if (number === -Infinity) {
140 return '$-Infinity';
141 } else {
142 return '$NaN';
143 }
144 }
145 }
146
147 function serializeUndefined(): string {
148 return '$undefined';
149 }
150
151 function serializeDateFromDateJSON(dateJSON: string): string {
152 // JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
153 // We need only tack on a $D prefix.
154 return '$D' + dateJSON;
155 }
156
157 function serializeBigInt(n: bigint): string {
158 return '$n' + n.toString(10);
159 }
160
161 function serializeMapID(id: number): string {
162 return '$Q' + id.toString(16);
163 }
164
165 function serializeSetID(id: number): string {
166 return '$W' + id.toString(16);
167 }
168
169 function serializeBlobID(id: number): string {
170 return '$B' + id.toString(16);
171 }
172
173 function serializeIteratorID(id: number): string {
174 return '$i' + id.toString(16);
175 }
176
177 function escapeStringValue(value: string): string {
178 if (value[0] === '$') {
179 // We need to escape $ prefixed strings since we use those to encode
180 // references to IDs and as special symbol values.
181 return '$' + value;
182 } else {
183 return value;
184 }
185 }
186
187 interface Reference {}
188
189 export function processReply(
190 root: ReactServerValue,
191 formFieldPrefix: string,
192 temporaryReferences: void | TemporaryReferenceSet,
193 onResolve: (string | FormData) => void,
194 onReject: (error: mixed) => void,
195 signal: void | AbortSignal,
196 ): void {
197 let nextPartId = 1;
198 let pendingParts = 0;
199 let formData: null | FormData = null;
200 const writtenObjects: WeakMap<Reference, string> = new WeakMap();
201 let modelRoot: null | ReactServerValue = root;
202 let settled = false;
203 // Bounds the abort listener that attachAbortSignal attaches to the caller's
204 // signal. Null until a signal is attached, so a reply that gets no signal
205 // never creates a controller.
206 let replyLifetimeController: null | AbortController = null;
207
208 // Ending the lifetime makes the runtime remove the caller's abort listener.
209 // Without that, the listener keeps everything this reply serialized reachable
210 // for as long as the caller's signal lives, and a composite signal from
211 // AbortSignal.any() is itself retained by the runtime while it has any abort
212 // listener attached.
213 function endReplyLifetime(): void {
214 if (replyLifetimeController !== null) {
215 replyLifetimeController.abort(REPLY_ENDED);
216 }
217 }
218
219 function resolve(value: string | FormData): void {
220 settled = true;
221 endReplyLifetime();
222 onResolve(value);
223 }
224
225 function reject(error: mixed): void {
226 settled = true;
227 endReplyLifetime();
228 onReject(error);
229 }
230
231 function attachAbortSignal(abortSignal: AbortSignal): void {
232 if (abortSignal.aborted) {
233 abort(abortSignal.reason);
234 return;
235 }
236 replyLifetimeController = new AbortController();
237 abortSignal.addEventListener(
238 'abort',
239 () => {
240 abort(abortSignal.reason);
241 },
242 {signal: replyLifetimeController.signal},
243 );
244 }
245
246 if (__DEV__) {
247 // We use eval to create fake function stacks which includes Component stacks.
248 // A warning would be noise if you used Flight without Components and don't encounter
249 // errors. We're warning eagerly so that you configure your environment accordingly
250 // before you encounter an error.
251 checkEvalAvailabilityOnceDev();
252 }
253
254 function serializeTypedArray(
255 tag: string,
256 typedArray: $ArrayBufferView,
257 ): string {
258 const blob = new Blob([
259 // We should be able to pass the buffer straight through but Node < 18 treat
260 // multi-byte array blobs differently so we first convert it to single-byte.
261 new Uint8Array(
262 typedArray.buffer,
263 typedArray.byteOffset,
264 typedArray.byteLength,
265 ),
266 ]);
267 const blobId = nextPartId++;
268 if (formData === null) {
269 formData = new FormData();
270 }
271 formData.append(formFieldPrefix + blobId, blob);
272 return '$' + tag + blobId.toString(16);
273 }
274
275 function serializeBinaryReader(reader: any): string {
276 if (formData === null) {
277 // Upgrade to use FormData to allow us to stream this value.
278 formData = new FormData();
279 }
280 const data = formData;
281
282 pendingParts++;
283 const streamId = nextPartId++;
284
285 const buffer = [];
286
287 function progress(entry: {done: boolean, value: ReactServerValue, ...}) {
288 if (entry.done) {
289 const blobId = nextPartId++;
290 data.append(formFieldPrefix + blobId, new Blob(buffer));
291 data.append(
292 formFieldPrefix + streamId,
293 '"$o' + blobId.toString(16) + '"',
294 );
295 data.append(formFieldPrefix + streamId, 'C'); // Close signal
296 pendingParts--;
297 if (pendingParts === 0) {
298 resolve(data);
299 }
300 } else {
301 buffer.push(entry.value);
302 reader.read(new Uint8Array(1024)).then(progress, reject);
303 }
304 }
305 reader.read(new Uint8Array(1024)).then(progress, reject);
306
307 return '$r' + streamId.toString(16);
308 }
309
310 function serializeReader(reader: ReadableStreamReader): string {
311 if (formData === null) {
312 // Upgrade to use FormData to allow us to stream this value.
313 formData = new FormData();
314 }
315 const data = formData;
316
317 pendingParts++;
318 const streamId = nextPartId++;
319
320 function progress(entry: {done: boolean, value: ReactServerValue, ...}) {
321 if (entry.done) {
322 data.append(formFieldPrefix + streamId, 'C'); // Close signal
323 pendingParts--;
324 if (pendingParts === 0) {
325 resolve(data);
326 }
327 } else {
328 try {
329 // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
330 const partJSON: string = JSON.stringify(entry.value, resolveToJSON);
331 data.append(formFieldPrefix + streamId, partJSON);
332 reader.read().then(progress, reject);
333 } catch (x) {
334 reject(x);
335 }
336 }
337 }
338 reader.read().then(progress, reject);
339
340 return '$R' + streamId.toString(16);
341 }
342
343 function serializeReadableStream(stream: ReadableStream): string {
344 // Detect if this is a BYOB stream. BYOB streams should be able to be read as bytes on the
345 // receiving side. For binary streams, we serialize them as plain Blobs.
346 let binaryReader;
347 try {
348 // $FlowFixMe[extra-arg]: This argument is accepted.
349 binaryReader = stream.getReader({mode: 'byob'});
350 } catch (x) {
351 return serializeReader(stream.getReader());
352 }
353 return serializeBinaryReader(binaryReader);
354 }
355
356 function serializeAsyncIterable(
357 iterable: $AsyncIterable<ReactServerValue, ReactServerValue, void>,
358 iterator: $AsyncIterator<ReactServerValue, ReactServerValue, void>,
359 ): string {
360 if (formData === null) {
361 // Upgrade to use FormData to allow us to stream this value.
362 formData = new FormData();
363 }
364 const data = formData;
365
366 pendingParts++;
367 const streamId = nextPartId++;
368
369 // Generators/Iterators are Iterables but they're also their own iterator
370 // functions. If that's the case, we treat them as single-shot. Otherwise,
371 // we assume that this iterable might be a multi-shot and allow it to be
372 // iterated more than once on the receiving server.
373 const isIterator = iterable === iterator;
374
375 // There's a race condition between when the stream is aborted and when the promise
376 // resolves so we track whether we already aborted it to avoid writing twice.
377 function progress(
378 entry:
379 | {done: false, +value: ReactServerValue, ...}
380 | {done: true, +value: ReactServerValue, ...},
381 ) {
382 if (entry.done) {
383 if (entry.value === undefined) {
384 data.append(formFieldPrefix + streamId, 'C'); // Close signal
385 } else {
386 // Unlike streams, the last value may not be undefined. If it's not
387 // we outline it and encode a reference to it in the closing instruction.
388 try {
389 // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
390 const partJSON: string = JSON.stringify(entry.value, resolveToJSON);
391 data.append(formFieldPrefix + streamId, 'C' + partJSON); // Close signal
392 } catch (x) {
393 reject(x);
394 return;
395 }
396 }
397 pendingParts--;
398 if (pendingParts === 0) {
399 resolve(data);
400 }
401 } else {
402 try {
403 // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
404 const partJSON: string = JSON.stringify(entry.value, resolveToJSON);
405 data.append(formFieldPrefix + streamId, partJSON);
406 iterator.next().then(progress, reject);
407 } catch (x) {
408 reject(x);
409 return;
410 }
411 }
412 }
413
414 iterator.next().then(progress, reject);
415 return '$' + (isIterator ? 'x' : 'X') + streamId.toString(16);
416 }
417
418 function resolveToJSON(
419 this:
420 | {+[key: string | number]: ReactServerValue}
421 | $ReadOnlyArray<ReactServerValue>,
422 key: string,
423 value: ReactServerValue,
424 ): ReactJSONValue {
425 const parent = this;
426
427 if (__DEV__) {
428 if (key === __PROTO__) {
429 console.error(
430 'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
431 describeObjectForErrorMessage(parent, key),
432 );
433 }
434 }
435
436 // Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
437 if (__DEV__) {
438 // $FlowFixMe[incompatible-use]
439 const originalValue = parent[key];
440 if (
441 typeof originalValue === 'object' &&
442 originalValue !== value &&
443 !(originalValue instanceof Date)
444 ) {
445 if (objectName(originalValue) !== 'Object') {
446 console.error(
447 'Only plain objects can be passed to Server Functions from the Client. ' +
448 '%s objects are not supported.%s',
449 objectName(originalValue),
450 describeObjectForErrorMessage(parent, key),
451 );
452 } else {
453 console.error(
454 'Only plain objects can be passed to Server Functions from the Client. ' +
455 'Objects with toJSON methods are not supported. Convert it manually ' +
456 'to a simple value before passing it to props.%s',
457 describeObjectForErrorMessage(parent, key),
458 );
459 }
460 }
461 }
462
463 if (value === null) {
464 return null;
465 }
466
467 if (typeof value === 'object') {
468 switch ((value as any).$$typeof) {
469 case REACT_ELEMENT_TYPE: {
470 if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
471 // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
472 const parentReference = writtenObjects.get(parent);
473 if (parentReference !== undefined) {
474 // If the parent has a reference, we can refer to this object indirectly
475 // through the property name inside that parent.
476 const reference = parentReference + ':' + key;
477 // Store this object so that the server can refer to it later in responses.
478 writeTemporaryReference(temporaryReferences, reference, value);
479 return serializeTemporaryReferenceMarker();
480 }
481 }
482 // This element is the root of a serializeModel call (e.g. JSX
483 // passed directly to encodeReply, or a promise that resolved to
484 // JSX). It was already registered as a temporary reference by
485 // serializeModel so we just need to emit the marker.
486 if (temporaryReferences !== undefined && modelRoot === value) {
487 modelRoot = null;
488 return serializeTemporaryReferenceMarker();
489 }
490 throw new Error(
491 'React Element cannot be passed to Server Functions from the Client without a ' +
492 'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
493 (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
494 );
495 }
496 case REACT_LAZY_TYPE: {
497 // Resolve lazy as if it wasn't here. In the future this will be encoded as a Promise.
498 const lazy: LazyComponent<any, any> = value as any;
499 const payload = lazy._payload;
500 const init = lazy._init;
501 if (formData === null) {
502 // Upgrade to use FormData to allow us to stream this value.
503 formData = new FormData();
504 }
505 pendingParts++;
506 try {
507 const resolvedModel = init(payload);
508 // We always outline this as a separate part even though we could inline it
509 // because it ensures a more deterministic encoding.
510 const lazyId = nextPartId++;
511 const partJSON = serializeModel(resolvedModel, lazyId);
512 // $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
513 const data: FormData = formData;
514 data.append(formFieldPrefix + lazyId, partJSON);
515 return serializeByValueID(lazyId);
516 } catch (x) {
517 if (
518 typeof x === 'object' &&
519 x !== null &&
520 typeof x.then === 'function'
521 ) {
522 // Suspended
523 pendingParts++;
524 const lazyId = nextPartId++;
525 const thenable: Thenable<any> = x as any;
526 const retry = function () {
527 // While the first promise resolved, its value isn't necessarily what we'll
528 // resolve into because we might suspend again.
529 try {
530 const partJSON = serializeModel(value, lazyId);
531 // $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
532 const data: FormData = formData;
533 data.append(formFieldPrefix + lazyId, partJSON);
534 pendingParts--;
535 if (pendingParts === 0) {
536 resolve(data);
537 }
538 } catch (reason) {
539 reject(reason);
540 }
541 };
542 thenable.then(retry, retry);
543 return serializeByValueID(lazyId);
544 } else {
545 // In the future we could consider serializing this as an error
546 // that throws on the server instead.
547 reject(x);
548 return null;
549 }
550 } finally {
551 pendingParts--;
552 }
553 }
554 }
555
556 const existingReference = writtenObjects.get(value);
557
558 // $FlowFixMe[method-unbinding]
559 if (typeof value.then === 'function') {
560 if (existingReference !== undefined) {
561 if (modelRoot === value) {
562 // This is the ID we're currently emitting so we need to write it
563 // once but if we discover it again, we refer to it by id.
564 modelRoot = null;
565 } else {
566 // We've already emitted this as an outlined object, so we can
567 // just refer to that by its existing ID.
568 return existingReference;
569 }
570 }
571
572 // We assume that any object with a .then property is a "Thenable" type,
573 // or a Promise type. Either of which can be represented by a Promise.
574 if (formData === null) {
575 // Upgrade to use FormData to allow us to stream this value.
576 formData = new FormData();
577 }
578 pendingParts++;
579 const promiseId = nextPartId++;
580 const promiseReference = serializePromiseID(promiseId);
581 writtenObjects.set(value, promiseReference);
582 const thenable: Thenable<any> = value as any;
583 thenable.then(
584 partValue => {
585 try {
586 const previousReference = writtenObjects.get(partValue);
587 let partJSON;
588 if (previousReference !== undefined) {
589 partJSON = JSON.stringify(previousReference);
590 } else {
591 partJSON = serializeModel(partValue, promiseId);
592 }
593 // $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
594 const data: FormData = formData;
595 data.append(formFieldPrefix + promiseId, partJSON);
596 pendingParts--;
597 if (pendingParts === 0) {
598 resolve(data);
599 }
600 } catch (reason) {
601 reject(reason);
602 }
603 },
604 // In the future we could consider serializing this as an error
605 // that throws on the server instead.
606 reject,
607 );
608 return promiseReference;
609 }
610
611 if (existingReference !== undefined) {
612 if (modelRoot === value) {
613 // This is the ID we're currently emitting so we need to write it
614 // once but if we discover it again, we refer to it by id.
615 modelRoot = null;
616 } else {
617 // We've already emitted this as an outlined object, so we can
618 // just refer to that by its existing ID.
619 return existingReference;
620 }
621 } else if (key.indexOf(':') === -1) {
622 // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
623 const parentReference = writtenObjects.get(parent);
624 if (parentReference !== undefined) {
625 // If the parent has a reference, we can refer to this object indirectly
626 // through the property name inside that parent.
627 const reference = parentReference + ':' + key;
628 writtenObjects.set(value, reference);
629 if (temporaryReferences !== undefined) {
630 // Store this object so that the server can refer to it later in responses.
631 writeTemporaryReference(temporaryReferences, reference, value);
632 }
633 }
634 }
635
636 if (isArray(value)) {
637 // $FlowFixMe[incompatible-type]
638 return value;
639 }
640 // TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
641 if (value instanceof FormData) {
642 if (formData === null) {
643 // Upgrade to use FormData to allow us to use rich objects as its values.
644 formData = new FormData();
645 }
646 const data: FormData = formData;
647 const refId = nextPartId++;
648 // Copy all the form fields with a prefix for this reference.
649 // These must come first in the form order because we assume that all the
650 // fields are available before this is referenced.
651 // We include a special marker so that the Server can detect FormData entries
652 // that are values in referenced FormData objects.
653 const prefix = formFieldPrefix + '_' + refId + '_';
654 // $FlowFixMe[prop-missing]: FormData has forEach.
655 value.forEach((originalValue: string | File, originalKey: string) => {
656 // $FlowFixMe[incompatible-type]
657 data.append(prefix + originalKey, originalValue);
658 });
659 return serializeFormDataReference(refId);
660 }
661 if (value instanceof Map) {
662 const mapId = nextPartId++;
663 const partJSON = serializeModel(Array.from(value), mapId);
664 if (formData === null) {
665 formData = new FormData();
666 }
667 formData.append(formFieldPrefix + mapId, partJSON);
668 return serializeMapID(mapId);
669 }
670 if (value instanceof Set) {
671 const setId = nextPartId++;
672 const partJSON = serializeModel(Array.from(value), setId);
673 if (formData === null) {
674 formData = new FormData();
675 }
676 formData.append(formFieldPrefix + setId, partJSON);
677 return serializeSetID(setId);
678 }
679
680 if (value instanceof ArrayBuffer) {
681 const blob = new Blob([value]);
682 const blobId = nextPartId++;
683 if (formData === null) {
684 formData = new FormData();
685 }
686 formData.append(formFieldPrefix + blobId, blob);
687 return '$' + 'A' + blobId.toString(16);
688 }
689 if (value instanceof Int8Array) {
690 // char
691 return serializeTypedArray('O', value);
692 }
693 if (value instanceof Uint8Array) {
694 // unsigned char
695 return serializeTypedArray('o', value);
696 }
697 if (value instanceof Uint8ClampedArray) {
698 // unsigned clamped char
699 return serializeTypedArray('U', value);
700 }
701 if (value instanceof Int16Array) {
702 // sort
703 return serializeTypedArray('S', value);
704 }
705 if (value instanceof Uint16Array) {
706 // unsigned short
707 return serializeTypedArray('s', value);
708 }
709 if (value instanceof Int32Array) {
710 // long
711 return serializeTypedArray('L', value);
712 }
713 if (value instanceof Uint32Array) {
714 // unsigned long
715 return serializeTypedArray('l', value);
716 }
717 if (value instanceof Float32Array) {
718 // float
719 return serializeTypedArray('G', value);
720 }
721 if (value instanceof Float64Array) {
722 // double
723 return serializeTypedArray('g', value);
724 }
725 if (value instanceof BigInt64Array) {
726 // number
727 return serializeTypedArray('M', value);
728 }
729 if (value instanceof BigUint64Array) {
730 // unsigned number
731 // We use "m" instead of "n" since JSON can start with "null"
732 return serializeTypedArray('m', value);
733 }
734 if (value instanceof DataView) {
735 return serializeTypedArray('V', value);
736 }
737 // TODO: Blob is not available in old Node/browsers. Remove the typeof check later.
738 if (typeof Blob === 'function' && value instanceof Blob) {
739 if (formData === null) {
740 formData = new FormData();
741 }
742 const blobId = nextPartId++;
743 formData.append(formFieldPrefix + blobId, value);
744 return serializeBlobID(blobId);
745 }
746
747 const iteratorFn = getIteratorFn(value);
748 if (iteratorFn) {
749 const iterator = iteratorFn.call(value);
750 // $FlowFixMe[invalid-compare]
751 if (iterator === value) {
752 // Iterator, not Iterable
753 const iteratorId = nextPartId++;
754 const partJSON = serializeModel(
755 Array.from(iterator as any),
756 iteratorId,
757 );
758 if (formData === null) {
759 formData = new FormData();
760 }
761 formData.append(formFieldPrefix + iteratorId, partJSON);
762 return serializeIteratorID(iteratorId);
763 }
764 return Array.from(iterator as any);
765 }
766
767 // TODO: ReadableStream is not available in old Node. Remove the typeof check later.
768 if (
769 typeof ReadableStream === 'function' &&
770 value instanceof ReadableStream
771 ) {
772 return serializeReadableStream(value);
773 }
774 const getAsyncIterator: void | (() => $AsyncIterator<any, any, any>) = (
775 value as any
776 )[ASYNC_ITERATOR];
777 if (typeof getAsyncIterator === 'function') {
778 // We treat AsyncIterables as a Fragment and as such we might need to key them.
779 return serializeAsyncIterable(
780 value as any,
781 getAsyncIterator.call(value as any),
782 );
783 }
784
785 // Verify that this is a simple plain object.
786 const proto = getPrototypeOf(value);
787 if (
788 proto !== ObjectPrototype &&
789 (proto === null || getPrototypeOf(proto) !== null)
790 ) {
791 if (temporaryReferences === undefined) {
792 throw new Error(
793 'Only plain objects, and a few built-ins, can be passed to Server Functions. ' +
794 'Classes or null prototypes are not supported.' +
795 (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
796 );
797 }
798 // We will have written this object to the temporary reference set above
799 // so we can replace it with a marker to refer to this slot later.
800 return serializeTemporaryReferenceMarker();
801 }
802 if (__DEV__) {
803 if ((value as any).$$typeof === REACT_CONTEXT_TYPE) {
804 console.error(
805 'React Context Providers cannot be passed to Server Functions from the Client.%s',
806 describeObjectForErrorMessage(parent, key),
807 );
808 } else if (objectName(value) !== 'Object') {
809 console.error(
810 'Only plain objects can be passed to Server Functions from the Client. ' +
811 '%s objects are not supported.%s',
812 objectName(value),
813 describeObjectForErrorMessage(parent, key),
814 );
815 } else if (!isSimpleObject(value)) {
816 console.error(
817 'Only plain objects can be passed to Server Functions from the Client. ' +
818 'Classes or other objects with methods are not supported.%s',
819 describeObjectForErrorMessage(parent, key),
820 );
821 } else if (Object.getOwnPropertySymbols) {
822 const symbols = Object.getOwnPropertySymbols(value);
823 if (symbols.length > 0) {
824 console.error(
825 'Only plain objects can be passed to Server Functions from the Client. ' +
826 'Objects with symbol properties like %s are not supported.%s',
827 symbols[0].description,
828 describeObjectForErrorMessage(parent, key),
829 );
830 }
831 }
832 }
833
834 // $FlowFixMe[incompatible-return]
835 return value;
836 }
837
838 if (typeof value === 'string') {
839 // TODO: Maybe too clever. If we support URL there's no similar trick.
840 if (value[value.length - 1] === 'Z') {
841 // Possibly a Date, whose toJSON automatically calls toISOString
842 // $FlowFixMe[incompatible-use]
843 const originalValue = parent[key];
844 if (originalValue instanceof Date) {
845 return serializeDateFromDateJSON(value);
846 }
847 }
848
849 return escapeStringValue(value);
850 }
851
852 if (typeof value === 'boolean') {
853 return value;
854 }
855
856 if (typeof value === 'number') {
857 return serializeNumber(value);
858 }
859
860 if (typeof value === 'undefined') {
861 return serializeUndefined();
862 }
863
864 if (typeof value === 'function') {
865 const referenceClosure = knownServerReferences.get(value);
866 if (referenceClosure !== undefined) {
867 const existingReference = writtenObjects.get(value);
868 if (existingReference !== undefined) {
869 return existingReference;
870 }
871 const {id, bound} = referenceClosure;
872 const referenceClosureJSON = JSON.stringify({id, bound}, resolveToJSON);
873 if (formData === null) {
874 // Upgrade to use FormData to allow us to stream this value.
875 formData = new FormData();
876 }
877 // The reference to this function came from the same client so we can pass it back.
878 const refId = nextPartId++;
879 formData.set(formFieldPrefix + refId, referenceClosureJSON);
880 const serverReferenceId = serializeServerReferenceID(refId);
881 // Store the server reference ID for deduplication.
882 writtenObjects.set(value, serverReferenceId);
883 return serverReferenceId;
884 }
885 if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
886 // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
887 const parentReference = writtenObjects.get(parent);
888 if (parentReference !== undefined) {
889 // If the parent has a reference, we can refer to this object indirectly
890 // through the property name inside that parent.
891 const reference = parentReference + ':' + key;
892 // Store this object so that the server can refer to it later in responses.
893 writeTemporaryReference(temporaryReferences, reference, value);
894 return serializeTemporaryReferenceMarker();
895 }
896 }
897 throw new Error(
898 'Client Functions cannot be passed directly to Server Functions. ' +
899 'Only Functions passed from the Server can be passed back again.',
900 );
901 }
902
903 if (typeof value === 'symbol') {
904 if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
905 // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
906 const parentReference = writtenObjects.get(parent);
907 if (parentReference !== undefined) {
908 // If the parent has a reference, we can refer to this object indirectly
909 // through the property name inside that parent.
910 const reference = parentReference + ':' + key;
911 // Store this object so that the server can refer to it later in responses.
912 writeTemporaryReference(temporaryReferences, reference, value);
913 return serializeTemporaryReferenceMarker();
914 }
915 }
916 throw new Error(
917 'Symbols cannot be passed to a Server Function without a ' +
918 'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
919 (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
920 );
921 }
922
923 if (typeof value === 'bigint') {
924 return serializeBigInt(value);
925 }
926
927 throw new Error(
928 `Type ${typeof value} is not supported as an argument to a Server Function.`,
929 );
930 }
931
932 function serializeModel(model: ReactServerValue, id: number): string {
933 if (typeof model === 'object' && model !== null) {
934 const reference = serializeByValueID(id);
935 writtenObjects.set(model, reference);
936 if (temporaryReferences !== undefined) {
937 // Store this object so that the server can refer to it later in responses.
938 writeTemporaryReference(temporaryReferences, reference, model);
939 }
940 }
941 modelRoot = model;
942 // $FlowFixMe[incompatible-type] it's not going to be undefined because we'll encode it.
943 return JSON.stringify(model, resolveToJSON);
944 }
945
946 function abort(reason: mixed): void {
947 // Nothing can make the reply pending again from here, so the caller's
948 // signal has no further effect on it.
949 endReplyLifetime();
950 if (pendingParts > 0) {
951 pendingParts = 0; // Don't resolve again later.
952 // Resolve with what we have so far, which may have holes at this point.
953 // They'll error when the stream completes on the server.
954 if (formData === null) {
955 resolve(json);
956 } else {
957 resolve(formData);
958 }
959 }
960 }
961
962 const json = serializeModel(root, 0);
963
964 if (formData === null) {
965 // If it's a simple data structure, we just use plain JSON.
966 resolve(json);
967 } else {
968 // Otherwise, we use FormData to let us stream in the result.
969 formData.set(formFieldPrefix + '0', json);
970 if (pendingParts === 0) {
971 // $FlowFixMe[incompatible-type] this has already been refined.
972 resolve(formData);
973 }
974 }
975
976 // Wired up after serializing: abort() reads `json` and resolves with the
977 // parts that finished, so it must not be reachable before then. A reply that
978 // already settled gets no listener, since aborting it would be a no-op and
979 // the lifetime that removes the listener has already ended.
980 //
981 // TODO: Skip serializing when the signal is already aborted, the way the
982 // server entry points abort before rendering starts. Needs a decision on what
983 // to resolve with, since abort() resolves with the parts that finished.
984 if (signal !== undefined && !settled) {
985 attachAbortSignal(signal);
986 }
987 }
988
989 const boundCache: WeakMap<
990 ServerReferenceClosure,
991 Thenable<FormData>,
992 > = new WeakMap();
993
994 function encodeFormData(reference: any): Thenable<FormData> {
995 let resolve, reject;
996 // We need to have a handle on the thenable so that we can synchronously set
997 // its status from processReply, when it can complete synchronously.
998 const thenable: Thenable<FormData> = new Promise((res, rej) => {
999 resolve = res;
1000 reject = rej;
1001 });
1002 processReply(
1003 reference,
1004 '',
1005 undefined, // TODO: This means React Elements can't be used as state in progressive enhancement.
1006 (body: string | FormData) => {
1007 if (typeof body === 'string') {
1008 const data = new FormData();
1009 data.append('0', body);
1010 body = data;
1011 }
1012 const fulfilled: FulfilledThenable<FormData> = thenable as any;
1013 fulfilled.status = 'fulfilled';
1014 fulfilled.value = body;
1015 resolve(body);
1016 },
1017 e => {
1018 const rejected: RejectedThenable<FormData> = thenable as any;
1019 rejected.status = 'rejected';
1020 rejected.reason = e;
1021 reject(e);
1022 },
1023 );
1024 return thenable;
1025 }
1026
1027 function defaultEncodeFormAction(
1028 this: any => Promise<any>,
1029 identifierPrefix: string,
1030 ): ReactCustomFormAction {
1031 const referenceClosure = knownServerReferences.get(this);
1032 if (!referenceClosure) {
1033 throw new Error(
1034 'Tried to encode a Server Action from a different instance than the encoder is from. ' +
1035 'This is a bug in React.',
1036 );
1037 }
1038 let data: null | FormData = null;
1039 let name;
1040 const boundPromise = referenceClosure.bound;
1041 if (boundPromise !== null) {
1042 let thenable = boundCache.get(referenceClosure);
1043 if (!thenable) {
1044 const {id, bound} = referenceClosure;
1045 thenable = encodeFormData({id, bound});
1046 boundCache.set(referenceClosure, thenable);
1047 }
1048 if (thenable.status === 'rejected') {
1049 throw thenable.reason;
1050 } else if (thenable.status !== 'fulfilled') {
1051 throw thenable;
1052 }
1053 const encodedFormData = thenable.value;
1054 // This is hacky but we need the identifier prefix to be added to
1055 // all fields but the suspense cache would break since we might get
1056 // a new identifier each time. So we just append it at the end instead.
1057 const prefixedData = new FormData();
1058 // $FlowFixMe[prop-missing]
1059 encodedFormData.forEach((value: string | File, key: string) => {
1060 // $FlowFixMe[incompatible-type]
1061 prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
1062 });
1063 data = prefixedData;
1064 // We encode the name of the prefix containing the data.
1065 name = '$ACTION_REF_' + identifierPrefix;
1066 } else {
1067 // This is the simple case so we can just encode the ID.
1068 name = '$ACTION_ID_' + referenceClosure.id;
1069 }
1070 return {
1071 name: name,
1072 method: 'POST',
1073 encType: 'multipart/form-data',
1074 data: data,
1075 };
1076 }
1077
1078 function customEncodeFormAction(
1079 reference: any => Promise<any>,
1080 identifierPrefix: string,
1081 encodeFormAction: EncodeFormActionCallback,
1082 ): ReactCustomFormAction {
1083 const referenceClosure = knownServerReferences.get(reference);
1084 if (!referenceClosure) {
1085 throw new Error(
1086 'Tried to encode a Server Action from a different instance than the encoder is from. ' +
1087 'This is a bug in React.',
1088 );
1089 }
1090 let boundPromise: Promise<Array<any>> = referenceClosure.bound as any;
1091 // $FlowFixMe[invalid-compare]
1092 if (boundPromise === null) {
1093 boundPromise = Promise.resolve([]);
1094 }
1095 return encodeFormAction(referenceClosure.id, boundPromise);
1096 }
1097
1098 function isSignatureEqual(
1099 this: any => Promise<any>,
1100 referenceId: ServerReferenceId,
1101 numberOfBoundArgs: number,
1102 ): boolean {
1103 const referenceClosure = knownServerReferences.get(this);
1104 if (!referenceClosure) {
1105 throw new Error(
1106 'Tried to encode a Server Action from a different instance than the encoder is from. ' +
1107 'This is a bug in React.',
1108 );
1109 }
1110 if (referenceClosure.id !== referenceId) {
1111 // These are different functions.
1112 return false;
1113 }
1114 // Now check if the number of bound arguments is the same.
1115 const boundPromise = referenceClosure.bound;
1116 if (boundPromise === null) {
1117 // No bound arguments.
1118 return numberOfBoundArgs === 0;
1119 }
1120 // Unwrap the bound arguments array by suspending, if necessary. As with
1121 // encodeFormData, this means isSignatureEqual can only be called while React
1122 // is rendering.
1123 switch (boundPromise.status) {
1124 case 'fulfilled': {
1125 const boundArgs = boundPromise.value;
1126 return boundArgs.length === numberOfBoundArgs;
1127 }
1128 case 'pending': {
1129 throw boundPromise;
1130 }
1131 case 'rejected': {
1132 throw boundPromise.reason;
1133 }
1134 default: {
1135 if (typeof boundPromise.status === 'string') {
1136 // Only instrument the thenable if the status if not defined.
1137 } else {
1138 const pendingThenable: PendingThenable<Array<any>> =
1139 boundPromise as any;
1140 pendingThenable.status = 'pending';
1141 pendingThenable.then(
1142 (boundArgs: Array<any>) => {
1143 const fulfilledThenable: FulfilledThenable<Array<any>> =
1144 boundPromise as any;
1145 fulfilledThenable.status = 'fulfilled';
1146 fulfilledThenable.value = boundArgs;
1147 },
1148 (error: mixed) => {
1149 const rejectedThenable: RejectedThenable<number> =
1150 boundPromise as any;
1151 rejectedThenable.status = 'rejected';
1152 rejectedThenable.reason = error;
1153 },
1154 );
1155 }
1156 throw boundPromise;
1157 }
1158 }
1159 }
1160
1161 let fakeServerFunctionIdx = 0;
1162
1163 function createFakeServerFunction<A: Iterable<any>, T>(
1164 name: string,
1165 filename: string,
1166 sourceMap: null | string,
1167 line: number,
1168 col: number,
1169 environmentName: string,
1170 innerFunction: (...A) => Promise<T>,
1171 ): (...A) => Promise<T> {
1172 // This creates a fake copy of a Server Module. It represents the Server Action on the server.
1173 // We use an eval so we can source map it to the original location.
1174
1175 const comment =
1176 '/* This module is a proxy to a Server Action. Turn on Source Maps to see the server source. */';
1177
1178 if (!name) {
1179 // An eval:ed function with no name gets the name "eval". We give it something more descriptive.
1180 name = '<anonymous>';
1181 }
1182 const encodedName = JSON.stringify(name);
1183 // We generate code where both the beginning of the function and its parenthesis is at the line
1184 // and column of the server executed code. We use a method form since that lets us name it
1185 // anything we want and because the beginning of the function and its parenthesis is the same
1186 // column. Because Chrome inspects the location of the parenthesis and Firefox inspects the
1187 // location of the beginning of the function. By not using a function expression we avoid the
1188 // ambiguity.
1189 let code;
1190 if (line <= 1) {
1191 const minSize = encodedName.length + 7;
1192 code =
1193 's=>({' +
1194 encodedName +
1195 ' '.repeat(col < minSize ? 0 : col - minSize) +
1196 ':' +
1197 '(...args) => s(...args)' +
1198 '})\n' +
1199 comment;
1200 } else {
1201 code =
1202 comment +
1203 '\n'.repeat(line - 2) +
1204 'server=>({' +
1205 encodedName +
1206 ':\n' +
1207 ' '.repeat(col < 1 ? 0 : col - 1) +
1208 // The function body can get printed so we make it look nice.
1209 // This "calls the server with the arguments".
1210 '(...args) => server(...args)' +
1211 '})';
1212 }
1213
1214 if (filename.startsWith('/')) {
1215 // If the filename starts with `/` we assume that it is a file system file
1216 // rather than relative to the current host. Since on the server fully qualified
1217 // stack traces use the file path.
1218 // TODO: What does this look like on Windows?
1219 filename = 'file://' + filename;
1220 }
1221
1222 if (sourceMap) {
1223 // We use the prefix about://React/ to separate these from other files listed in
1224 // the Chrome DevTools. We need a "host name" and not just a protocol because
1225 // otherwise the group name becomes the root folder. Ideally we don't want to
1226 // show these at all but there's two reasons to assign a fake URL.
1227 // 1) A printed stack trace string needs a unique URL to be able to source map it.
1228 // 2) If source maps are disabled or fails, you should at least be able to tell
1229 // which file it was.
1230 code +=
1231 '\n//# sourceURL=about://React/' +
1232 encodeURIComponent(environmentName) +
1233 '/' +
1234 encodeURI(filename) +
1235 '?s' + // We add an extra s here to distinguish from the fake stack frames
1236 fakeServerFunctionIdx++;
1237 code += '\n//# sourceMappingURL=' + sourceMap;
1238 } else if (filename) {
1239 code += '\n//# sourceURL=' + filename;
1240 }
1241
1242 try {
1243 // Eval a factory and then call it to create a closure over the inner function.
1244 // eslint-disable-next-line no-eval
1245 return (0, eval)(code)(innerFunction)[name];
1246 } catch (x) {
1247 // If eval fails, such as if in an environment that doesn't support it,
1248 // we fallback to just returning the inner function.
1249 return innerFunction;
1250 }
1251 }
1252
1253 export function registerBoundServerReference<T: Function>(
1254 reference: T,
1255 id: ServerReferenceId,
1256 bound: null | Thenable<Array<any>>,
1257 encodeFormAction: void | EncodeFormActionCallback,
1258 ): void {
1259 if (knownServerReferences.has(reference)) {
1260 return;
1261 }
1262
1263 knownServerReferences.set(reference, {
1264 id,
1265 originalBind: reference.bind,
1266 bound,
1267 });
1268
1269 // Expose encoder for use by SSR, as well as a special bind that can be used to
1270 // keep server capabilities.
1271 // $FlowFixMe[constant-condition]
1272 if (usedWithSSR) {
1273 // Only expose this in builds that would actually use it. Not needed in the browser.
1274 const $$FORM_ACTION =
1275 encodeFormAction === undefined
1276 ? defaultEncodeFormAction
1277 : function (
1278 this: any => Promise<any>,
1279 identifierPrefix: string,
1280 ): ReactCustomFormAction {
1281 return customEncodeFormAction(
1282 this,
1283 identifierPrefix,
1284 encodeFormAction,
1285 );
1286 };
1287 Object.defineProperties(reference as any, {
1288 $$FORM_ACTION: {value: $$FORM_ACTION},
1289 $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1290 bind: {value: bind},
1291 });
1292 }
1293 }
1294
1295 export function registerServerReference<T: Function>(
1296 reference: T,
1297 id: ServerReferenceId,
1298 encodeFormAction?: EncodeFormActionCallback,
1299 ): ServerReference<T> {
1300 registerBoundServerReference(reference, id, null, encodeFormAction);
1301 return reference;
1302 }
1303
1304 // $FlowFixMe[method-unbinding]
1305 const FunctionBind = Function.prototype.bind;
1306 // $FlowFixMe[method-unbinding]
1307 const ArraySlice = Array.prototype.slice;
1308 function bind(this: Function): Function {
1309 const referenceClosure = knownServerReferences.get(this);
1310
1311 if (!referenceClosure) {
1312 // $FlowFixMe[incompatible-type]
1313 return FunctionBind.apply(this, arguments);
1314 }
1315
1316 const newFn = referenceClosure.originalBind.apply(this, arguments);
1317
1318 if (__DEV__) {
1319 const thisBind = arguments[0];
1320 if (thisBind != null) {
1321 // This doesn't warn in browser environments since it's not instrumented outside
1322 // usedWithSSR. This makes this an SSR only warning which we don't generally do.
1323 // TODO: Consider a DEV only instrumentation in the browser.
1324 console.error(
1325 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
1326 );
1327 }
1328 }
1329
1330 const args = ArraySlice.call(arguments, 1);
1331 let boundPromise = null;
1332 if (referenceClosure.bound !== null) {
1333 boundPromise = Promise.resolve(referenceClosure.bound as any).then(
1334 boundArgs => boundArgs.concat(args),
1335 );
1336 } else {
1337 boundPromise = Promise.resolve(args);
1338 }
1339
1340 knownServerReferences.set(newFn, {
1341 id: referenceClosure.id,
1342 originalBind: newFn.bind,
1343 bound: boundPromise,
1344 });
1345
1346 // Expose encoder for use by SSR, as well as a special bind that can be used to
1347 // keep server capabilities.
1348 // $FlowFixMe[constant-condition]
1349 if (usedWithSSR) {
1350 // Only expose this in builds that would actually use it. Not needed on the client.
1351 Object.defineProperties(newFn as any, {
1352 $$FORM_ACTION: {value: this.$$FORM_ACTION},
1353 $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1354 bind: {value: bind},
1355 });
1356 }
1357
1358 return newFn;
1359 }
1360
1361 export type FindSourceMapURLCallback = (
1362 fileName: string,
1363 environmentName: string,
1364 ) => null | string;
1365
1366 export function createBoundServerReference<A: Iterable<any>, T>(
1367 metaData: {
1368 id: ServerReferenceId,
1369 bound: null | Thenable<Array<any>>,
1370 name?: string, // DEV-only
1371 env?: string, // DEV-only
1372 location?: ReactFunctionLocation, // DEV-only
1373 },
1374 callServer: CallServerCallback,
1375 encodeFormAction?: EncodeFormActionCallback,
1376 findSourceMapURL?: FindSourceMapURLCallback, // DEV-only
1377 ): (...A) => Promise<T> {
1378 const id = metaData.id;
1379 const bound = metaData.bound;
1380 let action = function (): Promise<T> {
1381 // $FlowFixMe[method-unbinding]
1382 const args = Array.prototype.slice.call(arguments);
1383 const p = bound;
1384 if (!p) {
1385 return callServer(id, args);
1386 }
1387 if (p.status === 'fulfilled') {
1388 const boundArgs = p.value;
1389 return callServer(id, boundArgs.concat(args));
1390 }
1391 // Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
1392 // TODO: Remove the wrapper once that's fixed.
1393 return (Promise.resolve(p) as any as Promise<Array<any>>).then(
1394 function (boundArgs) {
1395 return callServer(id, boundArgs.concat(args));
1396 },
1397 );
1398 };
1399 if (__DEV__) {
1400 const location = metaData.location;
1401 if (location) {
1402 const functionName = metaData.name || '';
1403 const [, filename, line, col] = location;
1404 const env = metaData.env || 'Server';
1405 const sourceMap =
1406 findSourceMapURL == null ? null : findSourceMapURL(filename, env);
1407 action = createFakeServerFunction(
1408 functionName,
1409 filename,
1410 sourceMap,
1411 line,
1412 col,
1413 env,
1414 action,
1415 );
1416 }
1417 }
1418 registerBoundServerReference(action, id, bound, encodeFormAction);
1419 return action;
1420 }
1421
1422 // This matches either of these V8 formats.
1423 // at name (filename:0:0)
1424 // at filename:0:0
1425 // at async filename:0:0
1426 const v8FrameRegExp =
1427 /^ {3} at (?:(.+) \((.+):(\d+):(\d+)\)|(?:async )?(.+):(\d+):(\d+))$/;
1428 // This matches either of these JSC/SpiderMonkey formats.
1429 // name@filename:0:0
1430 // filename:0:0
1431 const jscSpiderMonkeyFrameRegExp = /(?:(.*)@)?(.*):(\d+):(\d+)/;
1432
1433 function parseStackLocation(error: Error): null | ReactFunctionLocation {
1434 // This parsing is special in that we know that the calling function will always
1435 // be a module that initializes the server action. We also need this part to work
1436 // cross-browser so not worth a Config. It's DEV only so not super code size
1437 // sensitive but also a non-essential feature.
1438 let stack = error.stack;
1439 if (stack.startsWith('Error: react-stack-top-frame\n')) {
1440 // V8's default formatting prefixes with the error message which we
1441 // don't want/need.
1442 stack = stack.slice(29);
1443 }
1444 const endOfFirst = stack.indexOf('\n');
1445 let secondFrame;
1446 if (endOfFirst !== -1) {
1447 // Skip the first frame.
1448 const endOfSecond = stack.indexOf('\n', endOfFirst + 1);
1449 if (endOfSecond === -1) {
1450 secondFrame = stack.slice(endOfFirst + 1);
1451 } else {
1452 secondFrame = stack.slice(endOfFirst + 1, endOfSecond);
1453 }
1454 } else {
1455 secondFrame = stack;
1456 }
1457
1458 let parsed = v8FrameRegExp.exec(secondFrame);
1459 if (!parsed) {
1460 parsed = jscSpiderMonkeyFrameRegExp.exec(secondFrame);
1461 if (!parsed) {
1462 return null;
1463 }
1464 }
1465
1466 let name = parsed[1] || '';
1467 if (name === '<anonymous>') {
1468 name = '';
1469 }
1470 let filename = parsed[2] || parsed[5] || '';
1471 if (filename === '<anonymous>') {
1472 filename = '';
1473 }
1474 // This is really the enclosingLine/Column.
1475 const line = +(parsed[3] || parsed[6]);
1476 const col = +(parsed[4] || parsed[7]);
1477
1478 return [name, filename, line, col];
1479 }
1480
1481 export function createServerReference<A: Iterable<any>, T>(
1482 id: ServerReferenceId,
1483 callServer: CallServerCallback,
1484 encodeFormAction?: EncodeFormActionCallback,
1485 findSourceMapURL?: FindSourceMapURLCallback, // DEV-only
1486 functionName?: string,
1487 ): (...A) => Promise<T> {
1488 let action = function (): Promise<T> {
1489 // $FlowFixMe[method-unbinding]
1490 const args = Array.prototype.slice.call(arguments);
1491 return callServer(id, args);
1492 };
1493 if (__DEV__) {
1494 // Let's see if we can find a source map for the file which contained the
1495 // server action. We extract it from the runtime so that it's resilient to
1496 // multiple passes of compilation as long as we can find the final source map.
1497 const location = parseStackLocation(new Error('react-stack-top-frame'));
1498 if (location !== null) {
1499 const [, filename, line, col] = location;
1500 // While the environment that the Server Reference points to can be
1501 // in any environment, what matters here is where the compiled source
1502 // is from and that's in the currently executing environment. We hard
1503 // code that as the value "Client" in case the findSourceMapURL helper
1504 // needs it.
1505 const env = 'Client';
1506 const sourceMap =
1507 findSourceMapURL == null ? null : findSourceMapURL(filename, env);
1508 action = createFakeServerFunction(
1509 functionName || '',
1510 filename,
1511 sourceMap,
1512 line,
1513 col,
1514 env,
1515 action,
1516 );
1517 }
1518 }
1519 registerBoundServerReference(action, id, null, encodeFormAction);
1520 return action;
1521 }