Skip to content

Commit

Permalink
Merge branch 'main' into virtualized-list
Browse files Browse the repository at this point in the history
  • Loading branch information
thetarnav authored Sep 8, 2024
2 parents 8af4e45 + 1edb03f commit a1dffec
Show file tree
Hide file tree
Showing 22 changed files with 151 additions and 58 deletions.
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

6 changes: 6 additions & 0 deletions packages/marker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @solid-primitives/marker

## 0.1.0

### Minor Changes

- a1f602d: fix memory leak

## 0.0.3

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/marker/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/marker",
"version": "0.0.3",
"version": "0.1.0",
"description": "A reactive primitive for marking matching parts of a string.",
"author": "Damian Tarnawski <[email protected]>",
"contributors": [],
Expand Down
2 changes: 1 addition & 1 deletion packages/marker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function makeSearchRegex(search: string): RegExp {
return search
? new RegExp(
// join words `|` to match any of them
search.replace(SPLIT_WORDS_REGEX, "|"),
search.trim().replace(SPLIT_WORDS_REGEX, "|"),
"gi",
)
: NEVER_REGEX;
Expand Down
6 changes: 6 additions & 0 deletions packages/resource/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @solid-primitives/resource

## 0.3.1

### Patch Changes

- 89117a6: storage: expose init promise/value, resource: docs clarification

## 0.3.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/resource/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const [signal, abort] = makeAbortable({ timeout: 10000 });

const fetcher = (url: string) => fetch(url, { signal: signal() }).then(r => r.json());

// cached fetcher will only be called if `url` source changes, or gets invalidated
// cached fetcher will not be called if something for the same URL is still in cache
const [cachedFetcher, invalidate] = makeCache(fetcher, { storage: localStorage });

// works with createResource, or any wrapping API with the same interface
Expand Down
2 changes: 1 addition & 1 deletion packages/resource/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/resource",
"version": "0.3.0",
"version": "0.3.1",
"description": "A template primitive example.",
"author": "Alex <[email protected]>",
"contributors": [],
Expand Down
18 changes: 18 additions & 0 deletions packages/storage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# @solid-primitives/storage

## 4.2.1

### Patch Changes

- 7e50e35: Fix `makePersisted` type narrowing

## 4.2.0

### Minor Changes

- 89117a6: storage: expose init promise/value, resource: docs clarification

## 4.1.0

### Minor Changes

- fd4e161: fixes utf8 escaping in cookieStorage

## 4.0.0

### Major Changes
Expand Down
23 changes: 21 additions & 2 deletions packages/storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ yarn add @solid-primitives/storage
`makePersisted` allows you to persist a signal or store in any synchronous or asynchronous Storage API:

```ts
const [signal, setSignal] = makePersisted(createSignal("initial"), {storage: sessionStorage});
const [store, setStore] = makePersisted(createStore({test: true}), {name: "testing"});
const [signal, setSignal, init] = makePersisted(createSignal("initial"), {storage: sessionStorage});
const [store, setStore, init] = makePersisted(createStore({test: true}), {name: "testing"});
type PersistedOptions<Type, StorageOptions> = {
// localStorage is default
storage?: Storage | StorageWithOptions | AsyncStorage | AsyncStorageWithOptions | LocalForage,
Expand Down Expand Up @@ -64,6 +64,21 @@ If you are using an asynchronous storage to persist the state of a resource, it
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.

### 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:

```ts
const [state, setState, init] = makePersisted(createStore({}), {
name: "state",
storage: localForage,
});
// run the resource so it is triggered
createResource(() => init)[0]();
```

Now Suspense should be blocked until the initialisation is resolved.

### Different storage APIs

#### LocalStorage, SessionStorage
Expand Down Expand Up @@ -99,6 +114,10 @@ const [state, setState] = makePersisted(createSignal(), {
});
```

> HTTP headers are limited to 32kb, each header itself is limited to 16kb. So depending on your current headers, the space in `cookieStorage` is rather small. If the overall space is exceeded, subsequent requests will fail. We have no mechanism to prevent that, since we cannot infer all headers that the browser will set.
> Browsers do not support most UTF8 and UTF16 characters in Cookies, so `cookieStorage` encodes those characters that are not supported using `encodeURIComponent`. To save space, only those characters not supported by all Browsers will be encoded.
#### LocalForage

LocalForage uses indexedDB or WebSQL if available to greatly increase the size of what can be stored. Just drop it in as a storage (only supported in the client):
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/storage",
"version": "4.0.0",
"version": "4.2.1",
"description": "Primitive that provides reactive wrappers for storage access",
"author": "Alex Lohr <[email protected]>",
"contributors": [
Expand Down
34 changes: 22 additions & 12 deletions packages/storage/src/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getRequestEvent, isServer, type RequestEvent } from "solid-js/web";
import { SyncStorageWithOptions } from "./persisted.js";
import { addClearMethod, addWithOptionsMethod } from "./tools.js";
import { SyncStorageWithOptions } from "./index.js";
import { addWithOptionsMethod, addClearMethod } from "./tools.js";

export type CookieOptions =
| (CookieProperties & {
Expand All @@ -19,7 +19,9 @@ type CookiePropertyTypes = {
sameSite?: "None" | "Lax" | "Strict";
};

type CookieProperties = { [key in keyof CookiePropertyTypes]: CookiePropertyTypes[key] };
type CookieProperties = {
[key in keyof CookiePropertyTypes]: CookiePropertyTypes[key];
};

const cookiePropertyMap = {
domain: "Domain",
Expand All @@ -35,7 +37,7 @@ function serializeCookieOptions(options?: CookieOptions) {
if (!options) return "";
const result = Object.entries(options)
.map(([key, value]) => {
const serializedKey = cookiePropertyMap[key as keyof CookiePropertyTypes];
const serializedKey: string | undefined = cookiePropertyMap[key as keyof CookiePropertyTypes];
if (!serializedKey) return undefined;

if (value instanceof Date) return `${serializedKey}=${value.toUTCString()}`;
Expand All @@ -48,16 +50,17 @@ function serializeCookieOptions(options?: CookieOptions) {
}

function deserializeCookieOptions(cookie: string, key: string) {
return cookie.match(`(^|;)\\s*${key}\\s*=\\s*([^;]+)`)?.pop() ?? null;
const found = cookie.match(`(^|;)\\s*${key}\\s*=\\s*([^;]+)`)?.pop();
return found != null ? decodeURIComponent(found) : null;
}

const getRequestHeaders = isServer
? () => getRequestEvent()?.request?.headers || new Headers()
? () => getRequestEvent()?.request.headers || new Headers()
: () => new Headers();
const getResponseHeaders = isServer
? () =>
(getRequestEvent() as RequestEvent & { response: Response })?.response?.headers ||
new Headers()
(getRequestEvent() as (RequestEvent & { response: Response }) | undefined)?.response
.headers || new Headers()
: () => new Headers();

/**
Expand Down Expand Up @@ -103,9 +106,9 @@ export const cookieStorage: SyncStorageWithOptions<CookieOptions> = addWithOptio
const responseHeaders = getResponseHeaders();
const currentCookies =
responseHeaders
?.get("Set-Cookie")
.get("Set-Cookie")
?.split(", ")
?.filter(cookie => cookie && !cookie.startsWith(`${key}=`)) ?? [];
.filter(cookie => cookie && !cookie.startsWith(`${key}=`)) ?? [];
responseHeaders.set(
"Set-Cookie",
[...currentCookies, `${key}=${value}${serializeCookieOptions(options)}`].join(", "),
Expand All @@ -117,10 +120,17 @@ export const cookieStorage: SyncStorageWithOptions<CookieOptions> = addWithOptio
getItem: (key: string, options?: CookieOptions) =>
deserializeCookieOptions(cookieStorage._read(options), key),
setItem: (key: string, value: string, options?: CookieOptions) => {
cookieStorage._write(key, value, options);
cookieStorage._write(
key,
value.replace(/[\u00c0-\uffff\&;]/g, c => encodeURIComponent(c)),
options,
);
},
removeItem: (key: string, options?: CookieOptions) => {
cookieStorage._write(key, "deleted", { ...options, expires: new Date(0) });
cookieStorage._write(key, "deleted", {
...options,
expires: new Date(0),
});
},
key: (index: number, options?: CookieOptions) => {
let key: string | null = null;
Expand Down
54 changes: 31 additions & 23 deletions packages/storage/src/persisted.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Setter, Signal } from "solid-js";
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";
Expand Down Expand Up @@ -70,9 +70,18 @@ export type PersistenceOptions<T, O extends Record<string, any> | undefined> = {
storageOptions?: O;
});

export type SignalType<S extends Signal<any> | [Store<any>, SetStoreFunction<any>]> =
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;

/**
* Persists a signal, store or similar API
* ```ts
Expand All @@ -91,31 +100,32 @@ export type SignalType<S extends Signal<any> | [Store<any>, SetStoreFunction<any
*
* @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.
* @returns {Signal<T> | [get: Store<T>, set: SetStoreFunction<T>]} - The persisted signal or store.
* @returns {PersistedState<T>} - The persisted signal or store.
*/
export function makePersisted<S extends Signal<any> | [Store<any>, SetStoreFunction<any>]>(
export function makePersisted<S extends SignalInput>(
signal: S,
options?: PersistenceOptions<SignalType<S>, undefined>,
): S;
export function makePersisted<
S extends Signal<any> | [Store<any>, SetStoreFunction<any>],
O extends Record<string, any>,
>(signal: S, options: PersistenceOptions<SignalType<S>, O>): S;
): 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<
S extends Signal<any> | [Store<any>, SetStoreFunction<any>],
S extends SignalInput,
O extends Record<string, any> | undefined,
T = SignalType<S>,
>(signal: S, options: PersistenceOptions<T, O> = {} as PersistenceOptions<T, O>): S {
const storage = options.storage || globalThis.localStorage;
>(
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()}`;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!storage) {
return signal;
return [signal[0], signal[1], null] as PersistedState<S>;
}
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);
// @ts-ignore
const init = storage.getItem(name, storageOptions);
const set =
typeof signal[0] === "function"
Expand All @@ -137,7 +147,7 @@ export function makePersisted<
};
let unchanged = true;

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

if (typeof options.sync?.[0] === "function") {
Expand All @@ -161,24 +171,22 @@ export function makePersisted<
? (value?: T | ((prev: T) => T)) => {
const output = (signal[1] as Setter<T>)(value as any);
const serialized: string | null | undefined =
value != null ? (serialize(output) as string) : (value as null | undefined);
value != null ? serialize(output) : (value as null | undefined);
options.sync?.[1](name, serialized);
// @ts-ignore
if (value != null) storage.setItem(name, serialized as string, storageOptions);
// @ts-ignore
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] as any));
const value = serialize(untrack(() => signal[0]));
options.sync?.[1](name, value);
// @ts-ignore
storage.setItem(name, value, storageOptions);
unchanged = false;
},
] as S;
init,
] as PersistedState<S>;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/storage/tauri-storage/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createSignal, For, untrack } from "solid-js";
import { createStore } from "solid-js/store";
// this would usually be imported from "@solid-primitives/storage":
import { makePersisted, tauriStorage } from "../../src/index.ts";
import { makePersisted } from "../../src/index.js";
import { tauriStorage } from "../../src/tauri.js";

declare global {
interface Window {
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/tauri-storage/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
import { render } from "solid-js/web";

import "./styles.css";
import App from "./App";
import App from "./App.jsx";

render(() => <App />, document.getElementById("root") as HTMLElement);
9 changes: 8 additions & 1 deletion packages/storage/test/cookies.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createRoot } from "solid-js";
import { describe, expect, it, vi } from "vitest";

import { cookieStorage } from "../src/index.js";
Expand Down Expand Up @@ -40,4 +39,12 @@ describe("cookieStorage", () => {
expect(set).toHaveBeenCalledWith("test3=good");
});
});

it("(de)-serializes utf-8 characters correctly", () => {
const set = vi.spyOn(document, "cookie", "set");
const umlaute = "äöüÄÖÜ";
cookieStorage.setItem("test4", umlaute);
expect(set).toHaveBeenCalledWith("test4=" + encodeURIComponent(umlaute));
expect(cookieStorage.getItem("test4")).toBe(umlaute);
});
});
17 changes: 13 additions & 4 deletions packages/storage/test/persisted.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { createSignal } from "solid-js";
import { createStore } from "solid-js/store";
import { makePersisted } from "../src/persisted.js";
import { AsyncStorage } from "../src/types.js";
import { AsyncStorage } from "../src/index.js";

describe("makePersisted", () => {
let data: Record<string, string> = {};
Expand Down Expand Up @@ -127,9 +127,18 @@ describe("makePersisted", () => {
});
expect(signal()).toBe("init");
setSignal("overwritten");
if (resolve) {
resolve("persisted");
}
resolve("persisted");
expect(signal()).toBe("overwritten");
});

it("exposes the initial value as third part of the return tuple", () => {
const anotherMockAsyncStorage = { ...mockAsyncStorage };
const promise = Promise.resolve("init");
anotherMockAsyncStorage.getItem = () => promise;
const [_signal, _setSignal, init] = makePersisted(createSignal("default"), {
storage: anotherMockAsyncStorage,
name: "test8",
});
expect(init).toBe(promise);
});
});
Loading

0 comments on commit a1dffec

Please sign in to comment.