From 2440cf069978dae1cac17452830d7939e5cf81a6 Mon Sep 17 00:00:00 2001 From: kvba Date: Wed, 20 Aug 2025 11:45:20 +0200 Subject: [PATCH 1/3] feat: add watermelon db persist plugin --- package.json | 1 + src/persist-plugins/watermelondb.ts | 95 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/persist-plugins/watermelondb.ts diff --git a/package.json b/package.json index fb82f3cfd..6707564c1 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@evilmartians/lefthook": "^1.6.13", "@happy-dom/global-registrator": "^14.12.0", "@jest/globals": "^29.7.0", + "@nozbe/watermelondb": "^0.28.0", "@react-native-async-storage/async-storage": "^1.23.1", "@release-it/conventional-changelog": "^8.0.1", "@supabase/supabase-js": "^2.43.4", diff --git a/src/persist-plugins/watermelondb.ts b/src/persist-plugins/watermelondb.ts new file mode 100644 index 000000000..6edc311b7 --- /dev/null +++ b/src/persist-plugins/watermelondb.ts @@ -0,0 +1,95 @@ +import { applyChanges } from '@legendapp/state' + +import type { + ObservablePersistPlugin, + PersistMetadata, + PersistOptions, +} from '@legendapp/state/sync' +import type { Change } from '@legendapp/state'; +import type LocalStorage from '@nozbe/watermelondb/Database/LocalStorage' + +const MetadataSuffix = '__m' + +class ObservablePersistWatermelonDB implements ObservablePersistPlugin { + private readonly storage: LocalStorage + private data: Record = {} + + constructor(storage: LocalStorage) { + if (!storage) { + console.error( + '[legend-state] ObservablePersistWatermelonDB failed to initialize. You need to pass the WatermelonDB localStorage instance.' + ) + } + + this.storage = storage + } + + getTable(table: string, init: any): T { + if (!this.storage) return undefined + + if (this.data[table] === undefined) { + try { + this.storage._getSync(table, (val) => { + if (val !== undefined) this.data[table] = val + }) + } catch (e) { + console.error('[legend-state] ObservablePersistWatermelonDB parse failed', table, e) + } + } + + return this.data[table] + } + + deleteTable(table: string): Promise | void { + if (!this.storage) return undefined + + delete this.data[table] + return this.storage.remove(table) + } + + set(table: string, changes: Change[]): Promise | void { + const current = this.data[table] ?? {} + const updated = applyChanges(current, changes) + this.data[table] = updated + + return this.storage.set(table, updated) + } + + getMetadata(table: string, _config: PersistOptions): PersistMetadata { + return this.getTable(table + MetadataSuffix, {}) + } + + setMetadata(table: string, metadata: PersistMetadata): Promise | void { + const key = table + MetadataSuffix + this.data[key] = metadata + return this.storage.set(key, metadata) + } + + deleteMetadata(table: string): Promise | void { + const key = table + MetadataSuffix + delete this.data[key] + return this.storage.remove(key) + } +} + +/** + * Usage: + * ```ts + * import { syncObservable } from '@legendapp/state/sync' + * import { observable } from '@legendapp/state' + * import { observablePersistWatermelonDB } from '@legendapp/state/persist-plugins/watermelondb' + * import { database } from '@/lib/db' // Watermelon Database + * + * const settings$ = observable(true) + * + * syncObservable(settings$, { + * persist: { + * name: 'settings', + * plugin: observablePersistWatermelonDB(database.localStorage), + * }, + * }) + * ``` + */ +export function observablePersistWatermelonDB(localStorage: LocalStorage) { + return new ObservablePersistWatermelonDB(localStorage) +} From 2bd8458d897c479d8cfbbdd567932f22a6285c1b Mon Sep 17 00:00:00 2001 From: kvba Date: Wed, 20 Aug 2025 11:49:12 +0200 Subject: [PATCH 2/3] feat: prettier --- src/persist-plugins/watermelondb.ts | 62 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/src/persist-plugins/watermelondb.ts b/src/persist-plugins/watermelondb.ts index 6edc311b7..b4a66de6e 100644 --- a/src/persist-plugins/watermelondb.ts +++ b/src/persist-plugins/watermelondb.ts @@ -1,74 +1,70 @@ -import { applyChanges } from '@legendapp/state' +import { applyChanges } from '@legendapp/state'; -import type { - ObservablePersistPlugin, - PersistMetadata, - PersistOptions, -} from '@legendapp/state/sync' +import type { ObservablePersistPlugin, PersistMetadata, PersistOptions } from '@legendapp/state/sync'; import type { Change } from '@legendapp/state'; -import type LocalStorage from '@nozbe/watermelondb/Database/LocalStorage' +import type LocalStorage from '@nozbe/watermelondb/Database/LocalStorage'; -const MetadataSuffix = '__m' +const MetadataSuffix = '__m'; class ObservablePersistWatermelonDB implements ObservablePersistPlugin { - private readonly storage: LocalStorage - private data: Record = {} + private readonly storage: LocalStorage; + private data: Record = {}; constructor(storage: LocalStorage) { if (!storage) { console.error( - '[legend-state] ObservablePersistWatermelonDB failed to initialize. You need to pass the WatermelonDB localStorage instance.' - ) + '[legend-state] ObservablePersistWatermelonDB failed to initialize. You need to pass the WatermelonDB localStorage instance.', + ); } - this.storage = storage + this.storage = storage; } getTable(table: string, init: any): T { - if (!this.storage) return undefined + if (!this.storage) return undefined; if (this.data[table] === undefined) { try { this.storage._getSync(table, (val) => { - if (val !== undefined) this.data[table] = val - }) + if (val !== undefined) this.data[table] = val; + }); } catch (e) { - console.error('[legend-state] ObservablePersistWatermelonDB parse failed', table, e) + console.error('[legend-state] ObservablePersistWatermelonDB parse failed', table, e); } } - return this.data[table] + return this.data[table]; } deleteTable(table: string): Promise | void { - if (!this.storage) return undefined + if (!this.storage) return undefined; - delete this.data[table] - return this.storage.remove(table) + delete this.data[table]; + return this.storage.remove(table); } set(table: string, changes: Change[]): Promise | void { - const current = this.data[table] ?? {} - const updated = applyChanges(current, changes) - this.data[table] = updated + const current = this.data[table] ?? {}; + const updated = applyChanges(current, changes); + this.data[table] = updated; - return this.storage.set(table, updated) + return this.storage.set(table, updated); } getMetadata(table: string, _config: PersistOptions): PersistMetadata { - return this.getTable(table + MetadataSuffix, {}) + return this.getTable(table + MetadataSuffix, {}); } setMetadata(table: string, metadata: PersistMetadata): Promise | void { - const key = table + MetadataSuffix - this.data[key] = metadata - return this.storage.set(key, metadata) + const key = table + MetadataSuffix; + this.data[key] = metadata; + return this.storage.set(key, metadata); } deleteMetadata(table: string): Promise | void { - const key = table + MetadataSuffix - delete this.data[key] - return this.storage.remove(key) + const key = table + MetadataSuffix; + delete this.data[key]; + return this.storage.remove(key); } } @@ -91,5 +87,5 @@ class ObservablePersistWatermelonDB implements ObservablePersistPlugin { * ``` */ export function observablePersistWatermelonDB(localStorage: LocalStorage) { - return new ObservablePersistWatermelonDB(localStorage) + return new ObservablePersistWatermelonDB(localStorage); } From 1d4d5b7e103c412ba7d396703069b28b74f7c9a7 Mon Sep 17 00:00:00 2001 From: kvba Date: Wed, 20 Aug 2025 13:09:27 +0200 Subject: [PATCH 3/3] feat: types fix and safe parse-stringify --- src/persist-plugins/watermelondb.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/persist-plugins/watermelondb.ts b/src/persist-plugins/watermelondb.ts index b4a66de6e..59d78314a 100644 --- a/src/persist-plugins/watermelondb.ts +++ b/src/persist-plugins/watermelondb.ts @@ -1,4 +1,4 @@ -import { applyChanges } from '@legendapp/state'; +import { applyChanges, internal } from '@legendapp/state'; import type { ObservablePersistPlugin, PersistMetadata, PersistOptions } from '@legendapp/state/sync'; import type { Change } from '@legendapp/state'; @@ -6,13 +6,15 @@ import type LocalStorage from '@nozbe/watermelondb/Database/LocalStorage'; const MetadataSuffix = '__m'; +const { safeParse, safeStringify } = internal; + class ObservablePersistWatermelonDB implements ObservablePersistPlugin { private readonly storage: LocalStorage; private data: Record = {}; constructor(storage: LocalStorage) { if (!storage) { - console.error( + throw new Error( '[legend-state] ObservablePersistWatermelonDB failed to initialize. You need to pass the WatermelonDB localStorage instance.', ); } @@ -21,25 +23,24 @@ class ObservablePersistWatermelonDB implements ObservablePersistPlugin { } getTable(table: string, init: any): T { - if (!this.storage) return undefined; - if (this.data[table] === undefined) { try { this.storage._getSync(table, (val) => { - if (val !== undefined) this.data[table] = val; + this.data[table] = val ? safeParse(val) : init; }); } catch (e) { console.error('[legend-state] ObservablePersistWatermelonDB parse failed', table, e); } } - return this.data[table]; + return this.data[table] as T; } deleteTable(table: string): Promise | void { if (!this.storage) return undefined; delete this.data[table]; + return this.storage.remove(table); } @@ -48,7 +49,7 @@ class ObservablePersistWatermelonDB implements ObservablePersistPlugin { const updated = applyChanges(current, changes); this.data[table] = updated; - return this.storage.set(table, updated); + return this.storage.set(table, safeStringify(updated)); } getMetadata(table: string, _config: PersistOptions): PersistMetadata { @@ -58,12 +59,14 @@ class ObservablePersistWatermelonDB implements ObservablePersistPlugin { setMetadata(table: string, metadata: PersistMetadata): Promise | void { const key = table + MetadataSuffix; this.data[key] = metadata; + return this.storage.set(key, metadata); } deleteMetadata(table: string): Promise | void { const key = table + MetadataSuffix; delete this.data[key]; + return this.storage.remove(key); } } @@ -71,12 +74,8 @@ class ObservablePersistWatermelonDB implements ObservablePersistPlugin { /** * Usage: * ```ts - * import { syncObservable } from '@legendapp/state/sync' - * import { observable } from '@legendapp/state' - * import { observablePersistWatermelonDB } from '@legendapp/state/persist-plugins/watermelondb' - * import { database } from '@/lib/db' // Watermelon Database - * - * const settings$ = observable(true) + * import { observablePersistWatermelonDB } from '@legendapp/state/sync-plugins/ObservablePersistWatermelonDB' + * import { database } from '@/lib/db' * * syncObservable(settings$, { * persist: {