diff --git a/src/sync/syncObservable.ts b/src/sync/syncObservable.ts index 88bf3efd..654d5bd8 100644 --- a/src/sync/syncObservable.ts +++ b/src/sync/syncObservable.ts @@ -969,6 +969,10 @@ export function syncObservable( observableSyncConfiguration, removeNullUndefined(syncOptions || {}), ); + + // Store the original initial value to preserve it for reset functionality + const originalInitial = clone(syncOptions.initial); + const localState: LocalState = {}; let sync: () => Promise; @@ -1366,13 +1370,20 @@ export function syncObservable( unsubscribe = undefined; const promise = syncStateValue.resetPersistence(); onChangeRemote(() => { - obs$.set(syncOptions.initial ?? undefined); + obs$.set(clone(originalInitial) ?? undefined); }); syncState$.isLoaded.set(false); syncStateValue.isPersistEnabled = wasPersistEnabled; syncStateValue.isSyncEnabled = wasSyncEnabled; node.dirtyFn = sync; await promise; + + // For observables without remote data loading (no get function), set isLoaded back to true + // since there's no remote data to load. This matches the initial loading logic. + const hasRemoteLoad = !!syncOptions.get; + if (!hasRemoteLoad) { + syncState$.isLoaded.set(true); + } }; // Wait for this node and all parent nodes up the hierarchy to be loaded diff --git a/tests/persist.test.ts b/tests/persist.test.ts index 03cf541a..aed830ba 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -945,6 +945,302 @@ describe('reset sync state', () => { test('reset individual sync state with initial', async () => { return testReset({ test: 0 }); }); + test('reset individual sync state without get function', async () => { + const persistName = getPersistName(); + + const obs$ = observable>( + synced({ + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: {}, + }), + ); + + obs$.get(); + + const state$ = syncState(obs$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(obs$.get()).toEqual({}); + + obs$['id1'].set({ test: 1 }); + + expect(obs$.get()).toEqual({ id1: { test: 1 } }); + + await state$.reset(); + + expect(localStorage.getItem(persistName)).toEqual(null); + expect(obs$.get()).toEqual({}); + + obs$.get(); + + await when(state$.isLoaded); + expect(obs$.get()).toEqual({}); + }); + + test('reset syncedCrud state without list function', async () => { + const persistName = getPersistName(); + + const obs$ = observable>( + syncedCrud({ + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: {}, + as: 'object', + }), + ); + + obs$.get(); + + const state$ = syncState(obs$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(obs$.get()).toEqual({}); + + obs$['id1'].set({ id: 'id1', test: 'hi' }); + + expect(obs$.get()).toEqual({ id1: { id: 'id1', test: 'hi' } }); + + await state$.reset(); + + expect(localStorage.getItem(persistName)).toEqual(null); + expect(obs$.get()).toEqual({}); + }); + + test('reset syncedCrud state with list function', async () => { + const persistName = getPersistName(); + let numLists = 0; + + const todos$ = observable>( + syncedCrud({ + list: async () => { + numLists++; + return [{ id: `item${numLists}`, test: `value${numLists}` }]; + }, + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: {}, + as: 'object', + }), + ); + + todos$.get(); + + const state$ = syncState(todos$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numLists).toEqual(1); + expect(todos$.get()).toEqual({ item1: { id: 'item1', test: 'value1' } }); + + todos$['id1'].set({ id: 'id1', test: 'local' }); + expect(todos$.get()).toEqual({ + item1: { id: 'item1', test: 'value1' }, + id1: { id: 'id1', test: 'local' }, + }); + + await state$.reset(); + + expect(state$.isLoaded.get()).toBe(false); + expect(todos$.get()).toEqual({}); + + todos$.get(); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numLists).toBeGreaterThanOrEqual(2); + + const result = todos$.get(); + const keys = Object.keys(result); + expect(keys.length).toBe(1); + expect(keys[0]).toMatch(/^item\d+$/); + expect(result[keys[0]]).toMatchObject({ + id: keys[0], + test: expect.stringMatching(/^value\d+$/), + }); + }); + + test('reset same observable twice should work', async () => { + const persistName = getPersistName(); + + const obs$ = observable>( + synced({ + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: {}, + }), + ); + + obs$.get(); + + const state$ = syncState(obs$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(obs$.get()).toEqual({}); + + // First modification + obs$['id1'].set({ test: 1 }); + expect(obs$.get()).toEqual({ id1: { test: 1 } }); + await promiseTimeout(1); + + // First reset + await state$.reset(); + expect(localStorage.getItem(persistName)).toEqual(null); + expect(obs$.get()).toEqual({}); + expect(state$.isLoaded.get()).toEqual(true); + + // Second modification after first reset + obs$['id2'].set({ test: 2 }); + expect(obs$.get()).toEqual({ id2: { test: 2 } }); + await promiseTimeout(1); + + // Second reset - this should also work + await state$.reset(); + expect(localStorage.getItem(persistName)).toEqual(null); + expect(obs$.get()).toEqual({}); + expect(state$.isLoaded.get()).toEqual(true); + + // Third modification after second reset + obs$['id3'].set({ test: 3 }); + expect(obs$.get()).toEqual({ id3: { test: 3 } }); + }); + + test('reset same observable with get function twice should work', async () => { + const persistName = getPersistName(); + let numGets = 0; + + const obs$ = observable( + synced({ + get: async () => { + numGets++; + await promiseTimeout(0); + return { test: numGets }; + }, + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: { test: 0 }, + }), + ); + + obs$.get(); + + const state$ = syncState(obs$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numGets).toEqual(1); + expect(obs$.get()).toEqual({ test: 1 }); + + await state$.reset(); + expect(localStorage.getItem(persistName)).toEqual(null); + expect(obs$.get()).toEqual({ test: 0 }); + expect(state$.isLoaded.get()).toEqual(false); + + obs$.get(); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numGets).toEqual(2); + expect(obs$.get()).toEqual({ test: 2 }); + + await state$.reset(); + expect(localStorage.getItem(persistName)).toEqual(null); + expect(obs$.get()).toEqual({ test: 0 }); + expect(state$.isLoaded.get()).toEqual(false); + + obs$.get(); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numGets).toEqual(3); + expect(obs$.get()).toEqual({ test: 3 }); + }); + + test('reset syncedCrud with list function should handle isLoaded correctly', async () => { + const persistName = getPersistName(); + let numLists = 0; + + const todos$ = observable>( + syncedCrud({ + list: async () => { + numLists++; + await promiseTimeout(0); + return [{ id: `item${numLists}`, test: `value${numLists}` }]; + }, + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: {}, + as: 'object' as const, + }), + ); + + todos$.get(); + + const state$ = syncState(todos$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numLists).toEqual(1); + expect(state$.isLoaded.get()).toEqual(true); + + // Reset should set isLoaded to false because there's a list function (remote data to load) + await state$.reset(); + expect(state$.isLoaded.get()).toEqual(false); + + // Accessing should trigger reload and eventually set isLoaded to true + todos$.get(); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(numLists).toEqual(2); + expect(state$.isLoaded.get()).toEqual(true); + }); + + test('reset syncedCrud without list function should set isLoaded to true immediately', async () => { + const persistName = getPersistName(); + + const todos$ = observable>( + syncedCrud({ + persist: { + name: persistName, + plugin: ObservablePersistLocalStorage, + }, + initial: {}, + as: 'object' as const, + }), + ); + + todos$.get(); + + const state$ = syncState(todos$); + await when(state$.isLoaded); + await promiseTimeout(1); + + expect(state$.isLoaded.get()).toEqual(true); + + todos$['id1'].set({ id: 'id1', test: 'test' }); + expect(todos$.get()).toEqual({ id1: { id: 'id1', test: 'test' } }); + + // Reset should immediately set isLoaded to true because there's no remote data to load + await state$.reset(); + expect(state$.isLoaded.get()).toEqual(true); + expect(todos$.get()).toEqual({}); + }); }); describe('multiple persists', () => {