Skip to content

Commit 9b4afdb

Browse files
committed
enh(yjs): store baseVersionEtag alongside doc
... and use it to check if the server is still on the same session. Signed-off-by: Max <[email protected]>
1 parent 2bc387d commit 9b4afdb

File tree

4 files changed

+73
-20
lines changed

4 files changed

+73
-20
lines changed

src/components/Editor.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,10 @@ export default defineComponent({
228228
})
229229
const ydoc = new Doc()
230230
const awareness = new Awareness(ydoc)
231-
useIndexedDbProvider(props, ydoc)
231+
const { getBaseVersionEtag, setBaseVersionEtag } = useIndexedDbProvider(
232+
props,
233+
ydoc,
234+
)
232235
233236
const hasConnectionIssue = ref(false)
234237
const { delayed: requireReconnect } = useDelayedFlag(hasConnectionIssue)
@@ -237,7 +240,11 @@ export default defineComponent({
237240
isRichEditor,
238241
props,
239242
)
240-
const { connection, openConnection } = provideConnection(props)
243+
const { connection, openConnection } = provideConnection(
244+
props,
245+
getBaseVersionEtag,
246+
setBaseVersionEtag,
247+
)
241248
const { syncService } = provideSyncService(connection, openConnection)
242249
const extensions = [
243250
Autofocus.configure({ fileId: props.fileId }),

src/composables/useConnection.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,34 @@ export const openDataKey = Symbol('text:opendata') as InjectionKey<
4141
* @param props.relativePath Relative path to the file.
4242
* @param props.initialSession Initial session handed to the editor in direct editing
4343
* @param props.shareToken Share token of the file.
44+
* @param getBaseVersionEtag Async getter function for the base version etag.
45+
* @param setBaseVersionEtag Async setter function for the base version etag.
4446
*/
45-
export function provideConnection(props: {
46-
fileId: number
47-
relativePath: string
48-
initialSession?: InitialData
49-
shareToken?: string
50-
}) {
51-
let baseVersionEtag: string | undefined
47+
export function provideConnection(
48+
props: {
49+
fileId: number
50+
relativePath: string
51+
initialSession?: InitialData
52+
shareToken?: string
53+
},
54+
getBaseVersionEtag: () => Promise<string | undefined>,
55+
setBaseVersionEtag: (val: string) => Promise<string | undefined>,
56+
) {
5257
const connection = shallowRef<Connection | undefined>(undefined)
5358
const openData = shallowRef<OpenData | undefined>(undefined)
5459
const openConnection = async () => {
60+
const baseVersionEtag = await getBaseVersionEtag()
5561
const guestName = localStorage.getItem('nick') ?? ''
5662
const { connection: opened, data } =
57-
openInitialSession(props)
63+
openInitialSession(props, baseVersionEtag)
5864
|| (await open({
5965
fileId: props.fileId,
6066
guestName,
6167
token: props.shareToken,
6268
filePath: props.relativePath,
6369
baseVersionEtag,
6470
}))
65-
baseVersionEtag = data.document.baseVersionEtag
71+
await setBaseVersionEtag(data.document.baseVersionEtag)
6672
connection.value = opened
6773
openData.value = data
6874
return data
@@ -84,14 +90,27 @@ export const useConnection = () => {
8490
* @param props.relativePath Relative path to the file.
8591
* @param props.initialSession Initial session handed to the editor in direct editing
8692
* @param props.shareToken Share token of the file.
93+
* @param baseVersionEtag Etag from the last editing session.
8794
*/
88-
function openInitialSession(props: {
89-
relativePath: string
90-
initialSession?: InitialData
91-
shareToken?: string
92-
}) {
95+
function openInitialSession(
96+
props: {
97+
relativePath: string
98+
initialSession?: InitialData
99+
shareToken?: string
100+
},
101+
baseVersionEtag: string | undefined,
102+
) {
93103
if (props.initialSession) {
94104
const { document, session } = props.initialSession
105+
if (baseVersionEtag !== document.baseVersionEtag) {
106+
throw new Error(
107+
'Base version etag did not match when opening initial session.',
108+
)
109+
// In order to handle this properly we'd need to:
110+
// * fetch the file content.
111+
// * throw the same exception as a 409 response.
112+
// * include the file content as `outsideChange` in the error.
113+
}
95114
const connection = {
96115
documentId: document.id,
97116
sessionId: session.id,

src/composables/useIndexedDbProvider.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,24 @@ export function useIndexedDbProvider(
2323
indexedDbProvider.on('synced', (provider: IndexeddbPersistence) => {
2424
console.info('synced from indexeddb', provider)
2525
})
26+
27+
/**
28+
* Get the base version etag the document had when it was edited last.
29+
*/
30+
function getBaseVersionEtag(): Promise<string | undefined> {
31+
return indexedDbProvider.get('baseVersionEtag')
32+
}
33+
34+
/**
35+
* Set the base version etag for the current connection.
36+
* @param val the base version etag as returned by open.
37+
*/
38+
function setBaseVersionEtag(val: string) {
39+
return indexedDbProvider.set('baseVersionEtag', val)
40+
}
41+
42+
return {
43+
getBaseVersionEtag,
44+
setBaseVersionEtag,
45+
}
2646
}

src/tests/services/SyncService.spec.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,23 @@ const openResult = { connection, data: initialData }
4343

4444
describe('Sync service', () => {
4545
it('opens a connection', async () => {
46-
const { connection, openConnection, openData } = provideConnection({
47-
fileId: 123,
48-
relativePath: './',
49-
})
46+
const getBaseVersionEtag = vi.fn()
47+
const setBaseVersionEtag = vi.fn()
48+
const { connection, openConnection, openData } = provideConnection(
49+
{
50+
fileId: 123,
51+
relativePath: './',
52+
},
53+
getBaseVersionEtag,
54+
setBaseVersionEtag,
55+
)
5056
vi.mock('../../apis/connect')
5157
vi.mocked(connect.open).mockResolvedValue(openResult)
5258
const openHandler = vi.fn()
5359
const service = new SyncService({ connection, openConnection })
5460
service.on('opened', openHandler)
5561
await service.open()
62+
expect(setBaseVersionEtag).toHaveBeenCalledWith('etag')
5663
expect(openHandler).toHaveBeenCalledWith(
5764
expect.objectContaining({ session: initialData.session }),
5865
)

0 commit comments

Comments
 (0)