diff --git a/packages/storage/package.json b/packages/storage/package.json index 8fe6d0473..3eb4491a4 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -99,6 +99,7 @@ } }, "devDependencies": { - "solid-js": "^1.8.15" + "solid-js": "^1.8.15", + "localforage": "^1.10.0" } } diff --git a/packages/storage/src/persisted.ts b/packages/storage/src/persisted.ts index a8f26a1ba..16d227f8b 100644 --- a/packages/storage/src/persisted.ts +++ b/packages/storage/src/persisted.ts @@ -1,5 +1,5 @@ -import type { Accessor, Setter, Signal } from "solid-js"; -import { createUniqueId, untrack } from "solid-js"; +import type { Accessor, Setter, Signal, Owner } from "solid-js"; +import { createUniqueId, untrack, getOwner, runWithOwner } from "solid-js"; import { isServer, isDev } from "solid-js/web"; import type { SetStoreFunction, Store } from "solid-js/store"; import { reconcile } from "solid-js/store"; @@ -60,6 +60,7 @@ export type PersistenceSyncAPI = [ export type PersistenceOptions | undefined> = { name?: string; + owner?: Owner; serialize?: (data: T) => string; deserialize?: (data: string) => T; sync?: PersistenceSyncAPI; @@ -123,6 +124,7 @@ export function makePersisted< if (!storage) { return [signal[0], signal[1], null] as PersistedState; } + const owner = options.owner || getOwner(); const storageOptions = (options as unknown as { storageOptions: O }).storageOptions; const serialize: (data: T) => string = options.serialize || JSON.stringify.bind(JSON); const deserialize: (data: string) => T = options.deserialize || JSON.parse.bind(JSON); @@ -132,7 +134,7 @@ export function makePersisted< ? (data: string) => { try { const value = deserialize(data); - (signal[1] as any)(() => value); + runWithOwner(owner, () => (signal[1] as any)(() => value)); } catch (e) { // eslint-disable-next-line no-console if (isDev) console.warn(e); @@ -141,7 +143,7 @@ export function makePersisted< : (data: string) => { try { const value = deserialize(data); - (signal[1] as any)(reconcile(value)); + runWithOwner(owner, () => (signal[1] as any)(reconcile(value))); } catch (e) { // eslint-disable-next-line no-console if (isDev) console.warn(e); diff --git a/packages/storage/test/persisted.test.ts b/packages/storage/test/persisted.test.ts index dc0f0d689..a5459e490 100644 --- a/packages/storage/test/persisted.test.ts +++ b/packages/storage/test/persisted.test.ts @@ -3,6 +3,7 @@ import { createSignal } from "solid-js"; import { createStore } from "solid-js/store"; import { makePersisted } from "../src/persisted.js"; import { AsyncStorage } from "../src/index.js"; +import * as localforage from "localforage"; describe("makePersisted", () => { let data: Record = {}; @@ -133,7 +134,7 @@ describe("makePersisted", () => { it("exposes the initial value as third part of the return tuple", () => { const anotherMockAsyncStorage = { ...mockAsyncStorage }; - const promise = Promise.resolve("init"); + const promise = Promise.resolve('"init"'); anotherMockAsyncStorage.getItem = () => promise; const [_signal, _setSignal, init] = makePersisted(createSignal("default"), { storage: anotherMockAsyncStorage, @@ -142,3 +143,64 @@ describe("makePersisted", () => { expect(init).toBe(promise); }); }); + +describe("makePersisted and localforage", () => { + it("saves into a signal and reads from localforage", async () => { + const [test, setTest] = makePersisted(createSignal("initial"), { + storage: localforage, + name: "test9", + }); + expect(test()).toBe("initial"); + setTest("overwritten"); + let count = 0; + while (test() === "initial" && count++ < 10) { + await new Promise(r => setTimeout(r, 100)); + } + const [test2] = makePersisted( + createSignal("initial"), + { storage: localforage, name: "test9" } + ); + count = 0; + while (test2() === "initial" && count++ < 10) { + await new Promise(r => setTimeout(r, 100)); + } + expect(test2()).toBe("overwritten"); + }); + + it("saves into a store with getters and reads from localforage", async () => { + localforage.setItem("test10", '{"x":"foo","y":"foobar"}'); + const [test, setTest] = makePersisted( + createStore({ + x: "foo", + get y() { + return this.x + "bar"; + }, + }), + { storage: localforage, name: "test10" }, + ); + setTest("x", "boo"); + let count = 0; + while (test.y === "foobar" && count++ < 10) { + await new Promise(r => setTimeout(r, 100)); + } + expect(test.y).toBe("boobar"); + }); + + it("reads into a store from previously saved data from localforage", async () => { + localforage.setItem("test11", JSON.stringify({ x: "zoo", y: "zoobar" })); + const [test] = makePersisted( + createStore({ + x: "foo", + get y() { + return this.x + "bar"; + }, + }), + { storage: localforage, name: "test11" }, + ); + let count = 0; + while (test.y === "foobar" && count++ < 10) { + await new Promise(r => setTimeout(r, 100)); + } + expect(test.y).toBe("zoobar"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index addbfa8d1..390d300de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -877,6 +877,9 @@ importers: specifier: '*' version: 2.0.0 devDependencies: + localforage: + specifier: ^1.10.0 + version: 1.10.0 solid-js: specifier: ^1.8.15 version: 1.8.20 @@ -4320,6 +4323,9 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -4599,6 +4605,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -4631,6 +4640,9 @@ packages: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} + localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -10319,6 +10331,8 @@ snapshots: ignore@5.3.1: {} + immediate@3.0.6: {} + immer@9.0.21: {} immutable@3.7.6: {} @@ -10589,6 +10603,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.1.1: + dependencies: + immediate: 3.0.6 + lilconfig@2.1.0: {} lilconfig@3.1.2: {} @@ -10638,6 +10656,10 @@ snapshots: mlly: 1.7.1 pkg-types: 1.1.3 + localforage@1.10.0: + dependencies: + lie: 3.1.1 + locate-path@5.0.0: dependencies: p-locate: 4.1.0