Skip to content

Commit 17ea145

Browse files
committed
add createAsyncStorage, prev argument, remove store from cache
1 parent d1c7f40 commit 17ea145

5 files changed

Lines changed: 108 additions & 41 deletions

File tree

.changeset/pink-cooks-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/router": minor
3+
---
4+
5+
add createAsyncStorage, prev argument, remove store from cache

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ You can revalidate the cache using the `revalidate` method or you can set `reval
489489
This is light wrapper over `createResource` that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like `createMemo` and it expects a promise back that it turns into a Signal. Reading it before it is ready causes Suspense/Transitions to trigger.
490490

491491
```jsx
492-
const user = createAsync(() => getUser(params.id))
492+
const user = createAsync((currentValue) => getUser(params.id))
493493
```
494494

495495
Using `cache` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.

src/data/cache.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
type Signal,
88
startTransition
99
} from "solid-js";
10-
import { createStore, reconcile, type ReconcileOptions } from "solid-js/store";
1110
import { getRequestEvent, isServer } from "solid-js/web";
1211
import { useNavigate, getIntent } from "../routing.js";
1312
import { CacheEntry } from "../types.js";
@@ -53,17 +52,15 @@ export function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: Cache
5352
}
5453
}
5554

56-
export type CachedFunction<T extends (...args: any) => U | Response, U> = T & {
57-
keyFor: (...args: Parameters<T>) => string;
55+
export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends A ? (...args: never[]) => R : T) & {
56+
keyFor: (...args: A) => string;
5857
key: string;
59-
};
58+
} : never;
6059

61-
export function cache<T extends (...args: any) => U | Response, U>(
60+
export function cache<T extends (...args: any) => any>(
6261
fn: T,
6362
name: string,
64-
options?: ReconcileOptions
65-
): CachedFunction<T, U> {
66-
const [store, setStore] = createStore<Record<string, any>>({});
63+
): CachedFunction<T> {
6764
// prioritize GET for server functions
6865
if ((fn as any).GET) fn = (fn as any).GET;
6966
const cachedFn = ((...args: Parameters<T>) => {
@@ -111,8 +108,8 @@ export function cache<T extends (...args: any) => U | Response, U>(
111108
let res = cached[1];
112109
if (intent !== "preload") {
113110
res =
114-
"then" in (cached[1] as Promise<U>)
115-
? (cached[1] as Promise<U>).then(handleResponse(false), handleResponse(true))
111+
"then" in (cached[1])
112+
? (cached[1]).then(handleResponse(false), handleResponse(true))
116113
: handleResponse(false)(cached[1]);
117114
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
118115
}
@@ -153,14 +150,14 @@ export function cache<T extends (...args: any) => U | Response, U>(
153150
}
154151
if (intent !== "preload") {
155152
res =
156-
"then" in (res as Promise<U>)
157-
? (res as Promise<U>).then(handleResponse(false), handleResponse(true))
153+
"then" in (res)
154+
? (res).then(handleResponse(false), handleResponse(true))
158155
: handleResponse(false)(res);
159156
}
160157
return res;
161158

162159
function handleResponse(error: boolean) {
163-
return async (v: U | Response) => {
160+
return async (v: any | Response) => {
164161
if (v instanceof Response) {
165162
if (v.headers.has("Location")) {
166163
if (navigate) {
@@ -180,12 +177,10 @@ export function cache<T extends (...args: any) => U | Response, U>(
180177
if ((v as any).customBody) v = await (v as any).customBody();
181178
}
182179
if (error) throw v;
183-
if (isServer) return v;
184-
setStore(key, reconcile(v, options));
185-
return store[key];
180+
return v;
186181
};
187182
}
188-
}) as CachedFunction<T, U>;
183+
}) as CachedFunction<T>;
189184
cachedFn.keyFor = (...args: Parameters<T>) => name + hashKey(args);
190185
cachedFn.key = name;
191186
return cachedFn;

src/data/createAsync.ts

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,98 @@
11
/**
22
* This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
33
*/
4-
import { type Accessor, createResource, sharedConfig } from "solid-js";
4+
import { type Accessor, createResource, sharedConfig, type Setter, untrack } from "solid-js";
5+
import { createStore, reconcile, type ReconcileOptions, unwrap } from "solid-js/store";
56
import { isServer } from "solid-js/web";
6-
export function createAsync<T>(fn: () => Promise<T>, options: {
7-
name?: string;
8-
initialValue: T;
9-
deferStream?: boolean;
10-
}): Accessor<T>
11-
export function createAsync<T>(fn: () => Promise<T>, options?: {
12-
name?: string;
13-
initialValue?: T;
14-
deferStream?: boolean;
15-
}): Accessor<T | undefined>
16-
export function createAsync<T>(fn: () => Promise<T>, options?: {
17-
name?: string;
18-
initialValue?: T;
19-
deferStream?: boolean;
20-
}): Accessor<T | undefined> {
21-
const [resource] = createResource(
22-
() => subFetch(fn),
7+
8+
export function createAsync<T>(
9+
fn: (prev: T) => Promise<T>,
10+
options: {
11+
name?: string;
12+
initialValue: T;
13+
deferStream?: boolean;
14+
}
15+
): Accessor<T>;
16+
export function createAsync<T>(
17+
fn: (prev: T | undefined) => Promise<T>,
18+
options?: {
19+
name?: string;
20+
initialValue?: T;
21+
deferStream?: boolean;
22+
}
23+
): Accessor<T | undefined>;
24+
export function createAsync<T>(
25+
fn: (prev: T | undefined) => Promise<T>,
26+
options?: {
27+
name?: string;
28+
initialValue?: T;
29+
deferStream?: boolean;
30+
}
31+
): Accessor<T | undefined> {
32+
let resource: () => T;
33+
let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest;
34+
[resource] = createResource(
35+
() => subFetch(fn, untrack(prev)),
2336
v => v,
24-
options
37+
options as any
2538
);
2639
return () => resource();
2740
}
2841

42+
export function createAsyncStore<T>(
43+
fn: (prev: T) => Promise<T>,
44+
options: {
45+
name?: string;
46+
initialValue: T;
47+
deferStream?: boolean;
48+
reconcile?: ReconcileOptions;
49+
}
50+
): Accessor<T>;
51+
export function createAsyncStore<T>(
52+
fn: (prev: T | undefined) => Promise<T>,
53+
options?: {
54+
name?: string;
55+
initialValue?: T;
56+
deferStream?: boolean;
57+
reconcile?: ReconcileOptions;
58+
}
59+
): Accessor<T | undefined>;
60+
export function createAsyncStore<T>(
61+
fn: (prev: T | undefined) => Promise<T>,
62+
options: {
63+
name?: string;
64+
initialValue?: T;
65+
deferStream?: boolean;
66+
reconcile?: ReconcileOptions;
67+
} = {}
68+
): Accessor<T | undefined> {
69+
let resource: () => T;
70+
let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : unwrap((resource as any).latest);
71+
[resource] = createResource(
72+
() => subFetch(fn, untrack(prev)),
73+
v => v,
74+
{
75+
...options,
76+
storage: (init: T | undefined) => createDeepSignal(init, options.reconcile)
77+
} as any
78+
);
79+
return () => resource();
80+
}
81+
82+
function createDeepSignal<T>(value: T | undefined, options?: ReconcileOptions) {
83+
const [store, setStore] = createStore({
84+
value
85+
});
86+
return [
87+
() => store.value,
88+
(v: T) => {
89+
typeof v === "function" && (v = v());
90+
setStore("value", reconcile(v, options));
91+
return store.value;
92+
}
93+
] as [Accessor<T | null>, Setter<T | null>];
94+
}
95+
2996
// mock promise while hydrating to prevent fetching
3097
class MockPromise {
3198
static all() {
@@ -57,14 +124,14 @@ class MockPromise {
57124
}
58125
}
59126

60-
function subFetch<T>(fn: () => Promise<T>) {
61-
if (isServer || !sharedConfig.context) return fn();
127+
function subFetch<T>(fn: (prev: T | undefined) => Promise<T>, prev: T | undefined) {
128+
if (isServer || !sharedConfig.context) return fn(prev);
62129
const ogFetch = fetch;
63130
const ogPromise = Promise;
64131
try {
65132
window.fetch = () => new MockPromise() as any;
66133
Promise = MockPromise as any;
67-
return fn();
134+
return fn(prev);
68135
} finally {
69136
window.fetch = ogFetch;
70137
Promise = ogPromise;

src/data/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { createAsync } from "./createAsync.js";
1+
export { createAsync, createAsyncStore } from "./createAsync.js";
22
export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
33
export { cache, revalidate, type CachedFunction } from "./cache.js";
44
export { redirect, reload, json } from "./response.js";

0 commit comments

Comments
 (0)