@@ -5,10 +5,38 @@ 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+ * Circular references are replaced with undefined to make the object serializable.
15+ */
16+ function cloneWithoutCircularRefs < T > ( obj : T , seen = new WeakSet ( ) ) : T {
17+ if ( obj === null || typeof obj !== 'object' ) {
18+ return obj ;
19+ }
20+
21+ if ( seen . has ( obj ) ) {
22+ return undefined as T ;
23+ }
24+
25+ seen . add ( obj ) ;
26+
27+ if ( Array . isArray ( obj ) ) {
28+ return obj . map ( ( item ) => cloneWithoutCircularRefs ( item , seen ) ) as T ;
29+ }
30+
31+ const clone : Record < string , unknown > = { } ;
32+
33+ for ( const key of Object . keys ( obj ) ) {
34+ clone [ key ] = cloneWithoutCircularRefs ( ( obj as Record < string , unknown > ) [ key ] , seen ) ;
35+ }
36+
37+ return clone as T ;
38+ }
39+
1240/**
1341 * Serializer for converting between Deepnote YAML files and VS Code notebook format.
1442 * Handles reading/writing .deepnote files and manages project state persistence.
@@ -108,49 +136,63 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
108136 }
109137
110138 try {
139+ logger . debug ( 'SerializeNotebook: Starting serialization' ) ;
140+
111141 const projectId = data . metadata ?. deepnoteProjectId ;
112142
113143 if ( ! projectId ) {
114144 throw new Error ( 'Missing Deepnote project ID in notebook metadata' ) ;
115145 }
116146
147+ logger . debug ( `SerializeNotebook: Project ID: ${ projectId } ` ) ;
148+
117149 const originalProject = this . notebookManager . getOriginalProject ( projectId ) as DeepnoteFile | undefined ;
118150
119151 if ( ! originalProject ) {
120152 throw new Error ( 'Original Deepnote project not found. Cannot save changes.' ) ;
121153 }
122154
155+ logger . debug ( 'SerializeNotebook: Got original project' ) ;
156+
123157 const notebookId =
124158 data . metadata ?. deepnoteNotebookId || this . notebookManager . getTheSelectedNotebookForAProject ( projectId ) ;
125159
126160 if ( ! notebookId ) {
127161 throw new Error ( 'Cannot determine which notebook to save' ) ;
128162 }
129163
130- const notebookIndex = originalProject . project . notebooks . findIndex (
131- ( nb : { id : string } ) => nb . id === notebookId
132- ) ;
164+ logger . debug ( `SerializeNotebook: Notebook ID: ${ notebookId } ` ) ;
165+
166+ const notebook = originalProject . project . notebooks . find ( ( nb : { id : string } ) => nb . id === notebookId ) ;
133167
134- if ( notebookIndex === - 1 ) {
168+ if ( ! notebook ) {
135169 throw new Error ( `Notebook with ID ${ notebookId } not found in project` ) ;
136170 }
137171
138- const updatedProject = JSON . parse ( JSON . stringify ( originalProject ) ) as DeepnoteFile ;
172+ logger . debug ( `SerializeNotebook: Found notebook, converting ${ data . cells . length } cells to blocks` ) ;
173+
174+ // Clone blocks while removing circular references that may have been
175+ // introduced by VS Code's notebook cell/output handling
176+ const blocks = this . converter . convertCellsToBlocks ( data . cells ) ;
177+
178+ logger . debug ( `SerializeNotebook: Converted to ${ blocks . length } blocks, now cloning without circular refs` ) ;
179+
180+ notebook . blocks = cloneWithoutCircularRefs < DeepnoteBlock [ ] > ( blocks ) ;
139181
140- const updatedBlocks = this . converter . convertCellsToBlocks ( data . cells ) ;
182+ logger . debug ( 'SerializeNotebook: Cloned blocks, updating modifiedAt' ) ;
141183
142- updatedProject . project . notebooks [ notebookIndex ] . blocks = updatedBlocks ;
184+ originalProject . metadata . modifiedAt = new Date ( ) . toISOString ( ) ;
143185
144- updatedProject . metadata . modifiedAt = new Date ( ) . toISOString ( ) ;
186+ logger . debug ( 'SerializeNotebook: Starting yaml.dump' ) ;
145187
146- const yamlString = yaml . dump ( updatedProject , {
188+ const yamlString = yaml . dump ( originalProject , {
147189 indent : 2 ,
148190 lineWidth : - 1 ,
149191 noRefs : true ,
150192 sortKeys : false
151193 } ) ;
152194
153- this . notebookManager . storeOriginalProject ( projectId , updatedProject , notebookId ) ;
195+ logger . debug ( `SerializeNotebook: yaml.dump complete, ${ yamlString . length } chars` ) ;
154196
155197 return new TextEncoder ( ) . encode ( yamlString ) ;
156198 } catch ( error ) {
0 commit comments