Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-memes-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/storage": minor
---

types and resource usage
6 changes: 5 additions & 1 deletion packages/storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,17 @@ Instead of wrapping the resource itself, it is far simpler to use the `storage`
persisted signal or [deep signal](../resource/#createdeepsignal):

```ts
const [resource] = createResource(fetcher, { storage: makePersisted(createSignal()) });
const [resource] = createResource(fetcher, {
storage: value => makePersisted(createSignal(value)),
});
```

If you are using an asynchronous storage to persist the state of a resource, it might receive an update due to being
initialized from the storage before or after the fetcher resolved. If the initialization resolves after the fetcher, its
result is discarded not to overwrite more current data.

If instead of a signal you want to use a store, consider using the `makeDeepSignal` primitive from the `resource` package.

### Using `makePersisted` with Suspense

In case you are using an asynchronous storage and want the initialisation mesh into Suspense instead of mixing it with Show, we provide the output of the initialisation as third part of the returned tuple:
Expand Down
77 changes: 34 additions & 43 deletions packages/storage/src/persisted.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Accessor, Setter, Signal } from "solid-js";
import type { Setter, Signal } from "solid-js";
import { createUniqueId, untrack } from "solid-js";
import { isServer, isDev } from "solid-js/web";
import type { SetStoreFunction, Store } from "solid-js/store";
Expand Down Expand Up @@ -70,17 +70,7 @@ export type PersistenceOptions<T, O extends Record<string, any> | undefined> = {
storageOptions?: O;
});

export type SignalInput = Signal<any> | [Store<any>, SetStoreFunction<any>];

export type SignalType<S extends SignalInput> =
S extends Signal<infer T> ? T : S extends [Store<infer T>, SetStoreFunction<infer T>] ? T : never;

export type PersistedState<S extends SignalInput> =
S extends Signal<infer T>
? [get: Accessor<T>, set: Setter<T>, init: Promise<string> | string | null]
: S extends [Store<infer T>, SetStoreFunction<infer T>]
? [get: Store<T>, set: SetStoreFunction<T>, init: Promise<string> | string | null]
: never;
export type PersistedState<S> = S & { 2: Promise<string> | string | null };

/**
* Persists a signal, store or similar API
Expand All @@ -102,26 +92,27 @@ export type PersistedState<S extends SignalInput> =
* @param {PersistenceOptions<T, O>} options - The options for persistence.
* @returns {PersistedState<T>} - The persisted signal or store.
*/
export function makePersisted<S extends SignalInput>(
signal: S,
options?: PersistenceOptions<SignalType<S>, undefined>,
): PersistedState<S>;
export function makePersisted<S extends SignalInput, O extends Record<string, any>>(
export function makePersisted<T, S extends Signal<T> | [Store<T>, SetStoreFunction<T>]>(
signal: S,
options: PersistenceOptions<SignalType<S>, O>,
options?: PersistenceOptions<T, undefined>,
): PersistedState<S>;
export function makePersisted<
S extends SignalInput,
T,
S extends Signal<T> | [Store<T>, SetStoreFunction<T>],
O extends Record<string, any>,
>(signal: S, options: PersistenceOptions<T, O>): PersistedState<S>;
export function makePersisted<
T,
S extends Signal<T> | [Store<T>, SetStoreFunction<T>],
O extends Record<string, any> | undefined,
T = SignalType<S>,
>(
signal: S,
options: PersistenceOptions<T, O> = {} as PersistenceOptions<T, O>,
): PersistedState<S> {
const storage = options.storage || (globalThis.localStorage as Storage | undefined);
const name = options.name || `storage-${createUniqueId()}`;
if (!storage) {
return [signal[0], signal[1], null] as PersistedState<S>;
return Object.assign(signal, { 2: null });
}
const storageOptions = (options as unknown as { storageOptions: O }).storageOptions;
const serialize: (data: T) => string = options.serialize || JSON.stringify.bind(JSON);
Expand Down Expand Up @@ -167,28 +158,28 @@ export function makePersisted<
});
}

return [
signal[0],
typeof signal[0] === "function"
? (value?: T | ((prev: T) => T)) => {
const output = (signal[1] as Setter<T>)(value as any);
const serialized: string | null | undefined =
value != null ? serialize(output) : (value as null | undefined);
options.sync?.[1](name, serialized);
if (serialized != null) storage.setItem(name, serialized, storageOptions);
else storage.removeItem(name, storageOptions);
unchanged = false;
return output;
}
: (...args: any[]) => {
(signal[1] as any)(...args);
const value = serialize(untrack(() => signal[0]));
options.sync?.[1](name, value);
storage.setItem(name, value, storageOptions);
unchanged = false;
},
init,
] as PersistedState<S>;
return Object.assign([], signal, {
1:
typeof signal[0] === "function"
? (value?: T | ((prev: T) => T)) => {
const output = (signal[1] as Setter<T>)(value as any);
const serialized: string | null | undefined =
value != null ? serialize(output) : (value as null | undefined);
options.sync?.[1](name, serialized);
if (serialized != null) storage.setItem(name, serialized, storageOptions);
else storage.removeItem(name, storageOptions);
unchanged = false;
return output;
}
: (...args: any[]) => {
(signal[1] as any)(...args);
const value = untrack(() => serialize(signal[0] as T));
options.sync?.[1](name, value);
storage.setItem(name, value, storageOptions);
unchanged = false;
},
2: init,
}) as PersistedState<S>;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/test/persisted.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,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 Down
Loading