Skip to content

Commit

Permalink
storage: fix async initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
atk committed Dec 9, 2024
1 parent 1daf96b commit 25d82b2
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 6 deletions.
3 changes: 2 additions & 1 deletion packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
}
},
"devDependencies": {
"solid-js": "^1.8.15"
"solid-js": "^1.8.15",
"localforage": "^1.10.0"
}
}
10 changes: 6 additions & 4 deletions packages/storage/src/persisted.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -60,6 +60,7 @@ export type PersistenceSyncAPI = [

export type PersistenceOptions<T, O extends Record<string, any> | undefined> = {
name?: string;
owner?: Owner;
serialize?: (data: T) => string;
deserialize?: (data: string) => T;
sync?: PersistenceSyncAPI;
Expand Down Expand Up @@ -123,6 +124,7 @@ export function makePersisted<
if (!storage) {
return [signal[0], signal[1], null] as PersistedState<S>;
}
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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
64 changes: 63 additions & 1 deletion packages/storage/test/persisted.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {};
Expand Down Expand Up @@ -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,
Expand All @@ -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");
});
});
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 25d82b2

Please sign in to comment.