Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9404090
upgrade: storage package upgrade for Solid 2.0
atk Apr 21, 2026
ef953e9
Deep changes amongst a number of pritmivies
davedbase Apr 21, 2026
4a2b0ae
Added additional adjustments and migrations for rootless, static-stor…
davedbase Apr 21, 2026
6a2a17e
Remove deprecated HTMLFrameSetElement type
davedbase Apr 21, 2026
383da78
Update to latest solid beta, ensure tests run and ran formatting
davedbase May 2, 2026
0ceb475
Adds Solid 2.0 columns to README page
davedbase May 3, 2026
25d10f8
Oops add different method for checking Solid 2
davedbase May 3, 2026
c85f0ac
port to tanstack start v1
birkskyum Apr 11, 2026
3e602a0
fix import to manifest
birkskyum Apr 11, 2026
d72c4c3
footer
birkskyum Apr 11, 2026
2cb49ea
add vite preview
birkskyum Apr 11, 2026
c0f20d7
improe handling of not found
birkskyum Apr 11, 2026
a96bfb2
footer
birkskyum Apr 11, 2026
3d132af
improve not found
birkskyum Apr 11, 2026
31247bb
clear suspense
birkskyum Apr 11, 2026
9e5eaf5
fix not found
birkskyum Apr 11, 2026
e00599f
filter prerender
birkskyum Apr 11, 2026
aa218ec
relative link for playground
birkskyum Apr 11, 2026
ecb0763
replace solid tippy with floating-ui/dom
birkskyum Apr 13, 2026
37d3d4f
pr feedback
birkskyum Apr 27, 2026
417e029
Format
davedbase Apr 27, 2026
224d8fa
Fix lock
davedbase May 4, 2026
871aabf
Update
davedbase May 4, 2026
153c169
upgrade: storage package upgrade for Solid 2.0
atk Apr 21, 2026
19b9ee8
fix: attempt to fix optimistic (WIP)
atk May 8, 2026
98846a5
fix: optimistic, stores, tests (todo: types)
atk May 13, 2026
8d1ef6f
fix: types
atk May 13, 2026
aea1e1f
feat: update to 2.0.0-beta.12
atk May 13, 2026
2df91dc
Merge branch 'next' into update/v2/storage
atk May 13, 2026
61baf95
Merge branch 'next'
atk May 29, 2026
ba4f617
Merge branch 'next' into update/v2/storage
davedbase May 29, 2026
7719d6f
Merge branch 'next' into update/v2/storage
davedbase Jun 2, 2026
aff1087
Added stories
davedbase Jun 2, 2026
524e2f0
Remove old dev example
davedbase Jun 2, 2026
1c35ccf
Added more stories
davedbase Jun 2, 2026
1956ca9
fix: pr comments
atk Jun 7, 2026
3c953ab
feat: fine-grained update from storage initialization
atk Jun 9, 2026
2ee953b
Merge branch 'next' into update/v2/storage
davedbase Jun 9, 2026
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/eleven-buttons-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/storage": major
---

**`makePersisted`** - simplify setter, Solid 2.0 adaption, simpler types using function overloads
18 changes: 0 additions & 18 deletions packages/storage/dev/index.tsx

This file was deleted.

10 changes: 4 additions & 6 deletions packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
}
},
"scripts": {
"dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts",
"build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts",
"vitest": "vitest -c ../../configs/vitest.config.ts",
"test": "pnpm run vitest",
Expand All @@ -82,17 +81,16 @@
},
"peerDependencies": {
"@tauri-apps/plugin-store": "*",
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.14",
"solid-js": "^2.0.0-beta.14"
},
"peerDependenciesMeta": {
"solid-start": {
"optional": true
},
"@tauri-apps/plugin-store": {
"optional": true
}
},
"devDependencies": {
"solid-js": "^1.9.7"
"solid-js": "2.0.0-beta.14",
"@solidjs/web": "2.0.0-beta.14"
}
}
2 changes: 1 addition & 1 deletion packages/storage/src/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getRequestEvent, isServer, type RequestEvent } from "solid-js/web";
import { getRequestEvent, isServer, type RequestEvent } from "@solidjs/web";
import { type SyncStorageWithOptions } from "./index.js";
import { addWithOptionsMethod, addClearMethod } from "./tools.js";

Expand Down
192 changes: 104 additions & 88 deletions packages/storage/src/persisted.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Accessor, 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";
import { reconcile } from "solid-js/store";
import type { Signal, StoreSetter, Store } from "solid-js";
import { action, createUniqueId, latest, untrack, reconcile, snapshot, DEV } from "solid-js";
import { isServer } from "@solid-primitives/utils";

export type SyncStorage = {
getItem: (key: string) => string | null;
Expand Down Expand Up @@ -58,137 +56,155 @@ export type PersistenceSyncAPI = [
update: (key: string, value: string | null | undefined) => void,
];

export type PersistenceOptions<T, O extends Record<string, any> | undefined> = {
export type PersistenceOptions<
S extends Signal<any> | [Store<any>, StoreSetter<any>] | readonly [Store<any>, StoreSetter<any>],
O extends Record<string, any> | undefined,
T = S extends Signal<infer T> ? T : S extends [Store<infer T>, StoreSetter<infer T>] ? T : never
> = {
name?: string;
serialize?: (data: T) => string;
deserialize?: (data: string) => T;
sync?: PersistenceSyncAPI;
action?: (signal: S) => Parameters<typeof action>[0];
} & (undefined extends O
? { storage?: SyncStorage | AsyncStorage }
: {
storage: SyncStorageWithOptions<O> | AsyncStorageWithOptions<O>;
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, I = Promise<string> | string | null> = S extends [any, any] ? [...S, I] : 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 StoreTuple<T> = [Store<T>, StoreSetter<T>];

/**
* Persists a signal, store or similar API
* ```ts
* const [getter, setter] = makePersisted(createSignal("data"), options);
* const options = {
* storage: cookieStorage, // can be any synchronous or asynchronous storage
* storageOptions: { ... }, // for storages with options, otherwise not needed
* name: "solid-data", // optional
* serialize: (value: string) => value, // optional
* deserialize: (data: string) => data, // optional
* };
* ```
* Can be used with `createSignal` or `createStore`. The initial value from the storage will overwrite the initial
* value of the signal or store unless overwritten. Overwriting a signal with `null` or `undefined` will remove the
* item from the storage.
* ```ts
* const [getter, setter] = makePersisted(createSignal("data"), options);
* const options = {
* storage: cookieStorage, // can be any synchronous or asynchronous storage
* storageOptions: { ... }, // for storages with options, otherwise not needed
* name: "solid-data", // optional
* serialize: (value: string) => value, // optional
* deserialize: (data: string) => data, // optional
* action: (setter: Setter<T>) => Setter<T> // optional, to be put inside action
* };
* ```
* Can be used with `createSignal`, `createStore`, `createOptimistic`, or `createOptimisticStore` (for the latter
* two, use options.action to wrap the setter). The initial value from the storage will overwrite the initial
* value of the signal or store unless overwritten. Overwriting a signal with `null` or `undefined` will remove the
* item from the storage.
*
* @param {Signal<T> | [get: Store<T>, set: SetStoreFunction<T>]} signal - The signal or store to be persisted.
* @param {PersistenceOptions<T, O>} options - The options for persistence.
* @param {Signal<T> | [get: Store<T>, set: StoreSetter<T>]} signal - The signal or store to be persisted.
* @param {PersistenceOptions<Signal<T> | [get: Store<T>, set StoreSetter<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>>(
signal: S,
options: PersistenceOptions<SignalType<S>, O>,
): PersistedState<S>;
export function makePersisted<T>(
signal: Signal<T>,
options?: PersistenceOptions<Signal<T>, undefined>,
): PersistedState<Signal<T>>;
export function makePersisted<T>(
signal: StoreTuple<T>,
options?: PersistenceOptions<StoreTuple<T>, undefined>,
): PersistedState<StoreTuple<T>>;
export function makePersisted<
T,
O extends Record<string, any>
>(signal: Signal<T>, options: PersistenceOptions<Signal<T>, O>): PersistedState<Signal<T>>;
export function makePersisted<
T,
O extends Record<string, any>
>(signal: Signal<T>, options: PersistenceOptions<Signal<T>, O> & { storage: AsyncStorage | AsyncStorageWithOptions<O> }): PersistedState<Signal<T>, Promise<string> | null>;
export function makePersisted<
T,
O extends Record<string, any>
>(signal: StoreTuple<T>, options: PersistenceOptions<StoreTuple<T>, O>): PersistedState<StoreTuple<T>>;
export function makePersisted<
S extends SignalInput,
T,
O extends Record<string, any>
>(signal: StoreTuple<T>, options: PersistenceOptions<StoreTuple<T>, O> & { storage: AsyncStorage | AsyncStorageWithOptions<O> }): PersistedState<StoreTuple<T>, Promise<string> | null>;
export function makePersisted<
T,
O extends Record<string, any> | undefined,
T = SignalType<S>,
S extends Signal<T> | StoreTuple<T>,
>(
signal: S,
options: PersistenceOptions<T, O> = {} as PersistenceOptions<T, O>,
options: PersistenceOptions<S, O> = {} as PersistenceOptions<S, O>,
): PersistedState<S> {
const storage = options.storage || (globalThis.localStorage as Storage | undefined);
const name = options.name || `storage-${createUniqueId()}`;
const actionFn = options.action && options.action(signal);
if (actionFn) {
(signal as any)[1] = action(actionFn) as unknown as S[1];
}
if (!storage) {
return [signal[0], signal[1], null] as PersistedState<S>;
return [signal[0], signal[1], null] as unknown as PersistedState<S>;
}
const storageOptions = (options as unknown as { storageOptions: O }).storageOptions;
const serialize: (data: T) => string = options.serialize || JSON.stringify.bind(JSON);
Comment thread
atk marked this conversation as resolved.
const deserialize: (data: string) => T = options.deserialize || JSON.parse.bind(JSON);
const init = storage.getItem(name, storageOptions);
const set =
typeof signal[0] === "function"
? (data: string) => {
try {
const value = deserialize(data);
(signal[1] as any)(() => value);
} catch (e) {
// eslint-disable-next-line no-console
if (isDev) console.warn(e);
}
const isSignal = typeof signal[0] === "function";
const set = isSignal
? (data: string) => {
try {
const value = deserialize(data);
(signal[1] as any)(() => value);
} catch (e) {
// eslint-disable-next-line no-console
if (DEV) console.warn(e);
}
}
: (data: string) => {
try {
const value = deserialize(data);
(signal[1] as any)(reconcile(value, () => true));
} catch (e) {
// eslint-disable-next-line no-console
if (DEV) console.warn(e);
}
: (data: string) => {
try {
const value = deserialize(data);
(signal[1] as any)(reconcile(value));
} catch (e) {
// eslint-disable-next-line no-console
if (isDev) console.warn(e);
}
};
};
let unchanged = true;

if (init instanceof Promise) init.then(data => unchanged && data && set(data));
else if (init) set(init);

const getter: () => T = isSignal ? (signal[0] as () => T) : () => snapshot(signal[0] as T);

if (typeof options.sync?.[0] === "function") {
const get: () => T =
typeof signal[0] === "function" ? (signal[0] as () => T) : () => signal[0] as T;
options.sync[0]((data: PersistenceSyncData) => {
if (
data.key !== name ||
(!isServer && (data.url || globalThis.location.href) !== globalThis.location.href) ||
data.newValue === serialize(untrack(get))
data.newValue === serialize(untrack(getter))
) {
return;
}
set(data.newValue as string);
});
}

const persist = () => {
const next = untrack(() => latest(getter));
if (next == null) {
storage.removeItem(name, storageOptions);
options.sync?.[1](name, null);
} else {
const serialized = serialize(next);
storage.setItem(name, serialized, storageOptions);
options.sync?.[1](name, serialized);
}
};
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;
},
signal[0],
(value: any) => untrack(() => {
const output = signal[1](value);
persist();
unchanged = false;
return output instanceof Promise ? output.then((result) => (persist(), result), (err) => { persist(); throw err; }) : output;
}),
init,
] as PersistedState<S>;
] as unknown as PersistedState<S>;
}

/**
Expand All @@ -214,15 +230,15 @@ export const messageSync = (channel: Window | BroadcastChannel = window): Persis
}),
(key, newValue) =>
channel.postMessage(
{ key, newValue, timeStamp: +new Date(), url: location.href },
{ key, newValue, timeStamp: Date.now(), url: location.href },
location.origin,
),
];

/**
* wsSync - syncronize persisted storage via web socket
*/
export const wsSync = (ws: WebSocket, warnOnError: boolean = isDev): PersistenceSyncAPI => [
export const wsSync = (ws: WebSocket, warnOnError: boolean = !!DEV): PersistenceSyncAPI => [
(subscriber: PersistenceSyncCallback) =>
ws.addEventListener("message", (ev: MessageEvent) => {
try {
Expand All @@ -241,7 +257,7 @@ export const wsSync = (ws: WebSocket, warnOnError: boolean = isDev): Persistence
key,
newValue,
timeStamp: +new Date(),
Comment thread
atk marked this conversation as resolved.
...(isServer ? {} : { url: location.href }),
...(globalThis.window ? { url: location.href } : {}),
Comment thread
atk marked this conversation as resolved.
}),
),
];
Expand Down
Loading