Skip to content

Commit 8d033ab

Browse files
committed
Save current hash on the document
1 parent 7ea2cd5 commit 8d033ab

File tree

3 files changed

+82
-5
lines changed

3 files changed

+82
-5
lines changed

packages/docprovider/src/ydrive.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
55
import { TranslationBundle } from '@jupyterlab/translation';
66
import { Contents, Drive, User } from '@jupyterlab/services';
7+
import { ISignal, Signal } from '@lumino/signaling';
78

89
import { DocumentChange, ISharedDocument, YDocument } from '@jupyter/ydoc';
910

@@ -45,6 +46,10 @@ export class YDrive extends Drive implements ICollaborativeDrive {
4546
this._providers = new Map<string, WebSocketProvider>();
4647

4748
this.sharedModelFactory = new SharedModelFactory(this._onCreate);
49+
super.fileChanged.connect((_, change) => {
50+
// pass through any events from the Drive superclass
51+
this._ydriveFileChanged.emit(change);
52+
});
4853
}
4954

5055
/**
@@ -84,7 +89,7 @@ export class YDrive extends Drive implements ICollaborativeDrive {
8489
const provider = this._providers.get(key);
8590

8691
if (provider) {
87-
// If the document does't exist, `super.get` will reject with an
92+
// If the document doesn't exist, `super.get` will reject with an
8893
// error and the provider will never be resolved.
8994
// Use `Promise.all` to reject as soon as possible. The Context will
9095
// show a dialog to the user.
@@ -132,6 +137,13 @@ export class YDrive extends Drive implements ICollaborativeDrive {
132137
return super.save(localPath, options);
133138
}
134139

140+
/**
141+
* A signal emitted when a file operation takes place.
142+
*/
143+
get fileChanged(): ISignal<this, Contents.IChangedArgs> {
144+
return this._ydriveFileChanged;
145+
}
146+
135147
private _onCreate = (
136148
options: Contents.ISharedFactoryOptions,
137149
sharedModel: YDocument<DocumentChange>
@@ -161,6 +173,51 @@ export class YDrive extends Drive implements ICollaborativeDrive {
161173
const key = `${options.format}:${options.contentType}:${options.path}`;
162174
this._providers.set(key, provider);
163175

176+
sharedModel.changed.connect(async (_, change) => {
177+
// TODO: make use of the hash
178+
if (!change.stateChange) {
179+
return;
180+
}
181+
const hashChanges = change.stateChange.filter(
182+
change => change.name === 'hash'
183+
);
184+
if (hashChanges.length === 0) {
185+
return;
186+
}
187+
if (hashChanges.length > 1) {
188+
console.error(
189+
'Unexpected multiple changes to hash value in a single transaction'
190+
);
191+
}
192+
const hashChange = hashChanges[0];
193+
194+
// A change in hash signifies that a save occurred on the server-side
195+
// (e.g. a collaborator performed the save) - we want notify the observers
196+
// about this change so that they can store the new hash value.
197+
198+
const model = await this.get(options.path, { content: false });
199+
/*
200+
this._ydriveFileChanged.emit({
201+
type: 'server-side-save',
202+
newValue: {...model, hash: hashChange.newValue},
203+
// we do not have the old model because it was discarded when server made the change,
204+
// we only have the old hash here (which may be empty if the file was newly created!)
205+
oldValue: {hash: hashChange.oldValue}
206+
});
207+
*/
208+
// TODO: add handler for `server-side-save` in
209+
// https://github.com/jupyterlab/jupyterlab/blob/dca1ec376c66038b8df7001d32cf058c70fcd717/packages/docregistry/src/context.ts#L410-L444
210+
// For now, fake it:
211+
// it happens that "rename" will perform the update of context's internal
212+
// contentsModel (which we desire to solve the spurious "File Changed" dialog)
213+
// even if file path has not changed.
214+
this._ydriveFileChanged.emit({
215+
type: 'rename',
216+
newValue: { ...model, hash: hashChange.newValue },
217+
oldValue: { ...model, hash: hashChange.oldValue }
218+
});
219+
});
220+
164221
sharedModel.disposed.connect(() => {
165222
const provider = this._providers.get(key);
166223
if (provider) {
@@ -190,6 +247,7 @@ export class YDrive extends Drive implements ICollaborativeDrive {
190247
private _trans: TranslationBundle;
191248
private _providers: Map<string, WebSocketProvider>;
192249
private _globalAwareness: Awareness | null;
250+
private _ydriveFileChanged = new Signal<this, Contents.IChangedArgs>(this);
193251
}
194252

195253
/**

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ async def load_content(self, format: str, file_type: str) -> dict[str, Any]:
117117
self.last_modified = model["last_modified"]
118118
return model
119119

120-
async def maybe_save_content(self, model: dict[str, Any]) -> None:
120+
async def maybe_save_content(self, model: dict[str, Any]) -> dict[str, Any] | None:
121121
"""
122122
Save the content of the file.
123123
@@ -149,20 +149,34 @@ async def maybe_save_content(self, model: dict[str, Any]) -> None:
149149
# otherwise it could corrupt the file
150150
done_saving = asyncio.Event()
151151
task = asyncio.create_task(self._save_content(model, done_saving))
152+
saved_model = None
152153
try:
153-
await asyncio.shield(task)
154+
saved_model = await asyncio.shield(task)
154155
except asyncio.CancelledError:
155156
pass
156157
await done_saving.wait()
158+
return saved_model
157159
else:
158160
# file changed on disk, raise an error
159161
self.last_modified = m["last_modified"]
160162
raise OutOfBandChanges
161163

162-
async def _save_content(self, model: dict[str, Any], done_saving: asyncio.Event) -> None:
164+
async def _save_content(
165+
self, model: dict[str, Any], done_saving: asyncio.Event
166+
) -> dict[str, Any]:
163167
try:
164168
m = await ensure_async(self._contents_manager.save(model, self.path))
165169
self.last_modified = m["last_modified"]
170+
# TODO, get rid of the extra `get` here once upstream issue:
171+
# https://github.com/jupyter-server/jupyter_server/issues/1453 is resolved
172+
model_with_hash = await ensure_async(
173+
self._contents_manager.get(
174+
self.path,
175+
content=False,
176+
require_hash=True, # TODO require version supporting hash
177+
)
178+
)
179+
return {**m, "hash": model_with_hash["hash"]}
166180
finally:
167181
done_saving.set()
168182

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> No
271271
await asyncio.sleep(self._save_delay)
272272

273273
self.log.info("Saving the content from room %s", self._room_id)
274-
await self._file.maybe_save_content(
274+
saved_model = await self._file.maybe_save_content(
275275
{
276276
"format": self._file_format,
277277
"type": self._file_type,
@@ -280,6 +280,11 @@ async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> No
280280
)
281281
async with self._update_lock:
282282
self._document.dirty = False
283+
if saved_model:
284+
self._document.hash = saved_model["hash"]
285+
# for now circumvent the public API, TODO remove before undrafting,
286+
# once https://github.com/jupyter-server/jupyter_ydoc/pull/262 is merged
287+
self._document._ystate["hash"] = saved_model["hash"]
283288

284289
self._emit(LogLevel.INFO, "save", "Content saved.")
285290

0 commit comments

Comments
 (0)