From 8fd3fadd67a3bad1dfb32428b135bf5ad67c6229 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Tue, 16 Jul 2024 15:52:42 -0700 Subject: [PATCH 01/17] a new synced configuring strategy for creating configured syncObservable and synced functions, which is more flexible than creating a global configuration --- src/sync/createConfigured.ts | 18 ++++++++++++ sync.ts | 1 + tests/computed-persist.test.ts | 16 ++++------ tests/crud.test.ts | 8 ----- tests/keel.test.ts | 7 ----- tests/persist-localstorage.test.ts | 47 +++++++++++++++--------------- tests/persist.test.ts | 17 ++++++----- 7 files changed, 57 insertions(+), 57 deletions(-) create mode 100644 src/sync/createConfigured.ts diff --git a/src/sync/createConfigured.ts b/src/sync/createConfigured.ts new file mode 100644 index 00000000..7a9ecba1 --- /dev/null +++ b/src/sync/createConfigured.ts @@ -0,0 +1,18 @@ +import { ObservableParam, internal } from '@legendapp/state'; +import type { PersistOptions, SyncedOptions } from './syncTypes'; +import { syncObservable } from './syncObservable'; +import { synced } from './synced'; + +const { deepMerge } = internal; + +interface SyncedOptionsConfigure extends Omit { + persist?: Partial>; +} + +export function configuredSyncObservable(origOptions: SyncedOptionsConfigure): typeof syncObservable { + return (obs$: ObservableParam, options: SyncedOptionsConfigure) => + syncObservable(obs$, deepMerge(origOptions, options)); +} +export function configuredSynced(fn: T, origOptions: SyncedOptionsConfigure): T { + return ((options) => fn(deepMerge(origOptions as any, options))) as T; +} diff --git a/sync.ts b/sync.ts index 0f691bcd..0ab9032d 100644 --- a/sync.ts +++ b/sync.ts @@ -5,6 +5,7 @@ export * from './src/sync/syncHelpers'; export { mapSyncPlugins, onChangeRemote, syncObservable } from './src/sync/syncObservable'; export * from './src/sync/syncTypes'; export { synced } from './src/sync/synced'; +export * from './src/sync/createConfigured'; import { observableSyncConfiguration } from './src/sync/configureObservableSync'; export const internal: { diff --git a/tests/computed-persist.test.ts b/tests/computed-persist.test.ts index 57baf695..72c6ac02 100644 --- a/tests/computed-persist.test.ts +++ b/tests/computed-persist.test.ts @@ -1,5 +1,5 @@ import { event, observable, observe, syncState, when, whenReady } from '@legendapp/state'; -import { configureObservableSync, syncObservable, synced } from '@legendapp/state/sync'; +import { configuredSynced, syncObservable, synced } from '@legendapp/state/sync'; import { onChangeRemote } from '../src/sync/syncObservable'; import { ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; @@ -763,11 +763,9 @@ describe('Debouncing', () => { test('Remote changes debounce with global config', async () => { let startTime = 0; const didSet$ = observable(); - configureObservableSync({ - debounceSet: 20, - }); + const mySynced = configuredSynced(synced, { debounceSet: 20, persist: { plugin: undefined } }); const obs = observable( - synced({ + mySynced({ get: () => { return 'hi'; }, @@ -787,11 +785,9 @@ describe('Debouncing', () => { let startTime = 0; const didSet1$ = observable(); const didSet2$ = observable(); - configureObservableSync({ - debounceSet: 10, - }); + const mySynced = configuredSynced(synced, { debounceSet: 10 }); const obs = observable( - synced({ + mySynced({ get: () => { return 'hi'; }, @@ -801,7 +797,7 @@ describe('Debouncing', () => { }), ); const obs2 = observable( - synced({ + mySynced({ get: () => { return 'hi'; }, diff --git a/tests/crud.test.ts b/tests/crud.test.ts index de709fa9..09d1f4d5 100644 --- a/tests/crud.test.ts +++ b/tests/crud.test.ts @@ -1,5 +1,4 @@ import { observable, observe, syncState, when } from '@legendapp/state'; -import { configureObservableSync } from '@legendapp/state/sync'; import { syncedCrud } from '@legendapp/state/sync-plugins/crud'; import { clone, symbolDelete } from '../src/globals'; import { @@ -20,13 +19,6 @@ type GetOrListTestParams = | { get: () => Promise; list?: never; as?: never } | { list: () => Promise; as: 'value'; get?: never }; -beforeAll(() => { - configureObservableSync({ - debounceSet: null, - persist: null, - } as any); -}); - describe('Crud object get', () => { const getTests = { get: async (params: GetOrListTestParams) => { diff --git a/tests/keel.test.ts b/tests/keel.test.ts index 54f6557c..8980244f 100644 --- a/tests/keel.test.ts +++ b/tests/keel.test.ts @@ -35,13 +35,6 @@ const ItemBasicValue: () => BasicValue = () => ({ test: 'hi', }); -beforeAll(() => { - configureObservableSync({ - debounceSet: null, - persist: null, - } as any); -}); - async function fakeKeelList(results: T[]): Promise> { await promiseTimeout(0); return { diff --git a/tests/persist-localstorage.test.ts b/tests/persist-localstorage.test.ts index b3698f9b..5c3e8a31 100644 --- a/tests/persist-localstorage.test.ts +++ b/tests/persist-localstorage.test.ts @@ -1,7 +1,6 @@ import { isArray, isObject, isString } from '../src/is'; import { observable } from '../src/observable'; -import { configureObservableSync } from '../src/sync/configureObservableSync'; -import { syncObservable } from '../src/sync/syncObservable'; +import { configuredSyncObservable } from '../src/sync/createConfigured'; import { ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; export async function recursiveReplaceStrings( @@ -33,7 +32,7 @@ export async function recursiveReplaceStrings { const persistName = getPersistName(); const obs = observable({ test: '' }); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -59,7 +58,7 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable({}); - syncObservable(obs2, { + mySyncObservable(obs2, { persist: { name: persistName }, }); @@ -69,7 +68,7 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable(''); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -84,7 +83,7 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable(); - syncObservable(obs2, { + mySyncObservable(obs2, { persist: { name: persistName }, }); @@ -94,7 +93,7 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable({ test: { text: 'hi' } } as { test: Record }); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -109,7 +108,7 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable({}); - syncObservable(obs2, { + mySyncObservable(obs2, { persist: { name: persistName }, }); @@ -120,7 +119,7 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); localStorage.setItem(persistName, '{"test2":{"text":"hello"}}'); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -137,7 +136,7 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable({ test: 'hello' } as Record); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -152,7 +151,7 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable({}); - syncObservable(obs2, { + mySyncObservable(obs2, { persist: { name: persistName }, }); @@ -162,7 +161,7 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable(); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -177,7 +176,7 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable(); - syncObservable(obs2, { + mySyncObservable(obs2, { persist: { name: persistName }, }); @@ -190,7 +189,7 @@ describe('Persist primitives', () => { const persistName = getPersistName(); const obs = observable(''); - syncObservable(obs, { + mySyncObservable(obs, { persist: { name: persistName }, }); @@ -205,7 +204,7 @@ describe('Persist primitives', () => { // obs2 should load with the same value it was just saved as const obs2 = observable(''); - syncObservable(obs2, { + mySyncObservable(obs2, { persist: { name: persistName }, }); @@ -224,7 +223,7 @@ describe('Persist computed', () => { sub: () => sub$.num.get(), }); - syncObservable(obs$, { + mySyncObservable(obs$, { persist: { name: persistName }, }); @@ -248,7 +247,7 @@ describe('Persist computed', () => { }, }); - syncObservable(obs2$, { + mySyncObservable(obs2$, { persist: { name: persistName }, }); @@ -270,7 +269,7 @@ describe('Persist computed', () => { sub: () => sub$.num.get(), }); - syncObservable(obs$, { + mySyncObservable(obs$, { persist: { name: persistName }, }); @@ -294,7 +293,7 @@ describe('Persist computed', () => { }, }); - syncObservable(obs2$, { + mySyncObservable(obs2$, { persist: { name: persistName }, }); @@ -310,7 +309,7 @@ describe('Persist computed', () => { const persistName = getPersistName(); const obs$ = observable(new Map()); - syncObservable(obs$, { + mySyncObservable(obs$, { persist: { name: persistName }, }); obs$.set('key', 'val'); @@ -324,7 +323,7 @@ describe('Persist computed', () => { const obs2$ = observable(new Map()); - syncObservable(obs2$, { + mySyncObservable(obs2$, { persist: { name: persistName }, }); @@ -334,7 +333,7 @@ describe('Persist computed', () => { const persistName = getPersistName(); const obs$ = observable(new Set()); - syncObservable(obs$, { + mySyncObservable(obs$, { persist: { name: persistName }, }); obs$.add('key'); @@ -348,7 +347,7 @@ describe('Persist computed', () => { const obs2$ = observable(new Set()); - syncObservable(obs2$, { + mySyncObservable(obs2$, { persist: { name: persistName }, }); diff --git a/tests/persist.test.ts b/tests/persist.test.ts index 04f667a2..0764dc69 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -10,6 +10,7 @@ import { BasicValue, ObservablePersistLocalStorage, getPersistName, localStorage import { observe } from '../src/observe'; import { syncedCrud } from '@legendapp/state/sync-plugins/crud'; import { event } from '../src/event'; +import { configuredSyncObservable, configuredSynced } from '../src/sync/createConfigured'; describe('Creating', () => { test('Loading state works correctly', async () => { @@ -554,7 +555,7 @@ describe('persist objects', () => { ]), }); - configureObservableSync({ + const mySyncObservable = configuredSyncObservable({ persist: { plugin: ObservablePersistLocalStorage, }, @@ -562,7 +563,7 @@ describe('persist objects', () => { const persistName = getPersistName(); - syncObservable(tablesState$, { + mySyncObservable(tablesState$, { persist: { name: persistName, }, @@ -617,7 +618,7 @@ describe('persist objects', () => { ], ]), }); - syncObservable(tablesState2$, { + mySyncObservable(tablesState2$, { persist: { name: persistName, }, @@ -645,7 +646,7 @@ describe('persist objects', () => { ]), }); - configureObservableSync({ + const mySyncObservable = configuredSyncObservable({ persist: { plugin: ObservablePersistLocalStorage, }, @@ -653,7 +654,7 @@ describe('persist objects', () => { const persistName = getPersistName(); - syncObservable(obs$, { + mySyncObservable(obs$, { persist: { name: persistName, }, @@ -688,7 +689,7 @@ describe('persist objects', () => { ['v2', { h1: 'h3', h2: [1] }], ]), }); - syncObservable(obs2$, { + mySyncObservable(obs2$, { persist: { name: persistName, }, @@ -778,7 +779,7 @@ describe('global config', () => { test('takes global config persist changes', async () => { let setTo: any = undefined; const didSet$ = observable(false); - configureObservableSync({ + const mySynced = configuredSynced(synced, { persist: { retrySync: true, plugin: ObservablePersistLocalStorage, @@ -787,7 +788,7 @@ describe('global config', () => { const persistName = getPersistName(); const obs$ = observable( - synced({ + mySynced({ get: async () => { await promiseTimeout(0); return { test: false }; From 3bdb6b5945e270f009ae7e5d417003e51e7a7f25 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Tue, 16 Jul 2024 17:08:55 -0700 Subject: [PATCH 02/17] a comment --- src/observableInterfaces.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 9aadaeca..09696e5c 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -44,6 +44,7 @@ export type ClassConstructor = new (...args: Args export type ObservableListenerDispose = () => void; export interface ObservableRoot { + // Observable root value is set on a child of the object so the reference to the root never changes _: any; set?: (value: any) => void; } From 12b1b8593c417639f7e55c544a56a1af8cf74852 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Tue, 16 Jul 2024 17:09:27 -0700 Subject: [PATCH 03/17] a new persist plugin configuring strategy using functions for creating configured persist plugins, which is more flexible than global configuration --- src/persist-plugins/async-storage.ts | 27 +++++++++++--- src/persist-plugins/indexeddb.ts | 25 +++++++++---- src/persist-plugins/mmkv.ts | 23 +++++++++--- src/sync/syncTypes.ts | 21 ++++++----- tests/persist-indexeddb.test.ts | 54 ++++++++++++++-------------- tests/persist.test.ts | 10 +++--- 6 files changed, 104 insertions(+), 56 deletions(-) diff --git a/src/persist-plugins/async-storage.ts b/src/persist-plugins/async-storage.ts index 4269b179..2e41488f 100644 --- a/src/persist-plugins/async-storage.ts +++ b/src/persist-plugins/async-storage.ts @@ -1,6 +1,11 @@ import type { Change } from '@legendapp/state'; import { applyChanges, internal, isArray } from '@legendapp/state'; -import type { ObservablePersistPlugin, ObservablePersistPluginOptions, PersistMetadata } from '@legendapp/state/sync'; +import type { + ObservablePersistAsyncStoragePluginOptions, + ObservablePersistPlugin, + ObservablePersistPluginOptions, + PersistMetadata, +} from '@legendapp/state/sync'; import type { AsyncStorageStatic } from '@react-native-async-storage/async-storage'; const MetadataSuffix = '__m'; @@ -11,11 +16,15 @@ const { safeParse, safeStringify } = internal; export class ObservablePersistAsyncStorage implements ObservablePersistPlugin { private data: Record = {}; + private configuration: ObservablePersistAsyncStoragePluginOptions; + + constructor(configuration: ObservablePersistAsyncStoragePluginOptions) { + this.configuration = configuration; + } + public async initialize(configOptions: ObservablePersistPluginOptions) { + const storageConfig = this.configuration || configOptions.asyncStorage; - // Init - public async initialize(config: ObservablePersistPluginOptions) { let tables: readonly string[] = []; - const storageConfig = config.asyncStorage; if (storageConfig) { AsyncStorage = storageConfig.AsyncStorage; const { preload } = storageConfig; @@ -93,3 +102,13 @@ export class ObservablePersistAsyncStorage implements ObservablePersistPlugin { } } } + +export function configuredObservablePersistAsyncStorage( + configuration: ObservablePersistAsyncStoragePluginOptions, +): typeof ObservablePersistAsyncStorage { + return class ObservablePersistAsyncStorageConfigured extends ObservablePersistAsyncStorage { + constructor() { + super(configuration); + } + }; +} diff --git a/src/persist-plugins/indexeddb.ts b/src/persist-plugins/indexeddb.ts index c11ea83d..504a4e63 100644 --- a/src/persist-plugins/indexeddb.ts +++ b/src/persist-plugins/indexeddb.ts @@ -4,6 +4,7 @@ import type { ObservablePersistPlugin, PersistMetadata, PersistOptions, + ObservablePersistIndexedDBPluginOptions, } from '@legendapp/state/sync'; import { isPrimitive, isPromise, observable, setAtPath, when } from '@legendapp/state'; @@ -25,18 +26,20 @@ export class ObservablePersistIndexedDB implements ObservablePersistPlugin { Record }> >(); private promisesQueued: (() => void)[] = []; + private configuration: ObservablePersistIndexedDBPluginOptions; - constructor() { + constructor(configuration: ObservablePersistIndexedDBPluginOptions) { + this.configuration = configuration; this.doSave = this.doSave.bind(this); } - - public async initialize(config: ObservablePersistPluginOptions) { + public async initialize(configOptions: ObservablePersistPluginOptions) { + const config = this.configuration || configOptions.indexedDB; if (typeof indexedDB === 'undefined') return; - if (process.env.NODE_ENV === 'development' && !config?.indexedDB) { + if (process.env.NODE_ENV === 'development' && !config) { console.error('[legend-state] Must configure ObservablePersistIndexedDB'); } - const { databaseName, version, tableNames } = config!.indexedDB!; + const { databaseName, version, tableNames } = config; const openRequest = indexedDB.open(databaseName, version); openRequest.onerror = () => { @@ -45,7 +48,7 @@ export class ObservablePersistIndexedDB implements ObservablePersistPlugin { openRequest.onupgradeneeded = () => { const db = openRequest.result; - const { tableNames } = config!.indexedDB!; + const { tableNames } = config!; // Create a table for each name with "id" as the key tableNames.forEach((table) => { if (!db.objectStoreNames.contains(table)) { @@ -437,3 +440,13 @@ export class ObservablePersistIndexedDB implements ObservablePersistPlugin { return lastSet!; } } + +export function configuredObservablePersistIndexedDB( + configuration: ObservablePersistIndexedDBPluginOptions, +): typeof ObservablePersistIndexedDB { + return class ObservablePersistIndexedDBConfigured extends ObservablePersistIndexedDB { + constructor() { + super(configuration); + } + }; +} diff --git a/src/persist-plugins/mmkv.ts b/src/persist-plugins/mmkv.ts index ac59964d..de28717e 100644 --- a/src/persist-plugins/mmkv.ts +++ b/src/persist-plugins/mmkv.ts @@ -1,7 +1,7 @@ import type { Change } from '@legendapp/state'; import { internal, setAtPath } from '@legendapp/state'; import type { ObservablePersistPlugin, PersistMetadata, PersistOptions } from '@legendapp/state/sync'; -import { MMKV } from 'react-native-mmkv'; +import { MMKV, MMKVConfiguration } from 'react-native-mmkv'; const symbolDefault = Symbol(); const MetadataSuffix = '__m'; @@ -18,6 +18,11 @@ export class ObservablePersistMMKV implements ObservablePersistPlugin { }), ], ]); + private configuration: MMKVConfiguration; + + constructor(configuration: MMKVConfiguration) { + this.configuration = configuration; + } // Gets public getTable(table: string, init: object, config: PersistOptions): T { const storage = this.getStorage(config); @@ -58,12 +63,12 @@ export class ObservablePersistMMKV implements ObservablePersistPlugin { } // Private private getStorage(config: PersistOptions): MMKV { - const { mmkv } = config; - if (mmkv) { - const key = JSON.stringify(mmkv); + const configuration = config.mmkv || this.configuration; + if (configuration) { + const key = JSON.stringify(configuration); let storage = this.storages.get(key); if (!storage) { - storage = new MMKV(mmkv); + storage = new MMKV(configuration); this.storages.set(key, storage); } return storage; @@ -89,3 +94,11 @@ export class ObservablePersistMMKV implements ObservablePersistPlugin { } } } + +export function configuredObservablePersistMMKV(configuration: MMKVConfiguration): typeof ObservablePersistMMKV { + return class ObservablePersistMMKVConfigured extends ObservablePersistMMKV { + constructor() { + super(configuration); + } + }; +} diff --git a/src/sync/syncTypes.ts b/src/sync/syncTypes.ts index 071ecf68..42efb0cb 100644 --- a/src/sync/syncTypes.ts +++ b/src/sync/syncTypes.ts @@ -98,18 +98,21 @@ export interface SyncedOptionsGlobal persist?: ObservablePersistPluginOptions & Omit; } +export interface ObservablePersistIndexedDBPluginOptions { + databaseName: string; + version: number; + tableNames: string[]; +} +export interface ObservablePersistAsyncStoragePluginOptions { + AsyncStorage: AsyncStorageStatic; + preload?: boolean | string[]; +} + export interface ObservablePersistPluginOptions { onGetError?: (error: Error) => void; onSetError?: (error: Error) => void; - indexedDB?: { - databaseName: string; - version: number; - tableNames: string[]; - }; - asyncStorage?: { - AsyncStorage: AsyncStorageStatic; - preload?: boolean | string[]; - }; + indexedDB?: ObservablePersistIndexedDBPluginOptions; + asyncStorage?: ObservablePersistAsyncStoragePluginOptions; } export interface ObservablePersistPlugin { initialize?(config: ObservablePersistPluginOptions): void | Promise; diff --git a/tests/persist-indexeddb.test.ts b/tests/persist-indexeddb.test.ts index f82e56cb..6316035b 100644 --- a/tests/persist-indexeddb.test.ts +++ b/tests/persist-indexeddb.test.ts @@ -1,10 +1,10 @@ import { IDBFactory } from 'fake-indexeddb'; import 'fake-indexeddb/auto'; import { observable } from '../src/observable'; +import { configuredObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; +import { configuredSyncObservable } from '../src/sync/createConfigured'; +import { mapSyncPlugins } from '../src/sync/syncObservable'; import type { ObservablePersistPlugin, ObservablePersistPluginOptions } from '../src/sync/syncTypes'; -import { ObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; -import { configureObservableSync } from '../src/sync/configureObservableSync'; -import { mapSyncPlugins, syncObservable } from '../src/sync/syncObservable'; import { when } from '../src/when'; import { promiseTimeout } from './testglobals'; @@ -17,10 +17,10 @@ const persistOptions: ObservablePersistPluginOptions = { tableNames, }, }; -configureObservableSync({ +const myIndexedDBPlugin = configuredObservablePersistIndexedDB(persistOptions.indexedDB!); +const mySyncObservable = configuredSyncObservable({ persist: { - plugin: ObservablePersistIndexedDB, - ...persistOptions, + plugin: myIndexedDBPlugin, }, }); jest.setTimeout?.(150); @@ -29,7 +29,7 @@ async function reset() { // eslint-disable-next-line no-global-assign indexedDB = new IDBFactory(); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; if (persist) { await persist.initialize!(persistOptions); @@ -60,7 +60,7 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, }, @@ -76,7 +76,7 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, }, @@ -92,7 +92,7 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, }, @@ -102,12 +102,12 @@ describe('Persist IDB', () => { obs['test'].set({ id: 'test', text: 'hi' }); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = syncObservable(obs2, { + const state2 = mySyncObservable(obs2, { persist: { name: persistName, }, @@ -121,7 +121,7 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, }, @@ -133,11 +133,11 @@ describe('Persist IDB', () => { expectIDB(persistName, [{ id: 'test', text: 'hi' }]); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = syncObservable(obs2, { + const state2 = mySyncObservable(obs2, { persist: { name: persistName, }, @@ -149,11 +149,11 @@ describe('Persist IDB', () => { }); test('Persist IDB with no id', async () => { const persistName = getLocalName(); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, }, @@ -168,7 +168,7 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = syncObservable(obs2, { + const state2 = mySyncObservable(obs2, { persist: { name: persistName, }, @@ -180,11 +180,11 @@ describe('Persist IDB', () => { }); test('Persist IDB with itemID and primitive', async () => { const persistName = getLocalName(); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; const obs = observable('text'); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, indexedDB: { @@ -204,7 +204,7 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = syncObservable(obs2, { + const state2 = mySyncObservable(obs2, { persist: { name: persistName, indexedDB: { @@ -219,11 +219,11 @@ describe('Persist IDB', () => { }); test('Persist IDB with prefixId setting individual', async () => { const persistName = getLocalName(); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, indexedDB: { @@ -247,7 +247,7 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = syncObservable(obs2, { + const state2 = mySyncObservable(obs2, { persist: { name: persistName, indexedDB: { @@ -262,11 +262,11 @@ describe('Persist IDB', () => { }); test('Persist IDB with prefixId', async () => { const persistName = getLocalName(); - const persist = mapSyncPlugins.get(ObservablePersistIndexedDB)?.plugin as ObservablePersistPlugin; + const persist = mapSyncPlugins.get(myIndexedDBPlugin)?.plugin as ObservablePersistPlugin; const obs = observable>({}); - const state = syncObservable(obs, { + const state = mySyncObservable(obs, { persist: { name: persistName, indexedDB: { @@ -289,7 +289,7 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = syncObservable(obs2, { + const state2 = mySyncObservable(obs2, { persist: { name: persistName, indexedDB: { diff --git a/tests/persist.test.ts b/tests/persist.test.ts index 0764dc69..64d3cb3f 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -1,16 +1,16 @@ +import { syncedCrud } from '@legendapp/state/sync-plugins/crud'; import 'fake-indexeddb/auto'; +import { event } from '../src/event'; import { observable } from '../src/observable'; import { Change } from '../src/observableInterfaces'; import type { Observable } from '../src/observableTypes'; +import { observe } from '../src/observe'; +import { configuredSyncObservable, configuredSynced } from '../src/sync/createConfigured'; import { getAllSyncStates, syncObservable, transformSaveData } from '../src/sync/syncObservable'; import { syncState } from '../src/syncState'; import { when } from '../src/when'; -import { ObservablePersistPlugin, SyncedOptions, configureObservableSync, synced } from '../sync'; +import { ObservablePersistPlugin, SyncedOptions, synced } from '../sync'; import { BasicValue, ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; -import { observe } from '../src/observe'; -import { syncedCrud } from '@legendapp/state/sync-plugins/crud'; -import { event } from '../src/event'; -import { configuredSyncObservable, configuredSynced } from '../src/sync/createConfigured'; describe('Creating', () => { test('Loading state works correctly', async () => { From aa8080f1e17d3f5565bf9e3aa253972ea99f4404 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 17 Jul 2024 20:03:16 -0700 Subject: [PATCH 04/17] rename configure functions to create --- src/persist-plugins/async-storage.ts | 2 +- src/persist-plugins/indexeddb.ts | 2 +- src/persist-plugins/mmkv.ts | 2 +- src/sync/createConfigured.ts | 4 ++-- tests/computed-persist.test.ts | 6 +++--- tests/keel.test.ts | 1 - tests/persist-indexeddb.test.ts | 8 ++++---- tests/persist-localstorage.test.ts | 4 ++-- tests/persist.test.ts | 8 ++++---- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/persist-plugins/async-storage.ts b/src/persist-plugins/async-storage.ts index 2e41488f..3d2c2602 100644 --- a/src/persist-plugins/async-storage.ts +++ b/src/persist-plugins/async-storage.ts @@ -103,7 +103,7 @@ export class ObservablePersistAsyncStorage implements ObservablePersistPlugin { } } -export function configuredObservablePersistAsyncStorage( +export function createObservablePersistAsyncStorage( configuration: ObservablePersistAsyncStoragePluginOptions, ): typeof ObservablePersistAsyncStorage { return class ObservablePersistAsyncStorageConfigured extends ObservablePersistAsyncStorage { diff --git a/src/persist-plugins/indexeddb.ts b/src/persist-plugins/indexeddb.ts index 504a4e63..20a65feb 100644 --- a/src/persist-plugins/indexeddb.ts +++ b/src/persist-plugins/indexeddb.ts @@ -441,7 +441,7 @@ export class ObservablePersistIndexedDB implements ObservablePersistPlugin { } } -export function configuredObservablePersistIndexedDB( +export function createObservablePersistIndexedDB( configuration: ObservablePersistIndexedDBPluginOptions, ): typeof ObservablePersistIndexedDB { return class ObservablePersistIndexedDBConfigured extends ObservablePersistIndexedDB { diff --git a/src/persist-plugins/mmkv.ts b/src/persist-plugins/mmkv.ts index de28717e..6db545aa 100644 --- a/src/persist-plugins/mmkv.ts +++ b/src/persist-plugins/mmkv.ts @@ -95,7 +95,7 @@ export class ObservablePersistMMKV implements ObservablePersistPlugin { } } -export function configuredObservablePersistMMKV(configuration: MMKVConfiguration): typeof ObservablePersistMMKV { +export function createObservablePersistMMKV(configuration: MMKVConfiguration): typeof ObservablePersistMMKV { return class ObservablePersistMMKVConfigured extends ObservablePersistMMKV { constructor() { super(configuration); diff --git a/src/sync/createConfigured.ts b/src/sync/createConfigured.ts index 7a9ecba1..c91d4eb8 100644 --- a/src/sync/createConfigured.ts +++ b/src/sync/createConfigured.ts @@ -9,10 +9,10 @@ interface SyncedOptionsConfigure extends Omit { persist?: Partial>; } -export function configuredSyncObservable(origOptions: SyncedOptionsConfigure): typeof syncObservable { +export function createSyncObservable(origOptions: SyncedOptionsConfigure): typeof syncObservable { return (obs$: ObservableParam, options: SyncedOptionsConfigure) => syncObservable(obs$, deepMerge(origOptions, options)); } -export function configuredSynced(fn: T, origOptions: SyncedOptionsConfigure): T { +export function createSynced(fn: T, origOptions: SyncedOptionsConfigure): T { return ((options) => fn(deepMerge(origOptions as any, options))) as T; } diff --git a/tests/computed-persist.test.ts b/tests/computed-persist.test.ts index 72c6ac02..cfd1f711 100644 --- a/tests/computed-persist.test.ts +++ b/tests/computed-persist.test.ts @@ -1,5 +1,5 @@ import { event, observable, observe, syncState, when, whenReady } from '@legendapp/state'; -import { configuredSynced, syncObservable, synced } from '@legendapp/state/sync'; +import { createSynced, syncObservable, synced } from '@legendapp/state/sync'; import { onChangeRemote } from '../src/sync/syncObservable'; import { ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; @@ -763,7 +763,7 @@ describe('Debouncing', () => { test('Remote changes debounce with global config', async () => { let startTime = 0; const didSet$ = observable(); - const mySynced = configuredSynced(synced, { debounceSet: 20, persist: { plugin: undefined } }); + const mySynced = createSynced(synced, { debounceSet: 20, persist: { plugin: undefined } }); const obs = observable( mySynced({ get: () => { @@ -785,7 +785,7 @@ describe('Debouncing', () => { let startTime = 0; const didSet1$ = observable(); const didSet2$ = observable(); - const mySynced = configuredSynced(synced, { debounceSet: 10 }); + const mySynced = createSynced(synced, { debounceSet: 10 }); const obs = observable( mySynced({ get: () => { diff --git a/tests/keel.test.ts b/tests/keel.test.ts index 8980244f..12a6d3c5 100644 --- a/tests/keel.test.ts +++ b/tests/keel.test.ts @@ -1,5 +1,4 @@ import { observable } from '@legendapp/state'; -import { configureObservableSync } from '@legendapp/state/sync'; import { syncedKeel } from '../src/sync-plugins/keel'; import { promiseTimeout } from './testglobals'; diff --git a/tests/persist-indexeddb.test.ts b/tests/persist-indexeddb.test.ts index 6316035b..f2322541 100644 --- a/tests/persist-indexeddb.test.ts +++ b/tests/persist-indexeddb.test.ts @@ -1,8 +1,8 @@ import { IDBFactory } from 'fake-indexeddb'; import 'fake-indexeddb/auto'; import { observable } from '../src/observable'; -import { configuredObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; -import { configuredSyncObservable } from '../src/sync/createConfigured'; +import { createObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; +import { createSyncObservable } from '../src/sync/createConfigured'; import { mapSyncPlugins } from '../src/sync/syncObservable'; import type { ObservablePersistPlugin, ObservablePersistPluginOptions } from '../src/sync/syncTypes'; import { when } from '../src/when'; @@ -17,8 +17,8 @@ const persistOptions: ObservablePersistPluginOptions = { tableNames, }, }; -const myIndexedDBPlugin = configuredObservablePersistIndexedDB(persistOptions.indexedDB!); -const mySyncObservable = configuredSyncObservable({ +const myIndexedDBPlugin = createObservablePersistIndexedDB(persistOptions.indexedDB!); +const mySyncObservable = createSyncObservable({ persist: { plugin: myIndexedDBPlugin, }, diff --git a/tests/persist-localstorage.test.ts b/tests/persist-localstorage.test.ts index 5c3e8a31..f9e765f2 100644 --- a/tests/persist-localstorage.test.ts +++ b/tests/persist-localstorage.test.ts @@ -1,6 +1,6 @@ import { isArray, isObject, isString } from '../src/is'; import { observable } from '../src/observable'; -import { configuredSyncObservable } from '../src/sync/createConfigured'; +import { createSyncObservable } from '../src/sync/createConfigured'; import { ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; export async function recursiveReplaceStrings( @@ -32,7 +32,7 @@ export async function recursiveReplaceStrings { ]), }); - const mySyncObservable = configuredSyncObservable({ + const mySyncObservable = createSyncObservable({ persist: { plugin: ObservablePersistLocalStorage, }, @@ -646,7 +646,7 @@ describe('persist objects', () => { ]), }); - const mySyncObservable = configuredSyncObservable({ + const mySyncObservable = createSyncObservable({ persist: { plugin: ObservablePersistLocalStorage, }, @@ -779,7 +779,7 @@ describe('global config', () => { test('takes global config persist changes', async () => { let setTo: any = undefined; const didSet$ = observable(false); - const mySynced = configuredSynced(synced, { + const mySynced = createSynced(synced, { persist: { retrySync: true, plugin: ObservablePersistLocalStorage, From 045d79b6757e828e65b4e79d12ab94e1a57b1ce3 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Thu, 15 Aug 2024 13:45:00 -0700 Subject: [PATCH 05/17] add a reset function to syncState --- src/observableInterfaces.ts | 1 + src/sync/syncObservable.ts | 27 ++++++++++++++++++----- src/syncState.ts | 1 + tests/persist.test.ts | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 09696e5c..5b996f70 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -181,6 +181,7 @@ export interface ObservableSyncStateBase { } > | undefined; + reset: () => Promise; /* @internal */ numPendingLocalLoads?: number; numPendingRemoteLoads?: number; diff --git a/src/sync/syncObservable.ts b/src/sync/syncObservable.ts index 4578637b..073d903b 100644 --- a/src/sync/syncObservable.ts +++ b/src/sync/syncObservable.ts @@ -923,6 +923,8 @@ export function syncObservable( syncState$.isLoaded.set(!syncState$.numPendingRemoteLoads.peek()); let isSynced = false; + let isSubscribed = false; + let unsubscribe: void | (() => void) = undefined; const applyPending = (pending: PendingChanges | undefined) => { if (pending && !isEmpty(pending)) { @@ -952,8 +954,6 @@ export function syncObservable( }; if (syncOptions.get) { - let isSubscribed = false; - let unsubscribe: void | (() => void) = undefined; sync = async () => { // If this node is not being observed or sync is not enabled then don't sync if (isSynced && (!getNodeValue(getNode(syncState$)).isSyncEnabled || shouldIgnoreUnobserved(node, sync))) { @@ -1181,9 +1181,6 @@ export function syncObservable( }; if (waitFor) { - if (node.activationState) { - node.activationState.waitFor = undefined; - } whenReady(waitFor, () => trackSelector(runGet, sync)); } else { trackSelector(runGet, sync); @@ -1210,6 +1207,26 @@ export function syncObservable( } } + syncStateValue.reset = async () => { + // Reset all the state back to initial and clear persistence + const wasPersistEnabled = syncStateValue.isPersistEnabled; + const wasSyncEnabled = syncStateValue.isSyncEnabled; + syncStateValue.isPersistEnabled = false; + syncStateValue.isSyncEnabled = false; + syncStateValue.syncCount = 0; + isSynced = false; + isSubscribed = false; + unsubscribe?.(); + unsubscribe = undefined; + const promise = syncStateValue.clearPersist(); + obs$.set(syncOptions.initial ?? undefined); + syncState$.isLoaded.set(false); + syncStateValue.isPersistEnabled = wasPersistEnabled; + syncStateValue.isSyncEnabled = wasSyncEnabled; + node.dirtyFn = sync; + await promise; + }; + // Wait for this node and all parent nodes up the hierarchy to be loaded const onAllPersistLoaded = () => { let parentNode: NodeInfo | undefined = node; diff --git a/src/syncState.ts b/src/syncState.ts index 2f4090e3..83e894f8 100644 --- a/src/syncState.ts +++ b/src/syncState.ts @@ -16,6 +16,7 @@ export function syncState(obs: ObservableParam) { numPendingSets: 0, syncCount: 0, clearPersist: undefined as unknown as () => Promise, + reset: () => Promise.resolve(), sync: () => Promise.resolve(), getPendingChanges: () => ({}), }); diff --git a/tests/persist.test.ts b/tests/persist.test.ts index ccd2dff4..9f151b9c 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -881,6 +881,50 @@ describe('clear persist', () => { }); }); +describe('reset sync state', () => { + test('reset individual sync state', 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(localStorage.getItem(persistName)).toEqual('{"test":1}'); + + 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(localStorage.getItem(persistName)).toEqual('{"test":2}'); + }); +}); + describe('multiple persists', () => { test('saves to multiple persists with two syncObservable', async () => { const persistName1 = getPersistName(); From 8d52807d25ca149769f20bfdcc7d6e6fb85958fa Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 21 Aug 2024 11:21:43 -0700 Subject: [PATCH 06/17] refactor configuring sync to have just a configureSynced function --- src/sync/configureSynced.ts | 26 ++++ src/sync/createConfigured.ts | 18 --- sync.ts | 2 +- tests/computed-persist.test.ts | 6 +- tests/persist-indexeddb.test.ts | 212 +++++++++++++++++------------ tests/persist-localstorage.test.ts | 194 +++++++++++++++++--------- tests/persist.test.ts | 60 ++++---- 7 files changed, 322 insertions(+), 196 deletions(-) create mode 100644 src/sync/configureSynced.ts delete mode 100644 src/sync/createConfigured.ts diff --git a/src/sync/configureSynced.ts b/src/sync/configureSynced.ts new file mode 100644 index 00000000..845cfad8 --- /dev/null +++ b/src/sync/configureSynced.ts @@ -0,0 +1,26 @@ +import { internal } from '@legendapp/state'; +import type { PersistOptions, SyncedOptions } from './syncTypes'; +import type { synced } from './synced'; + +const { deepMerge } = internal; + +interface SyncedOptionsConfigure extends Omit { + persist?: Partial>; +} + +type RetType = typeof synced; + +export function configureSynced(fn: T, origOptions: SyncedOptionsConfigure): T; +export function configureSynced(origOptions: SyncedOptionsConfigure): RetType; +export function configureSynced, TRemote>( + fnOrOrigOptions: SyncedOptionsConfigure | T, + origOptions?: SyncedOptionsConfigure, +): RetType { + const fn = origOptions ? (fnOrOrigOptions as T) : undefined; + origOptions = origOptions ?? (fnOrOrigOptions as SyncedOptionsConfigure); + + return ((options: SyncedOptions) => { + const merged = deepMerge(origOptions as any, options); + return fn ? fn(merged) : merged; + }) as any; +} diff --git a/src/sync/createConfigured.ts b/src/sync/createConfigured.ts deleted file mode 100644 index c91d4eb8..00000000 --- a/src/sync/createConfigured.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ObservableParam, internal } from '@legendapp/state'; -import type { PersistOptions, SyncedOptions } from './syncTypes'; -import { syncObservable } from './syncObservable'; -import { synced } from './synced'; - -const { deepMerge } = internal; - -interface SyncedOptionsConfigure extends Omit { - persist?: Partial>; -} - -export function createSyncObservable(origOptions: SyncedOptionsConfigure): typeof syncObservable { - return (obs$: ObservableParam, options: SyncedOptionsConfigure) => - syncObservable(obs$, deepMerge(origOptions, options)); -} -export function createSynced(fn: T, origOptions: SyncedOptionsConfigure): T { - return ((options) => fn(deepMerge(origOptions as any, options))) as T; -} diff --git a/sync.ts b/sync.ts index 0ab9032d..f8d71c97 100644 --- a/sync.ts +++ b/sync.ts @@ -5,7 +5,7 @@ export * from './src/sync/syncHelpers'; export { mapSyncPlugins, onChangeRemote, syncObservable } from './src/sync/syncObservable'; export * from './src/sync/syncTypes'; export { synced } from './src/sync/synced'; -export * from './src/sync/createConfigured'; +export * from './src/sync/configureSynced'; import { observableSyncConfiguration } from './src/sync/configureObservableSync'; export const internal: { diff --git a/tests/computed-persist.test.ts b/tests/computed-persist.test.ts index cfd1f711..5c89d111 100644 --- a/tests/computed-persist.test.ts +++ b/tests/computed-persist.test.ts @@ -1,5 +1,5 @@ import { event, observable, observe, syncState, when, whenReady } from '@legendapp/state'; -import { createSynced, syncObservable, synced } from '@legendapp/state/sync'; +import { configureSynced, syncObservable, synced } from '@legendapp/state/sync'; import { onChangeRemote } from '../src/sync/syncObservable'; import { ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; @@ -763,7 +763,7 @@ describe('Debouncing', () => { test('Remote changes debounce with global config', async () => { let startTime = 0; const didSet$ = observable(); - const mySynced = createSynced(synced, { debounceSet: 20, persist: { plugin: undefined } }); + const mySynced = configureSynced(synced, { debounceSet: 20, persist: { plugin: undefined } }); const obs = observable( mySynced({ get: () => { @@ -785,7 +785,7 @@ describe('Debouncing', () => { let startTime = 0; const didSet1$ = observable(); const didSet2$ = observable(); - const mySynced = createSynced(synced, { debounceSet: 10 }); + const mySynced = configureSynced(synced, { debounceSet: 10 }); const obs = observable( mySynced({ get: () => { diff --git a/tests/persist-indexeddb.test.ts b/tests/persist-indexeddb.test.ts index f2322541..e344c5bb 100644 --- a/tests/persist-indexeddb.test.ts +++ b/tests/persist-indexeddb.test.ts @@ -2,8 +2,8 @@ import { IDBFactory } from 'fake-indexeddb'; import 'fake-indexeddb/auto'; import { observable } from '../src/observable'; import { createObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; -import { createSyncObservable } from '../src/sync/createConfigured'; -import { mapSyncPlugins } from '../src/sync/syncObservable'; +import { configureSynced } from '../src/sync/configureSynced'; +import { mapSyncPlugins, syncObservable } from '../src/sync/syncObservable'; import type { ObservablePersistPlugin, ObservablePersistPluginOptions } from '../src/sync/syncTypes'; import { when } from '../src/when'; import { promiseTimeout } from './testglobals'; @@ -18,7 +18,7 @@ const persistOptions: ObservablePersistPluginOptions = { }, }; const myIndexedDBPlugin = createObservablePersistIndexedDB(persistOptions.indexedDB!); -const mySyncObservable = createSyncObservable({ +const mySyncOptions = configureSynced({ persist: { plugin: myIndexedDBPlugin, }, @@ -60,11 +60,14 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - }, - }); + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state.isPersistLoaded); @@ -76,11 +79,14 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - }, - }); + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state.isPersistLoaded); @@ -92,11 +98,14 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - }, - }); + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state.isPersistLoaded); @@ -107,11 +116,14 @@ describe('Persist IDB', () => { const obs2 = observable>({}); - const state2 = mySyncObservable(obs2, { - persist: { - name: persistName, - }, - }); + const state2 = syncObservable( + obs2, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state2.isPersistLoaded); @@ -121,11 +133,14 @@ describe('Persist IDB', () => { const persistName = getLocalName(); const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - }, - }); + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state.isPersistLoaded); @@ -137,11 +152,14 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = mySyncObservable(obs2, { - persist: { - name: persistName, - }, - }); + const state2 = syncObservable( + obs2, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state2.isPersistLoaded); @@ -153,11 +171,14 @@ describe('Persist IDB', () => { const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - }, - }); + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state.isPersistLoaded); @@ -168,11 +189,14 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = mySyncObservable(obs2, { - persist: { - name: persistName, - }, - }); + const state2 = syncObservable( + obs2, + mySyncOptions({ + persist: { + name: persistName, + }, + }), + ); await when(state2.isPersistLoaded); @@ -184,14 +208,17 @@ describe('Persist IDB', () => { const obs = observable('text'); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - indexedDB: { - itemID: 'testItemId', + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + indexedDB: { + itemID: 'testItemId', + }, }, - }, - }); + }), + ); await when(state.isPersistLoaded); @@ -204,14 +231,17 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = mySyncObservable(obs2, { - persist: { - name: persistName, - indexedDB: { - itemID: 'testItemId', + const state2 = syncObservable( + obs2, + mySyncOptions({ + persist: { + name: persistName, + indexedDB: { + itemID: 'testItemId', + }, }, - }, - }); + }), + ); await when(state2.isPersistLoaded); @@ -223,14 +253,17 @@ describe('Persist IDB', () => { const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - indexedDB: { - prefixID: 'u', + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + indexedDB: { + prefixID: 'u', + }, }, - }, - }); + }), + ); await when(state.isPersistLoaded); @@ -247,14 +280,17 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = mySyncObservable(obs2, { - persist: { - name: persistName, - indexedDB: { - prefixID: 'u', + const state2 = syncObservable( + obs2, + mySyncOptions({ + persist: { + name: persistName, + indexedDB: { + prefixID: 'u', + }, }, - }, - }); + }), + ); await when(state2.isPersistLoaded); @@ -266,14 +302,17 @@ describe('Persist IDB', () => { const obs = observable>({}); - const state = mySyncObservable(obs, { - persist: { - name: persistName, - indexedDB: { - prefixID: 'u', + const state = syncObservable( + obs, + mySyncOptions({ + persist: { + name: persistName, + indexedDB: { + prefixID: 'u', + }, }, - }, - }); + }), + ); await when(state.isPersistLoaded); @@ -289,14 +328,17 @@ describe('Persist IDB', () => { await persist.initialize!(persistOptions); const obs2 = observable>({}); - const state2 = mySyncObservable(obs2, { - persist: { - name: persistName, - indexedDB: { - prefixID: 'u', + const state2 = syncObservable( + obs2, + mySyncOptions({ + persist: { + name: persistName, + indexedDB: { + prefixID: 'u', + }, }, - }, - }); + }), + ); await when(state2.isPersistLoaded); diff --git a/tests/persist-localstorage.test.ts b/tests/persist-localstorage.test.ts index f9e765f2..4d4565d2 100644 --- a/tests/persist-localstorage.test.ts +++ b/tests/persist-localstorage.test.ts @@ -1,6 +1,7 @@ +import { syncObservable } from '../src/sync/syncObservable'; import { isArray, isObject, isString } from '../src/is'; import { observable } from '../src/observable'; -import { createSyncObservable } from '../src/sync/createConfigured'; +import { configureSynced } from '../src/sync/configureSynced'; import { ObservablePersistLocalStorage, getPersistName, localStorage, promiseTimeout } from './testglobals'; export async function recursiveReplaceStrings( @@ -32,7 +33,7 @@ export async function recursiveReplaceStrings { const persistName = getPersistName(); const obs = observable({ test: '' }); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); obs.set({ test: 'hello' }); @@ -58,9 +62,12 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable({}); - mySyncObservable(obs2, { - persist: { name: persistName }, - }); + syncObservable( + obs2, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2.get()).toEqual({ test: 'hello' }); }); @@ -68,9 +75,12 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable(''); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); obs.set('hello'); @@ -83,9 +93,12 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable(); - mySyncObservable(obs2, { - persist: { name: persistName }, - }); + syncObservable( + obs2, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2.get()).toEqual('hello'); }); @@ -93,9 +106,12 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable({ test: { text: 'hi' } } as { test: Record }); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); obs.test.set({}); @@ -108,9 +124,12 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable({}); - mySyncObservable(obs2, { - persist: { name: persistName }, - }); + syncObservable( + obs2, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2.get()).toEqual({ test: {} }); }); @@ -119,9 +138,12 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); localStorage.setItem(persistName, '{"test2":{"text":"hello"}}'); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs.get()).toEqual({ test: { @@ -136,9 +158,12 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable({ test: 'hello' } as Record); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); obs.set({}); @@ -151,9 +176,12 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable({}); - mySyncObservable(obs2, { - persist: { name: persistName }, - }); + syncObservable( + obs2, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2.get()).toEqual({}); }); @@ -161,9 +189,12 @@ describe('Persist local localStorage', () => { const persistName = getPersistName(); const obs = observable(); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); obs.set({ key: 'value' }); @@ -176,9 +207,12 @@ describe('Persist local localStorage', () => { // obs2 should load with the same value it was just saved as const obs2 = observable(); - mySyncObservable(obs2, { - persist: { name: persistName }, - }); + syncObservable( + obs2, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2.get()).toEqual({ key: 'value' }); }); @@ -189,9 +223,12 @@ describe('Persist primitives', () => { const persistName = getPersistName(); const obs = observable(''); - mySyncObservable(obs, { - persist: { name: persistName }, - }); + syncObservable( + obs, + mySynced({ + persist: { name: persistName }, + }), + ); obs.set('hello'); @@ -204,9 +241,12 @@ describe('Persist primitives', () => { // obs2 should load with the same value it was just saved as const obs2 = observable(''); - mySyncObservable(obs2, { - persist: { name: persistName }, - }); + syncObservable( + obs2, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2.get()).toEqual('hello'); }); @@ -223,9 +263,12 @@ describe('Persist computed', () => { sub: () => sub$.num.get(), }); - mySyncObservable(obs$, { - persist: { name: persistName }, - }); + syncObservable( + obs$, + mySynced({ + persist: { name: persistName }, + }), + ); obs$.sub.get(); sub$.num.set(1); @@ -247,9 +290,12 @@ describe('Persist computed', () => { }, }); - mySyncObservable(obs2$, { - persist: { name: persistName }, - }); + syncObservable( + obs2$, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2$.sub.get()).toEqual(2); @@ -269,9 +315,12 @@ describe('Persist computed', () => { sub: () => sub$.num.get(), }); - mySyncObservable(obs$, { - persist: { name: persistName }, - }); + syncObservable( + obs$, + mySynced({ + persist: { name: persistName }, + }), + ); obs$.sub.get(); sub$.num.set(1); @@ -293,9 +342,12 @@ describe('Persist computed', () => { }, }); - mySyncObservable(obs2$, { - persist: { name: persistName }, - }); + syncObservable( + obs2$, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2$.sub.get()).toEqual(2); @@ -309,9 +361,12 @@ describe('Persist computed', () => { const persistName = getPersistName(); const obs$ = observable(new Map()); - mySyncObservable(obs$, { - persist: { name: persistName }, - }); + syncObservable( + obs$, + mySynced({ + persist: { name: persistName }, + }), + ); obs$.set('key', 'val'); await promiseTimeout(0); @@ -323,9 +378,12 @@ describe('Persist computed', () => { const obs2$ = observable(new Map()); - mySyncObservable(obs2$, { - persist: { name: persistName }, - }); + syncObservable( + obs2$, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2$.get()).toEqual(new Map([['key', 'val']])); }); @@ -333,9 +391,12 @@ describe('Persist computed', () => { const persistName = getPersistName(); const obs$ = observable(new Set()); - mySyncObservable(obs$, { - persist: { name: persistName }, - }); + syncObservable( + obs$, + mySynced({ + persist: { name: persistName }, + }), + ); obs$.add('key'); await promiseTimeout(0); @@ -347,9 +408,12 @@ describe('Persist computed', () => { const obs2$ = observable(new Set()); - mySyncObservable(obs2$, { - persist: { name: persistName }, - }); + syncObservable( + obs2$, + mySynced({ + persist: { name: persistName }, + }), + ); expect(obs2$.get()).toEqual(new Set(['key'])); }); diff --git a/tests/persist.test.ts b/tests/persist.test.ts index 9f151b9c..de392767 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -5,7 +5,7 @@ import { observable } from '../src/observable'; import { Change } from '../src/observableInterfaces'; import type { Observable } from '../src/observableTypes'; import { observe } from '../src/observe'; -import { createSyncObservable, createSynced } from '../src/sync/createConfigured'; +import { configureSynced } from '../src/sync/configureSynced'; import { getAllSyncStates, syncObservable, transformSaveData } from '../src/sync/syncObservable'; import { syncState } from '../src/syncState'; import { when } from '../src/when'; @@ -555,7 +555,7 @@ describe('persist objects', () => { ]), }); - const mySyncObservable = createSyncObservable({ + const myPersist = configureSynced({ persist: { plugin: ObservablePersistLocalStorage, }, @@ -563,11 +563,14 @@ describe('persist objects', () => { const persistName = getPersistName(); - mySyncObservable(tablesState$, { - persist: { - name: persistName, - }, - }); + syncObservable( + tablesState$, + myPersist({ + persist: { + name: persistName, + }, + }), + ); expect(tablesState$.get()).toEqual({ tables: new Map([ @@ -618,11 +621,14 @@ describe('persist objects', () => { ], ]), }); - mySyncObservable(tablesState2$, { - persist: { - name: persistName, - }, - }); + syncObservable( + tablesState2$, + myPersist({ + persist: { + name: persistName, + }, + }), + ); expect(tablesState2$.get()).toEqual({ tables: new Map([ @@ -646,7 +652,7 @@ describe('persist objects', () => { ]), }); - const mySyncObservable = createSyncObservable({ + const mySynced = configureSynced(synced, { persist: { plugin: ObservablePersistLocalStorage, }, @@ -654,11 +660,14 @@ describe('persist objects', () => { const persistName = getPersistName(); - mySyncObservable(obs$, { - persist: { - name: persistName, - }, - }); + syncObservable( + obs$, + mySynced({ + persist: { + name: persistName, + }, + }), + ); expect(obs$.get()).toEqual({ m: new Map([ @@ -689,11 +698,14 @@ describe('persist objects', () => { ['v2', { h1: 'h3', h2: [1] }], ]), }); - mySyncObservable(obs2$, { - persist: { - name: persistName, - }, - }); + syncObservable( + obs2$, + mySynced({ + persist: { + name: persistName, + }, + }), + ); expect(obs2$.get()).toEqual({ m: new Map([ @@ -779,7 +791,7 @@ describe('global config', () => { test('takes global config persist changes', async () => { let setTo: any = undefined; const didSet$ = observable(false); - const mySynced = createSynced(synced, { + const mySynced = configureSynced(synced, { persist: { retrySync: true, plugin: ObservablePersistLocalStorage, From 4b478a006fe5deac55d2161b554900dcbe3f3358 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 21 Aug 2024 14:04:47 -0700 Subject: [PATCH 07/17] fetch plugin skips fetching if url is falsy or not a string --- src/sync-plugins/fetch.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/sync-plugins/fetch.ts b/src/sync-plugins/fetch.ts index 0dda332d..0205ae6d 100644 --- a/src/sync-plugins/fetch.ts +++ b/src/sync-plugins/fetch.ts @@ -1,5 +1,5 @@ -import { Selector, computeSelector, getNodeValue } from '@legendapp/state'; -import { Synced, SyncedOptions, SyncedSetParams, synced } from '@legendapp/state/sync'; +import { Selector, computeSelector, getNodeValue, isString } from '@legendapp/state'; +import { type Synced, type SyncedOptions, type SyncedSetParams, synced } from '@legendapp/state/sync'; export interface SyncedFetchOnSavedParams { saved: TLocal; @@ -33,19 +33,23 @@ export function syncedFetch(props: SyncedFetchProps { const url = computeSelector(getParam); - const response = await fetch(url, getInit); + if (url && isString(url)) { + const response = await fetch(url, getInit); - if (!response.ok) { - throw new Error(response.statusText); - } + if (!response.ok) { + throw new Error(response.statusText); + } - let value = await response[valueType || 'json'](); + let value = await response[valueType || 'json'](); - if (transform?.load) { - value = transform?.load(value, 'get'); - } + if (transform?.load) { + value = transform?.load(value, 'get'); + } - return value; + return value; + } else { + return null; + } }; let set: ((params: SyncedSetParams) => void | Promise) | undefined = undefined; From 4dc6e13deafa55477fb9fdd0360de879f9acd2ab Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 21 Aug 2024 14:41:19 -0700 Subject: [PATCH 08/17] move configureLegendState to /config to reduce size of core --- config.ts | 1 + index.ts | 1 - jest.config.json | 3 ++- package.json | 1 + src/config/enable$GetSet.ts | 3 ++- src/config/enableReactTracking.ts | 11 ++--------- src/config/enableReactUse.ts | 3 ++- src/config/enable_PeekAssign.ts | 3 ++- src/{config.ts => configureLegendState.ts} | 0 tests/tests.test.ts | 2 +- tsconfig.json | 3 +++ tsup.config.ts | 1 + 12 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 config.ts rename src/{config.ts => configureLegendState.ts} (100%) diff --git a/config.ts b/config.ts new file mode 100644 index 00000000..3c8f8123 --- /dev/null +++ b/config.ts @@ -0,0 +1 @@ +export { configureLegendState } from './src/configureLegendState'; diff --git a/index.ts b/index.ts index 0d8bfcc3..ff493362 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,6 @@ export { isObserved, shouldIgnoreUnobserved } from './src/ObservableObject'; export { batch, beginBatch, endBatch } from './src/batching'; export { computed } from './src/computed'; -export { configureLegendState } from './src/config'; export { event } from './src/event'; export { isObservable } from './src/globals'; export { diff --git a/jest.config.json b/jest.config.json index 2f141e7c..27d7c81a 100644 --- a/jest.config.json +++ b/jest.config.json @@ -5,9 +5,10 @@ "moduleNameMapper": { "@legendapp/state/sync-plugins/crud": "/src/sync-plugins/crud", "@legendapp/state/sync": "/sync", + "@legendapp/state/config": "/config", "@legendapp/state": "/index" }, "transform": { - "^.+\\.tsx?$": ["ts-jest", {"tsconfig": { "jsx": "react"}}] + "^.+\\.tsx?$": ["ts-jest", { "tsconfig": { "jsx": "react" } }] } } \ No newline at end of file diff --git a/package.json b/package.json index 296d0dcb..cd19f7c4 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ ".", "sync", "react", + "config", "trace", "babel", "as/*", diff --git a/src/config/enable$GetSet.ts b/src/config/enable$GetSet.ts index 4a991355..bdeb2446 100644 --- a/src/config/enable$GetSet.ts +++ b/src/config/enable$GetSet.ts @@ -1,4 +1,5 @@ -import { configureLegendState, internal } from '@legendapp/state'; +import { internal } from '@legendapp/state'; +import { configureLegendState } from '@legendapp/state/config'; export function enable$GetSet() { configureLegendState({ diff --git a/src/config/enableReactTracking.ts b/src/config/enableReactTracking.ts index 14924e99..fa969c4d 100644 --- a/src/config/enableReactTracking.ts +++ b/src/config/enableReactTracking.ts @@ -1,12 +1,5 @@ -import { - type GetOptions, - configureLegendState, - internal, - isObject, - tracking, - type NodeInfo, - type TrackingType, -} from '@legendapp/state'; +import { type GetOptions, internal, isObject, tracking, type NodeInfo, type TrackingType } from '@legendapp/state'; +import { configureLegendState } from '@legendapp/state/config'; import { UseSelectorOptions, useSelector } from '@legendapp/state/react'; import { createContext, useContext } from 'react'; // @ts-expect-error Internals diff --git a/src/config/enableReactUse.ts b/src/config/enableReactUse.ts index 48772eb6..d4205da9 100644 --- a/src/config/enableReactUse.ts +++ b/src/config/enableReactUse.ts @@ -1,4 +1,5 @@ -import { configureLegendState, internal, NodeInfo } from '@legendapp/state'; +import { internal, NodeInfo } from '@legendapp/state'; +import { configureLegendState } from '@legendapp/state/config'; import { useSelector, UseSelectorOptions } from '@legendapp/state/react'; // TODO: Deprecated, remove in v4 diff --git a/src/config/enable_PeekAssign.ts b/src/config/enable_PeekAssign.ts index 56e9eabb..773d61c6 100644 --- a/src/config/enable_PeekAssign.ts +++ b/src/config/enable_PeekAssign.ts @@ -1,4 +1,5 @@ -import { configureLegendState, internal } from '@legendapp/state'; +import { internal } from '@legendapp/state'; +import { configureLegendState } from '@legendapp/state/config'; export function enable_PeekAssign() { configureLegendState({ diff --git a/src/config.ts b/src/configureLegendState.ts similarity index 100% rename from src/config.ts rename to src/configureLegendState.ts diff --git a/tests/tests.test.ts b/tests/tests.test.ts index 73f1a714..9ae13b89 100644 --- a/tests/tests.test.ts +++ b/tests/tests.test.ts @@ -1,6 +1,6 @@ import type { Observable } from '../src/observableTypes'; import { batch, beginBatch, endBatch } from '../src/batching'; -import { configureLegendState } from '../src/config'; +import { configureLegendState } from '../src/configureLegendState'; import { enable$GetSet } from '../src/config/enable$GetSet'; import { enable_PeekAssign } from '../src/config/enable_PeekAssign'; import { event } from '../src/event'; diff --git a/tsconfig.json b/tsconfig.json index 2bc591ba..7c185805 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -57,6 +57,9 @@ "@legendapp/state": [ "index" ], + "@legendapp/state/config": [ + "config" + ], "@legendapp/state/persist": [ "persist" ], diff --git a/tsup.config.ts b/tsup.config.ts index 6fe5ac95..eeaa34ef 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -17,6 +17,7 @@ const external = [ '@tanstack/react-query', '@tanstack/query-core', '@legendapp/state', + '@legendapp/state/config', '@legendapp/state/persist', '@legendapp/state/sync', '@legendapp/state/sync-plugins/crud', From 0aaaed0a54145fb036ae3b1d9cd489ea3d0726c4 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 21 Aug 2024 15:56:07 -0700 Subject: [PATCH 09/17] rename persist plugin configuration to match --- src/persist-plugins/async-storage.ts | 2 +- src/persist-plugins/indexeddb.ts | 2 +- src/persist-plugins/mmkv.ts | 2 +- tests/persist-indexeddb.test.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/persist-plugins/async-storage.ts b/src/persist-plugins/async-storage.ts index 3d2c2602..c11367ea 100644 --- a/src/persist-plugins/async-storage.ts +++ b/src/persist-plugins/async-storage.ts @@ -103,7 +103,7 @@ export class ObservablePersistAsyncStorage implements ObservablePersistPlugin { } } -export function createObservablePersistAsyncStorage( +export function configureObservablePersistAsyncStorage( configuration: ObservablePersistAsyncStoragePluginOptions, ): typeof ObservablePersistAsyncStorage { return class ObservablePersistAsyncStorageConfigured extends ObservablePersistAsyncStorage { diff --git a/src/persist-plugins/indexeddb.ts b/src/persist-plugins/indexeddb.ts index 20a65feb..e2cb1a72 100644 --- a/src/persist-plugins/indexeddb.ts +++ b/src/persist-plugins/indexeddb.ts @@ -441,7 +441,7 @@ export class ObservablePersistIndexedDB implements ObservablePersistPlugin { } } -export function createObservablePersistIndexedDB( +export function configureObservablePersistIndexedDB( configuration: ObservablePersistIndexedDBPluginOptions, ): typeof ObservablePersistIndexedDB { return class ObservablePersistIndexedDBConfigured extends ObservablePersistIndexedDB { diff --git a/src/persist-plugins/mmkv.ts b/src/persist-plugins/mmkv.ts index 6db545aa..8c4fec5a 100644 --- a/src/persist-plugins/mmkv.ts +++ b/src/persist-plugins/mmkv.ts @@ -95,7 +95,7 @@ export class ObservablePersistMMKV implements ObservablePersistPlugin { } } -export function createObservablePersistMMKV(configuration: MMKVConfiguration): typeof ObservablePersistMMKV { +export function configureObservablePersistMMKV(configuration: MMKVConfiguration): typeof ObservablePersistMMKV { return class ObservablePersistMMKVConfigured extends ObservablePersistMMKV { constructor() { super(configuration); diff --git a/tests/persist-indexeddb.test.ts b/tests/persist-indexeddb.test.ts index e344c5bb..6ce65199 100644 --- a/tests/persist-indexeddb.test.ts +++ b/tests/persist-indexeddb.test.ts @@ -1,7 +1,7 @@ import { IDBFactory } from 'fake-indexeddb'; import 'fake-indexeddb/auto'; import { observable } from '../src/observable'; -import { createObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; +import { configureObservablePersistIndexedDB } from '../src/persist-plugins/indexeddb'; import { configureSynced } from '../src/sync/configureSynced'; import { mapSyncPlugins, syncObservable } from '../src/sync/syncObservable'; import type { ObservablePersistPlugin, ObservablePersistPluginOptions } from '../src/sync/syncTypes'; @@ -17,7 +17,7 @@ const persistOptions: ObservablePersistPluginOptions = { tableNames, }, }; -const myIndexedDBPlugin = createObservablePersistIndexedDB(persistOptions.indexedDB!); +const myIndexedDBPlugin = configureObservablePersistIndexedDB(persistOptions.indexedDB!); const mySyncOptions = configureSynced({ persist: { plugin: myIndexedDBPlugin, From cca238d2e724af1dca2d6d83eee2f140ec982df5 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 21 Aug 2024 17:52:28 -0700 Subject: [PATCH 10/17] remove unused setInObservableAtPath, everything uses setAtPath now --- index.ts | 1 - src/helpers.ts | 30 ------------------------------ 2 files changed, 31 deletions(-) diff --git a/index.ts b/index.ts index ff493362..02f14bec 100644 --- a/index.ts +++ b/index.ts @@ -14,7 +14,6 @@ export { mergeIntoObservable, opaqueObject, setAtPath, - setInObservableAtPath, setSilently, } from './src/helpers'; export { diff --git a/src/helpers.ts b/src/helpers.ts index dc387d13..7b783783 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -93,36 +93,6 @@ export function setAtPath( return obj; } -export function setInObservableAtPath( - value$: ObservableParam, - path: string[], - pathTypes: TypeAtPath[], - value: any, - mode: 'assign' | 'set' | 'merge', -) { - let o: any = value$; - let v = value; - for (let i = 0; i < path.length; i++) { - const p = path[i]; - if (!o.peek()[p]) { - o[p].set(initializePathType(pathTypes[i])); - } - o = o[p]; - v = v[p]; - } - - if (v === symbolDelete) { - (o as Observable).delete(); - } - // Assign if possible, or set otherwise - else if (mode === 'assign' && (o as Observable).assign && isObject(o.peek())) { - (o as Observable<{}>).assign(v); - } else if (mode === 'merge') { - mergeIntoObservable(o, v); - } else { - o.set(v); - } -} export function mergeIntoObservable>(target: T, ...sources: any[]): T { if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { if (!isObservable(target)) { From 8747e116488c0ecdca9dd6ef44ac6c09f5bdac20 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Wed, 21 Aug 2024 17:52:41 -0700 Subject: [PATCH 11/17] remove duplicated logic of getValueAtPath --- src/helpers.ts | 7 ++++--- src/onChange.ts | 21 +++------------------ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 7b783783..8c1cfc65 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -2,7 +2,7 @@ import { beginBatch, endBatch } from './batching'; import { getNode, isObservable, setNodeValue, symbolDelete, symbolOpaque } from './globals'; import { hasOwnProperty, isArray, isEmpty, isFunction, isMap, isNumber, isObject, isPrimitive, isSet } from './is'; import type { Change, ObserveEvent, OpaqueObject, Selector, TypeAtPath } from './observableInterfaces'; -import type { Observable, ObservableParam } from './observableTypes'; +import type { ObservableParam } from './observableTypes'; export function computeSelector(selector: Selector, e?: ObserveEvent, retainObservable?: boolean): T { let c = selector as any; @@ -195,12 +195,13 @@ export function initializePathType(pathType: TypeAtPath): any { switch (pathType) { case 'array': return []; - case 'object': - return {}; case 'map': return new Map(); case 'set': return new Set(); + case 'object': + default: + return {}; } } export function applyChange(value: T, change: Change, applyPrevious?: boolean): T { diff --git a/src/onChange.ts b/src/onChange.ts index d89bb57d..03dc3579 100644 --- a/src/onChange.ts +++ b/src/onChange.ts @@ -1,5 +1,5 @@ import { getNodeValue } from './globals'; -import { isArray } from './is'; +import { deconstructObjectWithPath } from './helpers'; import type { ListenerFn, ListenerParams, NodeInfo, NodeListener, TrackingType } from './observableInterfaces'; export function onChange( @@ -106,10 +106,10 @@ export function onChange( function createCb(linkedFromNode: NodeInfo, path: string[], callback: ListenerFn) { // Create a callback for a path that calls it with the current value at the path - let { valueAtPath: prevAtPath } = getValueAtPath(getNodeValue(linkedFromNode), path); + let prevAtPath = deconstructObjectWithPath(path, [], getNodeValue(linkedFromNode)); return function ({ value: valueA, isFromPersist, isFromSync }: ListenerParams) { - const { valueAtPath } = getValueAtPath(valueA, path); + const valueAtPath = deconstructObjectWithPath(path, [], valueA); if (valueAtPath !== prevAtPath) { callback({ value: valueAtPath, @@ -129,18 +129,3 @@ function createCb(linkedFromNode: NodeInfo, path: string[], callback: ListenerFn prevAtPath = valueAtPath; }; } - -function getValueAtPath( - obj: Record, - path: string[], -): { valueAtPath: any; pathTypes: ('object' | 'array')[] } { - let o: Record = obj; - const pathTypes: ('object' | 'array')[] = []; - for (let i = 0; o && i < path.length; i++) { - pathTypes.push(isArray(o) ? 'array' : 'object'); - const p = path[i]; - o = (o as any)[p]; - } - - return { valueAtPath: o, pathTypes }; -} From 41c456884b64e057101d57a9d5ae34a6901b4c9c Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Sat, 24 Aug 2024 13:55:49 -0700 Subject: [PATCH 12/17] minor tweaks to shrink output size --- src/batching.ts | 8 ++++---- src/event.ts | 1 + src/globals.ts | 19 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/batching.ts b/src/batching.ts index a6d71db1..af81f833 100644 --- a/src/batching.ts +++ b/src/batching.ts @@ -69,8 +69,8 @@ export function notify(node: NodeInfo, value: any, prev: any, level: number, whe computeChangesRecursive( changesInBatch, node, - /*loading*/ globalState.isLoadingLocal, - /*remote*/ globalState.isLoadingRemote, + /*loading*/ !!globalState.isLoadingLocal, + /*remote*/ !!globalState.isLoadingRemote, value, [], [], @@ -98,8 +98,8 @@ export function notify(node: NodeInfo, value: any, prev: any, level: number, whe prev, level, whenOptimizedOnlyIf, - isFromSync: globalState.isLoadingRemote, - isFromPersist: globalState.isLoadingLocal, + isFromSync: !!globalState.isLoadingRemote, + isFromPersist: !!globalState.isLoadingLocal, }); } diff --git a/src/event.ts b/src/event.ts index 1c057ed5..54f6b8fb 100644 --- a/src/event.ts +++ b/src/event.ts @@ -8,6 +8,7 @@ export function event(): ObservableEvent { const obs = observable(0); const node = getNode(obs); node.isEvent = true; + return { fire: function () { // Notify increments the value so that the observable changes diff --git a/src/globals.ts b/src/globals.ts index 0dc6a578..33e79f07 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -2,6 +2,16 @@ import { isArray, isChildNode, isDate, isFunction, isMap, isObject, isSet } from import type { NodeInfo, ObservableEvent, TypeAtPath, UpdateFn } from './observableInterfaces'; import type { Observable, ObservableParam } from './observableTypes'; +type GlobalState = { + isLoadingLocal: boolean; + isLoadingRemote: boolean; + activateSyncedNode: (node: NodeInfo, newValue: any) => { update: UpdateFn; value: any }; + pendingNodes: Map void>; + dirtyNodes: Set; + replacer: ((this: any, key: string, value: any) => any) | undefined; + reviver: ((this: any, key: string, value: any) => any) | undefined; +}; + export const symbolToPrimitive = Symbol.toPrimitive; export const symbolIterator = Symbol.iterator; export const symbolGetNode = Symbol('getNode'); @@ -10,15 +20,10 @@ export const symbolOpaque = Symbol('opaque'); export const optimized = Symbol('optimized'); export const symbolLinked = Symbol('linked'); -export const globalState = { - isLoadingLocal: false, - isLoadingRemote: false, - activateSyncedNode: undefined as unknown as (node: NodeInfo, newValue: any) => { update: UpdateFn; value: any }, +export const globalState: GlobalState = { pendingNodes: new Map void>(), dirtyNodes: new Set(), - replacer: undefined as undefined | ((this: any, key: string, value: any) => any), - reviver: undefined as undefined | ((this: any, key: string, value: any) => any), -}; +} as GlobalState; export function isOpaqueObject(value: any) { // React elements have $$typeof and should be treated as opaque From b88957d906e3bbd8585349a3828eeadf0c30dafb Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Sat, 24 Aug 2024 14:09:42 -0700 Subject: [PATCH 13/17] refactor sync retrying to properly call onGetError and onSetError, fix retries were not getting cancelled if a new set changed the value --- src/retry.ts | 49 ++++++++++++++++++++++---------- src/sync-plugins/keel.ts | 8 +++--- src/sync/syncObservable.ts | 58 ++++++++++++++++++++++++-------------- src/sync/syncTypes.ts | 19 +++++++------ tests/persist.test.ts | 6 ++++ 5 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/retry.ts b/src/retry.ts index 5b351878..9b14019a 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -1,5 +1,6 @@ import { isPromise } from './is'; -import type { RetryOptions } from './observableInterfaces'; +import type { NodeInfo, RetryOptions } from './observableInterfaces'; +import type { OnErrorRetryParams, SyncedGetSetBaseParams } from './sync/syncTypes'; function calculateRetryDelay(retryOptions: RetryOptions, retryNum: number): number | null { const { backoff, delay = 1000, infinite, times = 3, maxDelay = 30000 } = retryOptions; @@ -10,39 +11,57 @@ function calculateRetryDelay(retryOptions: RetryOptions, retryNum: number): numb return null; } -function createRetryTimeout(retryOptions: RetryOptions, retryNum: number, fn: () => void) { +function createRetryTimeout(retryOptions: RetryOptions, retryNum: number, fn: () => void): number | false { const delayTime = calculateRetryDelay(retryOptions, retryNum); if (delayTime) { - return setTimeout(fn, delayTime); + return setTimeout(fn, delayTime) as unknown as number; + } else { + return false; } } +const mapRetryTimeouts = new Map(); + export function runWithRetry( - state: { retryNum: number; retry: RetryOptions | undefined }, - fn: (e: { retryNum: number; cancelRetry: () => void }) => T | Promise, + state: SyncedGetSetBaseParams, + retryOptions: RetryOptions | undefined, + fn: (params: OnErrorRetryParams) => T | Promise, + onError: (error: Error) => void, ): T | Promise { - const { retry } = state; - const e = Object.assign(state, { cancel: false, cancelRetry: () => (e.cancel = false) }); - let value = fn(e); + let value = fn(state); - if (isPromise(value) && retry) { - let timeoutRetry: any; - return new Promise((resolve) => { + if (isPromise(value) && retryOptions) { + let timeoutRetry: number; + if (mapRetryTimeouts.has(state.node)) { + clearTimeout(mapRetryTimeouts.get(state.node)); + } + return new Promise((resolve, reject) => { const run = () => { (value as Promise) .then((val: any) => { resolve(val); }) - .catch(() => { + .catch((error: Error) => { state.retryNum++; if (timeoutRetry) { clearTimeout(timeoutRetry); } - if (!e.cancel) { - timeoutRetry = createRetryTimeout(retry, state.retryNum, () => { - value = fn(e); + if (onError) { + onError(error); + } + if (!state.cancelRetry) { + const timeout = createRetryTimeout(retryOptions, state.retryNum, () => { + value = fn(state); run(); }); + + if (timeout === false) { + state.cancelRetry = true; + reject(); + } else { + mapRetryTimeouts.set(state.node, timeout); + timeoutRetry = timeout; + } } }); }; diff --git a/src/sync-plugins/keel.ts b/src/sync-plugins/keel.ts index ce4fabb7..facbc222 100644 --- a/src/sync-plugins/keel.ts +++ b/src/sync-plugins/keel.ts @@ -424,7 +424,7 @@ export function syncedKeel< fn: Function, from: 'create' | 'update' | 'delete', ) => { - const { retryNum, cancelRetry, update } = params; + const { retryNum, update } = params; if ( from === 'create' && @@ -434,7 +434,7 @@ export function syncedKeel< if (__DEV__) { console.log('Creating duplicate data already saved, just ignore.'); } - cancelRetry(); + params.cancelRetry = true; // This has already been saved but didn't update pending changes, so just update with {} to clear the pending state update({ value: {} as TRemote, @@ -445,13 +445,13 @@ export function syncedKeel< if (__DEV__) { console.log('Deleting non-existing data, just ignore.'); } - cancelRetry(); + params.cancelRetry = true; } } else if (error.type === 'bad_request') { keelConfig.onError?.({ error, params, input, type: from, action: fn.name || fn.toString() }); if (retryNum > 4) { - cancelRetry(); + params.cancelRetry = true; } throw new Error(error.message); diff --git a/src/sync/syncObservable.ts b/src/sync/syncObservable.ts index 073d903b..ccfdcb8e 100644 --- a/src/sync/syncObservable.ts +++ b/src/sync/syncObservable.ts @@ -640,7 +640,7 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) { syncOptions.onSetError?.(error, setParams as SyncedSetParams); }; - const setParams: Omit, 'cancelRetry' | 'retryNum'> = { + const setParams: SyncedSetParams = { node, value$: obs$, changes: changesRemote, @@ -659,22 +659,29 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) { } }, refresh: syncState.sync, + retryNum: 0, + cancelRetry: false, }; - let savedPromise = runWithRetry({ retryNum: 0, retry: syncOptions.retry }, async (retryEvent) => { - const params = setParams as SyncedSetParams; - params.cancelRetry = retryEvent.cancelRetry; - params.retryNum = retryEvent.retryNum; + let savedPromise = runWithRetry( + setParams, + syncOptions.retry, + async () => { + return syncOptions!.set!(setParams); + }, + onError, + ); + let didError = false; - return syncOptions!.set!(params); - }); if (isPromise(savedPromise)) { - savedPromise = savedPromise.catch(onError); + savedPromise = savedPromise.catch((error) => { + didError = true; + onError(error); + }); + await savedPromise; } - await savedPromise; - - if (!state$.error.peek()) { + if (!didError) { // If this remote save changed anything then update cache and metadata // Because save happens after a timeout and they're batched together, some calls to save will // return saved data and others won't, so those can be ignored. @@ -907,7 +914,7 @@ export function syncObservable( allSyncStates.set(syncState$, node); syncStateValue.getPendingChanges = () => localState.pendingChanges; - const onError = (error: Error, getParams: SyncedGetParams | undefined, source: 'get' | 'subscribe') => { + const onGetError = (error: Error, getParams: SyncedGetParams | undefined, source: 'get' | 'subscribe') => { syncState$.error.set(error); syncOptions.onGetError?.(error, getParams, source); }; @@ -1090,7 +1097,7 @@ export function syncObservable( }); }, refresh: () => when(syncState$.isLoaded, sync), - onError: (error) => onError(error, undefined, 'subscribe'), + onError: (error) => onGetError(error, undefined, 'subscribe'), }); }; @@ -1102,7 +1109,9 @@ export function syncObservable( } const existingValue = getNodeValue(node); - const getParams: Omit, 'cancelRetry' | 'retryNum'> = { + const onError = (error: Error) => onGetError(error, getParams as SyncedGetParams, 'get'); + + const getParams: SyncedGetParams = { node, value$: obs$, value: isFunction(existingValue) || existingValue?.[symbolLinked] ? undefined : existingValue, @@ -1111,7 +1120,9 @@ export function syncObservable( options: syncOptions, lastSync, updateLastSync: (lastSync: number) => (getParams.lastSync = lastSync), - onError: (error) => onError(error, getParams as SyncedGetParams, 'get'), + onError, + retryNum: 0, + cancelRetry: false, }; let modeBeforeReset: GetMode | undefined = undefined; @@ -1137,12 +1148,17 @@ export function syncObservable( numPendingGets: (syncStateValue.numPendingGets! || 0) + 1, isGetting: true, }); - const got = runWithRetry({ retryNum: 0, retry: syncOptions.retry }, (retryEvent) => { - const params = getParams as SyncedGetParams; - params.cancelRetry = retryEvent.cancelRetry; - params.retryNum = retryEvent.retryNum; - return get(params); - }); + const got = runWithRetry( + getParams, + syncOptions.retry, + (retryEvent) => { + const params = getParams as SyncedGetParams; + params.cancelRetry = retryEvent.cancelRetry; + params.retryNum = retryEvent.retryNum; + return get(params); + }, + onError, + ); const numGets = (node.numGets = (node.numGets || 0) + 1); const handle = (value: any) => { syncState$.numPendingGets.set((v) => v! - 1); diff --git a/src/sync/syncTypes.ts b/src/sync/syncTypes.ts index 42efb0cb..36e4732c 100644 --- a/src/sync/syncTypes.ts +++ b/src/sync/syncTypes.ts @@ -39,24 +39,25 @@ export interface SyncedGetSetSubscribeBaseParams { refresh: () => void; } -export interface SyncedGetParams extends SyncedGetSetSubscribeBaseParams { +export interface SyncedGetSetBaseParams extends SyncedGetSetSubscribeBaseParams, OnErrorRetryParams {} + +export interface OnErrorRetryParams { + retryNum: number; + cancelRetry: boolean; +} + +export interface SyncedGetParams extends SyncedGetSetBaseParams { value: any; lastSync: number | undefined; updateLastSync: (lastSync: number) => void; mode: GetMode; - retryNum: number; - cancelRetry: () => void; onError: (error: Error) => void; options: SyncedOptions; } -export interface SyncedSetParams - extends Pick, 'changes' | 'value'>, - SyncedGetSetSubscribeBaseParams { +export interface SyncedSetParams extends Pick, 'changes' | 'value'>, SyncedGetSetBaseParams { update: UpdateFn; - cancelRetry: () => void; - retryNum: number; - onError: (error: Error) => void; + onError: (error: Error, retryParams: OnErrorRetryParams) => void; } export interface SyncedSubscribeParams extends SyncedGetSetSubscribeBaseParams { diff --git a/tests/persist.test.ts b/tests/persist.test.ts index de392767..97ddf017 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -213,12 +213,15 @@ describe('Pending', () => { test('Pending created and updated', async () => { const persistName = getPersistName(); let isOk = false; + let didSetWrongValue = false; const obs$ = observable( synced({ get: () => { return { test: 'hi' }; }, set: ({ value }) => { + didSetWrongValue = value.test !== obs$.get().test; + expect(value.test).toEqual(obs$.get().test); if (!isOk) { throw new Error('Did not save' + value); } @@ -246,6 +249,8 @@ describe('Pending', () => { // Updates pending obs$.test.set('hello2'); await promiseTimeout(0); + expect(didSetWrongValue).toEqual(false); + pending = state$.getPendingChanges(); expect(pending).toEqual({ test: { p: 'hi', t: ['object'], v: 'hello2' } }); @@ -280,6 +285,7 @@ describe('Pending', () => { pending = state$.getPendingChanges(); expect(pending).toEqual({}); + expect(didSetWrongValue).toEqual(false); }); test('Pending applied if changed', async () => { const persistName = getPersistName(); From 775e86cd56fc6d9f031d8989ad279f4f6833859e Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Sat, 24 Aug 2024 14:47:35 -0700 Subject: [PATCH 14/17] move retrying out of core into sync --- index.ts | 2 -- src/{ => sync}/retry.ts | 6 +++--- src/sync/syncObservable.ts | 14 +++----------- 3 files changed, 6 insertions(+), 16 deletions(-) rename src/{ => sync}/retry.ts (95%) diff --git a/index.ts b/index.ts index 02f14bec..9c54c9a7 100644 --- a/index.ts +++ b/index.ts @@ -70,7 +70,6 @@ import { symbolLinked, } from './src/globals'; import { deepMerge, getValueAtPath, initializePathType, setAtPath } from './src/helpers'; -import { runWithRetry } from './src/retry'; import { tracking } from './src/tracking'; export const internal = { @@ -90,7 +89,6 @@ export const internal = { observableFns, optimized, peek, - runWithRetry, safeParse, safeStringify, set, diff --git a/src/retry.ts b/src/sync/retry.ts similarity index 95% rename from src/retry.ts rename to src/sync/retry.ts index 9b14019a..77afe311 100644 --- a/src/retry.ts +++ b/src/sync/retry.ts @@ -1,6 +1,6 @@ -import { isPromise } from './is'; -import type { NodeInfo, RetryOptions } from './observableInterfaces'; -import type { OnErrorRetryParams, SyncedGetSetBaseParams } from './sync/syncTypes'; +import { isPromise } from '../is'; +import type { NodeInfo, RetryOptions } from '../observableInterfaces'; +import type { OnErrorRetryParams, SyncedGetSetBaseParams } from './syncTypes'; function calculateRetryDelay(retryOptions: RetryOptions, retryNum: number): number | null { const { backoff, delay = 1000, infinite, times = 3, maxDelay = 30000 } = retryOptions; diff --git a/src/sync/syncObservable.ts b/src/sync/syncObservable.ts index ccfdcb8e..29cba631 100644 --- a/src/sync/syncObservable.ts +++ b/src/sync/syncObservable.ts @@ -46,18 +46,10 @@ import type { SyncedOptions, SyncedSetParams, } from './syncTypes'; +import { runWithRetry } from './retry'; -const { - clone, - deepMerge, - getNode, - getNodeValue, - getValueAtPath, - globalState, - runWithRetry, - symbolLinked, - createPreviousHandler, -} = internal; +const { clone, deepMerge, getNode, getNodeValue, getValueAtPath, globalState, symbolLinked, createPreviousHandler } = + internal; export const mapSyncPlugins: WeakMap< ClassConstructor, From d6b75fb687c7c8943e837811cc5743a83cb543b2 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Sat, 24 Aug 2024 17:20:56 -0700 Subject: [PATCH 15/17] Refactor retry error handling --- src/sync-plugins/keel.ts | 9 ++++++--- src/sync/retry.ts | 2 +- src/sync/syncObservable.ts | 21 ++++++++++++++------- src/sync/syncTypes.ts | 12 ++++++++++-- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/sync-plugins/keel.ts b/src/sync-plugins/keel.ts index facbc222..e3e07b7b 100644 --- a/src/sync-plugins/keel.ts +++ b/src/sync-plugins/keel.ts @@ -82,7 +82,10 @@ export interface SyncedKeelConfiguration | 'generateId' > { client: { - auth: { refresh: () => Promise; isAuthenticated: () => Promise }; + auth: { + refresh: () => Promise>; + isAuthenticated: () => Promise>; + }; api: { queries: Record Promise> }; }; realtimePlugin?: KeelRealtimePlugin; @@ -454,11 +457,11 @@ export function syncedKeel< params.cancelRetry = true; } - throw new Error(error.message); + throw new Error(error.message, { cause: { input } }); } else { await handleApiError(error); - throw new Error(error.message); + throw new Error(error.message, { cause: { input } }); } }; diff --git a/src/sync/retry.ts b/src/sync/retry.ts index 77afe311..cb3faa86 100644 --- a/src/sync/retry.ts +++ b/src/sync/retry.ts @@ -57,7 +57,7 @@ export function runWithRetry( if (timeout === false) { state.cancelRetry = true; - reject(); + reject(error); } else { mapRetryTimeouts.set(state.node, timeout); timeoutRetry = timeout; diff --git a/src/sync/syncObservable.ts b/src/sync/syncObservable.ts index 29cba631..2f89eda0 100644 --- a/src/sync/syncObservable.ts +++ b/src/sync/syncObservable.ts @@ -42,9 +42,11 @@ import type { SyncTransform, SyncTransformMethod, Synced, + SyncedErrorParams, SyncedGetParams, SyncedOptions, SyncedSetParams, + SyncedSubscribeParams, } from './syncTypes'; import { runWithRetry } from './retry'; @@ -629,7 +631,11 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) { const onError = (error: Error) => { state$.error.set(error); - syncOptions.onSetError?.(error, setParams as SyncedSetParams); + syncOptions.onSetError?.(error, { + setParams: setParams as SyncedSetParams, + source: 'set', + value$: obs$, + }); }; const setParams: SyncedSetParams = { @@ -906,9 +912,9 @@ export function syncObservable( allSyncStates.set(syncState$, node); syncStateValue.getPendingChanges = () => localState.pendingChanges; - const onGetError = (error: Error, getParams: SyncedGetParams | undefined, source: 'get' | 'subscribe') => { + const onGetError = (error: Error, params: Omit) => { syncState$.error.set(error); - syncOptions.onGetError?.(error, getParams, source); + syncOptions.onGetError?.(error, { ...params, value$: obs$ }); }; loadLocal(obs$, syncOptions, syncState$, localState); @@ -1076,7 +1082,7 @@ export function syncObservable( const subscribe = syncOptions.subscribe; isSubscribed = true; const doSubscribe = () => { - unsubscribe = subscribe({ + const subscribeParams: SyncedSubscribeParams = { node, value$: obs$, lastSync, @@ -1089,8 +1095,9 @@ export function syncObservable( }); }, refresh: () => when(syncState$.isLoaded, sync), - onError: (error) => onGetError(error, undefined, 'subscribe'), - }); + onError: (error: Error) => onGetError(error, { source: 'subscribe', subscribeParams }), + }; + unsubscribe = subscribe(subscribeParams); }; if (waitFor) { @@ -1101,7 +1108,7 @@ export function syncObservable( } const existingValue = getNodeValue(node); - const onError = (error: Error) => onGetError(error, getParams as SyncedGetParams, 'get'); + const onError = (error: Error) => onGetError(error, { getParams, source: 'get' }); const getParams: SyncedGetParams = { node, diff --git a/src/sync/syncTypes.ts b/src/sync/syncTypes.ts index 36e4732c..5ce0d495 100644 --- a/src/sync/syncTypes.ts +++ b/src/sync/syncTypes.ts @@ -66,6 +66,14 @@ export interface SyncedSubscribeParams extends SyncedGetSetSubscribeBas onError: (error: Error) => void; } +export interface SyncedErrorParams { + getParams?: SyncedGetParams; + setParams?: SyncedSetParams; + subscribeParams?: SyncedSubscribeParams; + source: 'get' | 'set' | 'subscribe'; + value$: ObservableParam; +} + export interface SyncedOptions extends Omit, 'get' | 'set'> { get?: (params: SyncedGetParams) => Promise | TRemote; set?: (params: SyncedSetParams) => void | Promise; @@ -76,8 +84,8 @@ export interface SyncedOptions extends Omit; - onGetError?: (error: Error, getParams: SyncedGetParams | undefined, source: 'get' | 'subscribe') => void; - onSetError?: (error: Error, setParams: SyncedSetParams) => void; + onGetError?: (error: Error, params: SyncedErrorParams) => void; + onSetError?: (error: Error, params: SyncedErrorParams) => void; onBeforeGet?: (params: { value: TRemote; lastSync: number | undefined; From d3ff0b28fd090b3bbd91626703f2a890a6fd6f52 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Sat, 24 Aug 2024 17:45:22 -0700 Subject: [PATCH 16/17] fix setAtPath in "merge" mode was sometimes converting strings to objects --- src/helpers.ts | 31 ++++++++++++++++++++----------- tests/tests.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 8c1cfc65..d4a94f35 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -56,7 +56,7 @@ export function setAtPath( } else if (o[p] === undefined && value === undefined && i === path.length - 1) { // If setting undefined and the key is undefined, no need to initialize or set it return obj; - } else if (o[p] === undefined || o[p] === null) { + } else if (i < path.length - 1 && (o[p] === undefined || o[p] === null)) { const child = initializePathType(pathTypes[i]); if (isMap(o)) { o.set(p, child); @@ -219,21 +219,30 @@ export function deepMerge(target: T, ...sources: any[]): T { return sources[sources.length - 1]; } - const result: T = (isArray(target) ? [...target] : { ...target }) as T; + let result: T = (isArray(target) ? [...target] : { ...target }) as T; for (let i = 0; i < sources.length; i++) { const obj2 = sources[i]; - for (const key in obj2) { - if (hasOwnProperty.call(obj2, key)) { - if (obj2[key] instanceof Object && !isObservable(obj2[key]) && Object.keys(obj2[key]).length > 0) { - (result as any)[key] = deepMerge( - (result as any)[key] || (isArray((obj2 as any)[key]) ? [] : {}), - (obj2 as any)[key], - ); - } else { - (result as any)[key] = obj2[key]; + if (isObject(obj2) || isArray(obj2)) { + const objTarget = obj2 as Record; + for (const key in objTarget) { + if (hasOwnProperty.call(objTarget, key)) { + if ( + objTarget[key] instanceof Object && + !isObservable(objTarget[key]) && + Object.keys(objTarget[key]).length > 0 + ) { + (result as any)[key] = deepMerge( + (result as any)[key] || (isArray((objTarget as any)[key]) ? [] : {}), + (objTarget as any)[key], + ); + } else { + (result as any)[key] = objTarget[key]; + } } } + } else { + result = obj2; } } diff --git a/tests/tests.test.ts b/tests/tests.test.ts index 9ae13b89..db2f8f19 100644 --- a/tests/tests.test.ts +++ b/tests/tests.test.ts @@ -3351,6 +3351,51 @@ describe('setAtPath', () => { expect(Object.keys(res)).toEqual([]); expect(res).toEqual({}); }); + test('Set on empty object', () => { + const value = {}; + + const res = setAtPath(value, ['key', 'status'], ['object', 'object'], 'Completed'); + + expect(res).toEqual({ key: { status: 'Completed' } }); + }); + test('Set with merge on empty object', () => { + const value = {}; + + const res = setAtPath(value, ['key', 'status'], ['object', 'object'], 'Completed', 'merge'); + + expect(res).toEqual({ key: { status: 'Completed' } }); + }); + test('Set with merge on array', () => { + const value = { + arr: [{ id: 1 }, { id: 2 }, { id: 3 }], + }; + + const res = setAtPath(value, ['arr', '1', 'id'], ['object', 'object', 'object'], 22, 'merge'); + + expect(res).toEqual({ arr: [{ id: 1 }, { id: 22 }, { id: 3 }] }); + }); + test('Set on object with existing key', () => { + const value = { + key: { + sessionId: 'zz', + }, + }; + + const res = setAtPath(value, ['key', 'status'], ['object', 'object'], 'Completed'); + + expect(res).toEqual({ key: { sessionId: 'zz', status: 'Completed' } }); + }); + test('Set with merge on object with existing key', () => { + const value = { + key: { + sessionId: 'zz', + }, + }; + + const res = setAtPath(value, ['key', 'status'], ['object', 'object'], 'Completed', 'merge'); + + expect(res).toEqual({ key: { sessionId: 'zz', status: 'Completed' } }); + }); }); describe('new computed', () => { test('new computed basic', () => { From 5c7085e0f4f68d8e1b9bdbf472ce6f90249c9a61 Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Sat, 24 Aug 2024 18:11:28 -0700 Subject: [PATCH 17/17] 3.0.0-alpha.30 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02e2111e..81d30803 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@legendapp/state", - "version": "3.0.0-alpha.29", + "version": "3.0.0-alpha.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@legendapp/state", - "version": "3.0.0-alpha.29", + "version": "3.0.0-alpha.30", "license": "MIT", "dependencies": { "use-sync-external-store": "^1.2.0" diff --git a/package.json b/package.json index cd19f7c4..e0f349fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@legendapp/state", - "version": "3.0.0-alpha.29", + "version": "3.0.0-alpha.30", "description": "legend-state", "sideEffects": false, "private": true, @@ -137,4 +137,4 @@ "@commitlint/config-conventional" ] } -} \ No newline at end of file +}