@@ -183,12 +183,12 @@ function serializeParallelExecutions(
183183 return result
184184}
185185
186- export function serializePauseSnapshot (
186+ function buildExecutionSnapshot (
187187 context : ExecutionContext ,
188188 triggerBlockIds : string [ ] ,
189189 dag ?: DAG ,
190190 edgeManager ?: EdgeManager
191- ) : SerializedSnapshot {
191+ ) : { snapshot : ExecutionSnapshot ; state : SerializableExecutionState } {
192192 const metadataFromContext = context . metadata as ExecutionMetadata | undefined
193193 let useDraftState : boolean
194194 if ( metadataFromContext ?. useDraftState !== undefined ) {
@@ -316,9 +316,113 @@ export function serializePauseSnapshot(
316316 context . selectedOutputs ,
317317 state
318318 )
319+ return { snapshot, state }
320+ }
319321
322+ export function serializePauseSnapshot (
323+ context : ExecutionContext ,
324+ triggerBlockIds : string [ ] ,
325+ dag ?: DAG ,
326+ edgeManager ?: EdgeManager
327+ ) : SerializedSnapshot {
320328 return {
321- snapshot : snapshot . toJSON ( ) ,
329+ snapshot : buildExecutionSnapshot ( context , triggerBlockIds , dag , edgeManager ) . snapshot . toJSON ( ) ,
322330 triggerIds : triggerBlockIds ,
323331 }
324332}
333+
334+ const LIVE_EXECUTION_STATE = Symbol ( 'liveExecutionState' )
335+
336+ /**
337+ * Throws exactly where `JSON.stringify(value)` would — a cycle or a BigInt,
338+ * after applying `toJSON` — without building the string. Iterative, so nesting
339+ * that native serialization handles cannot overflow the JS stack here.
340+ */
341+ function assertJsonSerializable ( value : unknown ) : void {
342+ type Frame = { node : object ; keys : string [ ] | undefined ; length : number ; index : number }
343+ const ancestors = new Set < object > ( )
344+ const stack : Frame [ ] = [ ]
345+
346+ const enter = ( raw : unknown , key : string ) : void => {
347+ let current = raw
348+ if (
349+ ( typeof current === 'object' && current !== null ) ||
350+ typeof current === 'function' ||
351+ typeof current === 'bigint'
352+ ) {
353+ const toJSON = ( current as { toJSON ?: unknown } ) . toJSON
354+ if ( typeof toJSON === 'function' ) current = toJSON . call ( current , key )
355+ }
356+ if ( typeof current === 'bigint' || current instanceof BigInt ) {
357+ throw new TypeError ( 'Do not know how to serialize a BigInt' )
358+ }
359+ if ( typeof current !== 'object' || current === null ) return
360+ // Serialized as their primitive value; their own properties are never read.
361+ if ( current instanceof Number || current instanceof String || current instanceof Boolean ) {
362+ return
363+ }
364+ if ( ancestors . has ( current ) ) {
365+ throw new TypeError ( 'Converting circular structure to JSON' )
366+ }
367+ ancestors . add ( current )
368+ const keys = Array . isArray ( current ) ? undefined : Object . keys ( current )
369+ stack . push ( {
370+ node : current ,
371+ keys,
372+ length : keys ? keys . length : ( current as unknown [ ] ) . length ,
373+ index : 0 ,
374+ } )
375+ }
376+
377+ enter ( value , '' )
378+ while ( stack . length > 0 ) {
379+ const frame = stack [ stack . length - 1 ]
380+ if ( frame . index >= frame . length ) {
381+ ancestors . delete ( frame . node )
382+ stack . pop ( )
383+ continue
384+ }
385+ const index = frame . index ++
386+ const key = frame . keys ? frame . keys [ index ] : String ( index )
387+ enter ( ( frame . node as Record < string , unknown > ) [ key ] , key )
388+ }
389+ }
390+
391+ /**
392+ * Execution state for a run whose blocks have all settled. Validates and fails
393+ * exactly like `JSON.parse(serializePauseSnapshot(...).snapshot).state`, but
394+ * skips that full JSON clone: block logs and block states are shallow copies
395+ * sharing their inputs and outputs with the run, and JSON normalization is left
396+ * to whoever serializes the state.
397+ */
398+ export function buildCompletedExecutionState (
399+ context : ExecutionContext ,
400+ dag ?: DAG ,
401+ edgeManager ?: EdgeManager
402+ ) : SerializableExecutionState {
403+ const { snapshot, state } = buildExecutionSnapshot ( context , [ ] , dag , edgeManager )
404+ assertJsonSerializable ( snapshot . toSerializable ( ) )
405+ const completed : SerializableExecutionState = {
406+ ...state ,
407+ blockLogs : state . blockLogs . map ( ( log ) => ( { ...log } ) ) ,
408+ blockStates : Object . fromEntries (
409+ Object . entries ( state . blockStates ) . map ( ( [ blockId , blockState ] ) => [ blockId , { ...blockState } ] )
410+ ) ,
411+ }
412+ // Enumerable so object spreads carry it; JSON serialization ignores symbol keys.
413+ Object . defineProperty ( completed , LIVE_EXECUTION_STATE , { value : true , enumerable : true } )
414+ return completed
415+ }
416+
417+ /**
418+ * Whether execution state came from {@link buildCompletedExecutionState} (or a
419+ * spread of it) and so still holds live, not-yet-JSON-normalized values.
420+ * Other states came out of a JSON round-trip already.
421+ */
422+ export function isLiveExecutionState ( state : unknown ) : boolean {
423+ return (
424+ typeof state === 'object' &&
425+ state !== null &&
426+ ( state as Record < symbol , unknown > ) [ LIVE_EXECUTION_STATE ] === true
427+ )
428+ }
0 commit comments