Skip to content

Commit d78217c

Browse files
Carreaukrassowski
andauthored
Save immediately when triggering manual save (#540)
* Save immediately when triggering manual save Should fix #537. I'm not sure between a parameter, and have a different function. And not quite sure why the require-hash was missing. * Update projects/jupyter-server-ydoc/jupyter_server_ydoc/test_utils.py Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com> * reformat with pre-commit * fix type * better error message --------- Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>
1 parent fdf409b commit d78217c

File tree

4 files changed

+128
-8
lines changed

4 files changed

+128
-8
lines changed

projects/jupyter-server-ydoc/jupyter_server_ydoc/rooms.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,20 +278,28 @@ def _save_to_disc(self):
278278
return
279279

280280
self._saving_document = asyncio.create_task(
281-
self._maybe_save_document(self._saving_document)
281+
self._maybe_save_document(self._saving_document, save_now=True)
282282
)
283283
return self._saving_document
284284

285-
async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> None:
285+
async def _maybe_save_document(
286+
self, saving_document: asyncio.Task | None, save_now: bool = False
287+
) -> None:
286288
"""
287289
Saves the content of the document to disk.
288290
289291
### Note:
290292
There is a save delay to debounce the save since we could receive a high
291293
amount of changes in a short period of time. This way we can cancel the
292-
previous save.
294+
previous save. When save_now is True, the delay is skipped and the save
295+
executes immediately.
296+
297+
Parameters:
298+
saving_document: The previous saving task to cancel if needed.
299+
save_now: If True, skip the debounce delay, and save immediately.
300+
This is used when manually saving.
293301
"""
294-
if self._save_delay is None:
302+
if self._save_delay is None and not save_now:
295303
return
296304
if saving_document is not None and not saving_document.done():
297305
# the document is being saved, cancel that
@@ -301,8 +309,10 @@ async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> No
301309
# because this coroutine is run in a cancellable task and cancellation is handled here
302310

303311
try:
304-
# save after X seconds of inactivity
305-
await asyncio.sleep(self._save_delay)
312+
# When save_now is False, wait X seconds of inactivity before saving (auto-save).
313+
# When save_now is True, save immediately without debounce delay (manual save).
314+
if not save_now and self._save_delay is not None:
315+
await asyncio.sleep(self._save_delay)
306316

307317
self.log.info("Saving the content from room %s", self._room_id)
308318
saved_model = await self._file.maybe_save_content(

projects/jupyter-server-ydoc/jupyter_server_ydoc/test_utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,19 @@ def __init__(self, model: dict):
3333
"mimetype": None,
3434
"size": 0,
3535
"writable": False,
36+
"hash": "fake_hash",
3637
}
3738
self.model.update(model)
3839

3940
self.actions: list[str] = []
4041

4142
def get(
42-
self, path: str, content: bool = True, format: str | None = None, type: str | None = None
43+
self,
44+
path: str,
45+
content: bool = True,
46+
format: str | None = None,
47+
type: str | None = None,
48+
require_hash: bool | None = None,
4349
) -> dict:
4450
if not self.model:
4551
raise HTTPError(404, f"File not found: {path}")

tests/test_documents.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ async def connect(file_format, file_type, file_path):
6969
tg.start_soon(connect, file_format, file_type, file_path)
7070
tg.start_soon(connect, file_format, file_type, file_path)
7171
t1 = time()
72-
assert t1 - t0 < 0.5
72+
delta = t1 - t0
73+
assert delta < 0.5
7374

7475
await cleanup(jp_serverapp)
7576

tests/test_rooms.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,109 @@ async def test_should_save_content_when_at_least_one_client_has_autosave_enabled
121121
assert "save" in cm.actions
122122

123123

124+
async def test_manual_save_should_not_have_delay(
125+
rtc_create_mock_document_room,
126+
):
127+
content = "test"
128+
cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=0.5)
129+
130+
await room.initialize()
131+
132+
# Trigger a manual save
133+
room._save_to_disc()
134+
135+
# Manual save should execute immediately, without waiting for the 0.5s delay
136+
# Check that save happens within a very short time (100ms should be enough)
137+
await asyncio.sleep(0.1)
138+
139+
assert cm.actions.count("save") == 1
140+
141+
142+
async def test_manual_save_with_pending_autosave_should_cancel_autosave(
143+
rtc_create_mock_document_room,
144+
):
145+
content = "test"
146+
cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=1.0)
147+
148+
await room.initialize()
149+
150+
room._document.source = "Test 2"
151+
152+
await asyncio.sleep(0.1)
153+
154+
assert cm.actions.count("save") == 0
155+
156+
save_task = room._save_to_disc()
157+
158+
# Manual save should execute immediately
159+
await asyncio.sleep(0.1)
160+
assert save_task.done()
161+
162+
# Check that the manual save was recorded
163+
assert cm.actions.count("save") == 1
164+
165+
await asyncio.sleep(1.0)
166+
167+
# There should be only one save (the manual one), not two
168+
assert cm.actions.count("save") == 1
169+
170+
171+
async def test_manual_save_should_execute_immediately_even_with_long_delay(
172+
rtc_create_mock_document_room,
173+
):
174+
content = "test"
175+
cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=5.0)
176+
177+
await room.initialize()
178+
179+
save_task = room._save_to_disc()
180+
181+
await asyncio.sleep(0.5)
182+
183+
assert "save" in cm.actions
184+
assert save_task.done()
185+
186+
187+
async def test_autosave_should_still_have_delay(
188+
rtc_create_mock_document_room,
189+
):
190+
content = "test"
191+
save_delay = 0.3
192+
cm, _, room = rtc_create_mock_document_room(
193+
"test-id", "test.txt", content, save_delay=save_delay
194+
)
195+
196+
await room.initialize()
197+
198+
room._document.source = "Test 3"
199+
200+
await asyncio.sleep(0.1)
201+
assert "save" not in cm.actions
202+
203+
# Wait for the delay to complete
204+
await asyncio.sleep(save_delay)
205+
206+
assert "save" in cm.actions
207+
208+
209+
async def test_manual_save_should_work_when_save_delay_is_none_and_save_now_is_true(
210+
rtc_create_mock_document_room,
211+
):
212+
"""Test that manual saves execute even when save_delay is None."""
213+
content = "test"
214+
# When save_delay is None, autosave is disabled
215+
cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=None)
216+
217+
await room.initialize()
218+
219+
# Trigger a manual save with save_now=True
220+
# Even though save_delay is None, manual saves should still work
221+
await room._maybe_save_document(None, save_now=True)
222+
223+
# Manual save should have executed
224+
assert cm.actions.count("save") == 1
225+
226+
124227
# The following test should be restored when package versions are fixed.
125228

126229
# async def test_document_path(rtc_create_mock_document_room):

0 commit comments

Comments
 (0)