@@ -5,10 +5,43 @@ import { l10n, workspace, type CancellationToken, type NotebookData, type Notebo
55import { logger } from '../../platform/logging' ;
66import { IDeepnoteNotebookManager } from '../types' ;
77import { DeepnoteDataConverter } from './deepnoteDataConverter' ;
8- import type { DeepnoteFile , DeepnoteNotebook } from '../../platform/deepnote/deepnoteTypes' ;
8+ import type { DeepnoteBlock , DeepnoteFile , DeepnoteNotebook } from '../../platform/deepnote/deepnoteTypes' ;
99
1010export { DeepnoteBlock , DeepnoteNotebook , DeepnoteOutput , DeepnoteFile } from '../../platform/deepnote/deepnoteTypes' ;
1111
12+ /**
13+ * Deep clones an object while removing circular references.
14+ * Uses a recursion stack pattern to only drop true cycles, preserving shared references.
15+ */
16+ function cloneWithoutCircularRefs < T > ( obj : T , seen = new WeakSet < object > ( ) ) : T {
17+ if ( obj === null || typeof obj !== 'object' ) {
18+ return obj ;
19+ }
20+
21+ if ( seen . has ( obj as object ) ) {
22+ // True circular reference on the current path - drop it
23+ return undefined as T ;
24+ }
25+
26+ seen . add ( obj as object ) ;
27+
28+ try {
29+ if ( Array . isArray ( obj ) ) {
30+ return obj . map ( ( item ) => cloneWithoutCircularRefs ( item , seen ) ) as T ;
31+ }
32+
33+ const clone : Record < string , unknown > = { } ;
34+
35+ for ( const key of Object . keys ( obj as Record < string , unknown > ) ) {
36+ clone [ key ] = cloneWithoutCircularRefs ( ( obj as Record < string , unknown > ) [ key ] , seen ) ;
37+ }
38+
39+ return clone as T ;
40+ } finally {
41+ seen . delete ( obj as object ) ;
42+ }
43+ }
44+
1245/**
1346 * Serializer for converting between Deepnote YAML files and VS Code notebook format.
1447 * Handles reading/writing .deepnote files and manages project state persistence.
@@ -108,49 +141,63 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
108141 }
109142
110143 try {
144+ logger . debug ( 'SerializeNotebook: Starting serialization' ) ;
145+
111146 const projectId = data . metadata ?. deepnoteProjectId ;
112147
113148 if ( ! projectId ) {
114149 throw new Error ( 'Missing Deepnote project ID in notebook metadata' ) ;
115150 }
116151
152+ logger . debug ( `SerializeNotebook: Project ID: ${ projectId } ` ) ;
153+
117154 const originalProject = this . notebookManager . getOriginalProject ( projectId ) as DeepnoteFile | undefined ;
118155
119156 if ( ! originalProject ) {
120157 throw new Error ( 'Original Deepnote project not found. Cannot save changes.' ) ;
121158 }
122159
160+ logger . debug ( 'SerializeNotebook: Got original project' ) ;
161+
123162 const notebookId =
124163 data . metadata ?. deepnoteNotebookId || this . notebookManager . getTheSelectedNotebookForAProject ( projectId ) ;
125164
126165 if ( ! notebookId ) {
127166 throw new Error ( 'Cannot determine which notebook to save' ) ;
128167 }
129168
130- const notebookIndex = originalProject . project . notebooks . findIndex (
131- ( nb : { id : string } ) => nb . id === notebookId
132- ) ;
169+ logger . debug ( `SerializeNotebook: Notebook ID: ${ notebookId } ` ) ;
170+
171+ const notebook = originalProject . project . notebooks . find ( ( nb : { id : string } ) => nb . id === notebookId ) ;
133172
134- if ( notebookIndex === - 1 ) {
173+ if ( ! notebook ) {
135174 throw new Error ( `Notebook with ID ${ notebookId } not found in project` ) ;
136175 }
137176
138- const updatedProject = JSON . parse ( JSON . stringify ( originalProject ) ) as DeepnoteFile ;
177+ logger . debug ( `SerializeNotebook: Found notebook, converting ${ data . cells . length } cells to blocks` ) ;
178+
179+ // Clone blocks while removing circular references that may have been
180+ // introduced by VS Code's notebook cell/output handling
181+ const blocks = this . converter . convertCellsToBlocks ( data . cells ) ;
182+
183+ logger . debug ( `SerializeNotebook: Converted to ${ blocks . length } blocks, now cloning without circular refs` ) ;
184+
185+ notebook . blocks = cloneWithoutCircularRefs < DeepnoteBlock [ ] > ( blocks ) ;
139186
140- const updatedBlocks = this . converter . convertCellsToBlocks ( data . cells ) ;
187+ logger . debug ( 'SerializeNotebook: Cloned blocks, updating modifiedAt' ) ;
141188
142- updatedProject . project . notebooks [ notebookIndex ] . blocks = updatedBlocks ;
189+ originalProject . metadata . modifiedAt = new Date ( ) . toISOString ( ) ;
143190
144- updatedProject . metadata . modifiedAt = new Date ( ) . toISOString ( ) ;
191+ logger . debug ( 'SerializeNotebook: Starting yaml.dump' ) ;
145192
146- const yamlString = yaml . dump ( updatedProject , {
193+ const yamlString = yaml . dump ( originalProject , {
147194 indent : 2 ,
148195 lineWidth : - 1 ,
149196 noRefs : true ,
150197 sortKeys : false
151198 } ) ;
152199
153- this . notebookManager . storeOriginalProject ( projectId , updatedProject , notebookId ) ;
200+ logger . debug ( `SerializeNotebook: yaml.dump complete, ${ yamlString . length } chars` ) ;
154201
155202 return new TextEncoder ( ) . encode ( yamlString ) ;
156203 } catch ( error ) {
0 commit comments