From 2ad1b770d06707b7693ef673d11b0278ab9c2cf7 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Sat, 22 Apr 2023 17:32:57 -0500 Subject: [PATCH] fix(y-indexeddb): alert user when write operation unfinished (#2085) --- .../y-indexeddb/src/__tests__/index.spec.ts | 25 +++++++ packages/y-indexeddb/src/index.ts | 65 +++++++++++++------ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/packages/y-indexeddb/src/__tests__/index.spec.ts b/packages/y-indexeddb/src/__tests__/index.spec.ts index 641203427..50bccf103 100644 --- a/packages/y-indexeddb/src/__tests__/index.spec.ts +++ b/packages/y-indexeddb/src/__tests__/index.spec.ts @@ -240,6 +240,31 @@ describe('indexeddb provider', () => { expect(yDoc.getMap().get('foo')).toBe('bar'); } }); + + test('beforeunload', async () => { + const oldAddEventListener = window.addEventListener; + window.addEventListener = vi.fn((event: string, fn, options) => { + expect(event).toBe('beforeunload'); + return oldAddEventListener(event, fn, options); + }); + const oldRemoveEventListener = window.removeEventListener; + window.removeEventListener = vi.fn((event: string, fn, options) => { + expect(event).toBe('beforeunload'); + return oldRemoveEventListener(event, fn, options); + }); + const doc = new Doc(); + const provider = createIndexedDBProvider('1', doc); + const map = doc.getMap('map'); + map.set('1', 1); + provider.connect(); + await provider.whenSynced; + + expect(window.addEventListener).toBeCalledTimes(1); + expect(window.removeEventListener).toBeCalledTimes(1); + + window.addEventListener = oldAddEventListener; + window.removeEventListener = oldRemoveEventListener; + }); }); describe('milestone', () => { diff --git a/packages/y-indexeddb/src/index.ts b/packages/y-indexeddb/src/index.ts index 743bc8ed2..ecacb77af 100644 --- a/packages/y-indexeddb/src/index.ts +++ b/packages/y-indexeddb/src/index.ts @@ -21,6 +21,25 @@ const snapshotOrigin = Symbol('snapshot-origin'); let mergeCount = 500; +/** + * @internal + */ +const saveAlert = (event: BeforeUnloadEvent) => { + event.preventDefault(); + return (event.returnValue = + 'Data is not saved. Are you sure you want to leave?'); +}; + +export const writeOperation = async (op: Promise) => { + window.addEventListener('beforeunload', saveAlert, { + capture: true, + }); + await op; + window.removeEventListener('beforeunload', saveAlert, { + capture: true, + }); +}; + export function setMergeCount(count: number) { mergeCount = count; } @@ -169,9 +188,9 @@ export const createIndexedDBProvider = ( }, ], }; - await store.put(data); + await writeOperation(store.put(data)); } else { - await store.put(data); + await writeOperation(store.put(data)); } } @@ -205,15 +224,17 @@ export const createIndexedDBProvider = ( return; } if (!data) { - await db.put('workspace', { - id, - updates: [ - { - timestamp: Date.now(), - update: encodeStateAsUpdate(doc), - }, - ], - }); + await writeOperation( + db.put('workspace', { + id, + updates: [ + { + timestamp: Date.now(), + update: encodeStateAsUpdate(doc), + }, + ], + }) + ); } else { const updates = data.updates.map(({ update }) => update); const fakeDoc = new Doc(); @@ -226,16 +247,18 @@ export const createIndexedDBProvider = ( encodeStateAsUpdate(doc), encodeStateAsUpdate(fakeDoc) ); - await store.put({ - ...data, - updates: [ - ...data.updates, - { - timestamp: Date.now(), - update: newUpdate, - }, - ], - }); + await writeOperation( + store.put({ + ...data, + updates: [ + ...data.updates, + { + timestamp: Date.now(), + update: newUpdate, + }, + ], + }) + ); doc.transact(() => { updates.forEach(update => { applyUpdate(doc, update);