Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions jupyter_server_documents/outputs/handlers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import json

from tornado import web

from jupyter_server.auth.decorator import authorized
from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join


class OutputsAPIHandler(APIHandler):
Expand All @@ -18,7 +15,7 @@ def outputs(self):

@web.authenticated
@authorized
async def get(self, file_id=None, cell_id=None, output_index=None):
async def get(self, file_id, cell_id=None, output_index=None):
try:
if output_index:
output = self.outputs.get_output(file_id, cell_id, output_index)
Expand All @@ -35,6 +32,19 @@ async def get(self, file_id=None, cell_id=None, output_index=None):
self.write(output)
self.finish(set_content_type=content_type)

@web.authenticated
@authorized
async def delete(self, file_id, cell_id=None, output_index=None):
# output_index is accepted but ignored as we clear all cell outputs regardless
try:
self.outputs.clear(file_id, cell_id)
except (FileNotFoundError):
self.set_status(404)
self.finish({"error": "Output not found."})
else:
self.set_status(200)
self.finish()


class StreamAPIHandler(APIHandler):
"""An outputs service API handler."""
Expand Down Expand Up @@ -69,7 +79,7 @@ async def get(self, file_id=None, cell_id=None):

_file_id_regex = r"(?P<file_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})"
# In nbformat, cell_ids follow this format, compatible with uuid4
_cell_id_regex = rf"(?P<cell_id>[a-zA-Z0-9_-]+)"
_cell_id_regex = r"(?P<cell_id>[a-zA-Z0-9_-]+)"

# non-negative integers
_output_index_regex = r"(?P<output_index>0|[1-9]\d*)"
Expand Down
4 changes: 3 additions & 1 deletion jupyter_server_documents/rooms/yroom.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ def _init_awareness(self, ydoc: pycrdt.Doc) -> pycrdt.Awareness:
`awareness.unobserve(self._awareness_subscription)`.
"""
self._awareness = pycrdt.Awareness(ydoc=ydoc)
if self.room_id != "JupyterLab:globalAwareness":
file_format, file_type, file_id = self.room_id.split(":")
self._awareness.set_local_state_field("file_id", file_id)
self._awareness_subscription = self._awareness.observe(
self._on_awareness_update
)
Expand Down Expand Up @@ -325,7 +328,6 @@ def _init_jupyter_ydoc(self, ydoc: pycrdt.Doc, awareness: pycrdt.Awareness) -> Y
self._jupyter_ydoc.observe(self._on_jupyter_ydoc_update)
return self._jupyter_ydoc


@property
def clients(self) -> YjsClientGroup:
"""
Expand Down
51 changes: 50 additions & 1 deletion src/notebook-factory/notebook-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ICodeCellModel
} from '@jupyterlab/cells';
import { IChangedArgs } from '@jupyterlab/coreutils';
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
import { Notebook, NotebookPanel, NotebookActions } from '@jupyterlab/notebook';
import { CellChange, createMutex, ISharedCodeCell } from '@jupyter/ydoc';
import { IOutputAreaModel, OutputAreaModel } from '@jupyterlab/outputarea';
import { requestAPI } from '../handler';
Expand Down Expand Up @@ -340,3 +340,52 @@ export class RtcNotebookContentFactory
return new ResettableNotebook(options);
}
}

// Add a handler for the outputCleared signal
NotebookActions.outputCleared.connect((sender, args) => {
const { notebook, cell } = args;
const cellId = cell.model.sharedModel.getId();
const awareness = notebook.model?.sharedModel.awareness;
const awarenessStates = awareness?.getStates();

// FIRST: Clear outputs in YDoc for immediate real-time sync to all clients
try {
const sharedCodeCell = cell.model.sharedModel as ISharedCodeCell;
sharedCodeCell.setOutputs([]);
console.debug(`Cleared outputs in YDoc for cell ${cellId}`);
} catch (error: unknown) {
console.error('Error clearing YDoc outputs:', error);
}

if (awarenessStates?.size === 0) {
console.log('Could not delete cell output, awareness is not present');
return; // Early return since we can't get fileId without awareness
}

let fileId = null;
for (const [_, state] of awarenessStates || []) {
if (state && 'file_id' in state) {
fileId = state['file_id'];
}
}

if (fileId === null) {
console.error('No fileId found in awareness');
return; // Early return since we can't make API call without fileId
}

// SECOND: Send API request to clear outputs from disk storage
try {
requestAPI(`/api/outputs/${fileId}/${cellId}`, {
method: 'DELETE'
})
.then(() => {
console.debug(`Successfully cleared outputs from disk for cell ${cellId}`);
})
.catch((error: Error) => {
console.error(`Failed to clear outputs from disk for cell ${cellId}:`, error);
});
} catch (error: unknown) {
console.error('Error in disk output clearing process:', error);
}
});
Loading