@@ -18,7 +18,10 @@ const temporalConstructorNames = [
1818] as const
1919
2020type TemporalConstructorName = ( typeof temporalConstructorNames ) [ number ]
21- type TemporalConstructor = { from : ( value : string ) => unknown }
21+ type TemporalConstructor = {
22+ from : ( value : string ) => unknown
23+ prototype ?: { toString ?: ( ) => string }
24+ }
2225
2326function getTemporalConstructorName (
2427 type : unknown ,
@@ -47,6 +50,33 @@ function requireTemporalConstructor(
4750 return constructor
4851}
4952
53+ function serializeTemporalValue (
54+ value : object ,
55+ name : TemporalConstructorName ,
56+ ) : string | undefined {
57+ const constructor = requireTemporalConstructor ( name )
58+ const toString = constructor . prototype ?. toString
59+ if ( typeof toString !== `function` || toString === Object . prototype . toString )
60+ return
61+ try {
62+ const serialized = toString . call ( value )
63+ return typeof serialized === `string` ? serialized : undefined
64+ } catch {
65+ // Temporal prototype methods brand-check their receiver. A matching
66+ // Symbol.toStringTag without the corresponding internal slots is user data.
67+ return
68+ }
69+ }
70+
71+ function mayBeNativeScalar ( value : object ) : boolean {
72+ const prototype = Object . getPrototypeOf ( value )
73+ return (
74+ prototype !== null &&
75+ prototype !== Object . prototype &&
76+ ! Array . isArray ( value )
77+ )
78+ }
79+
5080export class MissingTemporalConstructorError extends Error { }
5181
5282function setDataProperty (
@@ -184,29 +214,51 @@ export class TransactionSerializer {
184214 } as PendingMutation
185215 }
186216
187- private serializeValue ( value : any , jsonKey ?: string | false ) : any {
217+ private serializeValue (
218+ value : any ,
219+ jsonKey ?: string | false ,
220+ ancestors = new WeakSet < object > ( ) ,
221+ ) : any {
188222 if ( value === null || typeof value !== `object` ) return value
189223
190224 if ( jsonKey !== false && value instanceof Date ) {
191225 return { __type : `Date` , value : value . toISOString ( ) }
192226 }
193227
194228 const temporalConstructorName =
195- jsonKey !== false
229+ jsonKey !== false && mayBeNativeScalar ( value )
196230 ? getTemporalConstructorName ( value [ Symbol . toStringTag ] )
197231 : undefined
198232 if ( temporalConstructorName ) {
199- requireTemporalConstructor ( temporalConstructorName )
200- return {
201- __type : `Temporal` ,
202- type : `Temporal.${ temporalConstructorName } ` ,
203- value : value . toString ( ) ,
204- }
233+ const temporalValue = serializeTemporalValue (
234+ value ,
235+ temporalConstructorName ,
236+ )
237+ if ( temporalValue !== undefined )
238+ return {
239+ __type : `Temporal` ,
240+ type : `Temporal.${ temporalConstructorName } ` ,
241+ value : temporalValue ,
242+ }
205243 }
206244
245+ if ( ancestors . has ( value ) )
246+ throw new TypeError ( `Converting circular structure to JSON` )
247+
207248 const toJSON = typeof jsonKey === `string` && value . toJSON
208- if ( typeof toJSON === `function` )
209- return this . serializeValue ( toJSON . call ( value , jsonKey ) , false )
249+ if ( typeof toJSON === `function` ) {
250+ const replacement = toJSON . call ( value , jsonKey )
251+ if ( replacement === value ) {
252+ return this . serializeValue ( replacement , false , ancestors )
253+ }
254+
255+ ancestors . add ( value )
256+ try {
257+ return this . serializeValue ( replacement , false , ancestors )
258+ } finally {
259+ ancestors . delete ( value )
260+ }
261+ }
210262 if (
211263 jsonKey !== undefined &&
212264 ( value instanceof Boolean ||
@@ -216,20 +268,41 @@ export class TransactionSerializer {
216268 ) {
217269 return value . valueOf ( )
218270 }
271+
272+ ancestors . add ( value )
273+
219274 const isArray = Array . isArray ( value )
220275 const result : any = isArray ? [ ] : { }
221- const keys = isArray
222- ? Array . from ( { length : value . length } , ( _ , index ) => String ( index ) )
223- : Object . keys ( value )
224- for ( const key of keys ) {
225- setDataProperty (
226- result ,
227- key ,
228- this . serializeValue (
229- value [ key ] ,
230- jsonKey === undefined ? undefined : key ,
231- ) ,
232- )
276+ try {
277+ if ( isArray ) {
278+ const length = value . length
279+ for ( let index = 0 ; index < length ; index ++ ) {
280+ const key = String ( index )
281+ setDataProperty (
282+ result ,
283+ key ,
284+ this . serializeValue (
285+ value [ index ] ,
286+ jsonKey === undefined ? undefined : key ,
287+ ancestors ,
288+ ) ,
289+ )
290+ }
291+ } else {
292+ for ( const key of Object . keys ( value ) ) {
293+ setDataProperty (
294+ result ,
295+ key ,
296+ this . serializeValue (
297+ value [ key ] ,
298+ jsonKey === undefined ? undefined : key ,
299+ ancestors ,
300+ ) ,
301+ )
302+ }
303+ }
304+ } finally {
305+ ancestors . delete ( value )
233306 }
234307 if ( jsonKey === false && typeof result . toJSON === `function` )
235308 delete result . toJSON
0 commit comments