Skip to content

Commit

Permalink
rename cache to query, action onComplete
Browse files Browse the repository at this point in the history
  • Loading branch information
ryansolid committed Oct 21, 2024
1 parent d6c52cf commit 6799556
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-pears-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/router": minor
---

rename `cache` to `query`, action `onComplete`
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,30 +427,30 @@ The return value of the `preload` function is passed to the page component when

Keep in mind these are completely optional. To use but showcase the power of our preload mechanism.

### `cache`
### `query`

To prevent duplicate fetching and to trigger handle refetching we provide a cache api. That takes a function and returns the same function.
To prevent duplicate fetching and to trigger handle refetching we provide a query api. That takes a function and returns the same function.

```jsx
const getUser = cache(async (id) => {
const getUser = query(async (id) => {
return (await fetch(`/api/users${id}`)).json()
}, "users") // used as cache key + serialized arguments
}, "users") // used as the query key + serialized arguments
```
It is expected that the arguments to the cache function are serializable.
It is expected that the arguments to the query function are serializable.

This cache accomplishes the following:
This query accomplishes the following:

1. It does just deduping on the server for the lifetime of the request.
2. It does preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
1. It does deduping on the server for the lifetime of the request.
2. It fills a preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache.
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses this cache. Revalidation or new fetch updates the cache.

Using it with preload function might look like:

```js
import { lazy } from "solid-js";
import { Route } from "@solidjs/router";
import { getUser } from ... // the cache function
import { getUser } from ... // the query function

const User = lazy(() => import("./pages/users/[id].js"));

Expand All @@ -467,7 +467,7 @@ Inside your page component you:

```jsx
// pages/users/[id].js
import { getUser } from ... // the cache function
import { getUser } from ... // the query function

export default function User(props) {
const user = createAsync(() => getUser(props.params.id));
Expand All @@ -483,9 +483,9 @@ getUser.key // returns "users"
getUser.keyFor(id) // returns "users[5]"
```

You can revalidate the cache using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the cache (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
You can revalidate the query using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the query (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.

`cache` can be defined anywhere and then used inside your components with:
`query` can be defined anywhere and then used inside your components with:

### `createAsync`

Expand All @@ -502,7 +502,7 @@ const user = createAsync((currentValue) => getUser(params.id))
return <h1>{user.latest.name}</h1>;
```

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

### `createAsyncStore`

Expand Down Expand Up @@ -597,13 +597,13 @@ const submission = useSubmission(action, (input) => filter(input));

### Response Helpers

These are used to communicate router navigations from cache/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
These are used to communicate router navigations from query/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.

#### `redirect(path, options)`

Redirects to the next route
```js
const getUser = cache(() => {
const getUser = query(() => {
const user = await api.getCurrentUser()
if (!user) throw redirect("/login");
return user;
Expand All @@ -614,7 +614,7 @@ const getUser = cache(() => {

Reloads the data on the current page
```js
const getTodo = cache(async (id: number) => {
const getTodo = query(async (id: number) => {
const todo = await fetchTodo(id);
return todo;
}, "todo")
Expand Down Expand Up @@ -937,7 +937,7 @@ Related without Outlet component it has to be passed in manually. At which point

### `data` functions & `useRouteData`

These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/query APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.

That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:

Expand Down
19 changes: 14 additions & 5 deletions src/data/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isServer } from "solid-js/web";
import { useRouter } from "../routing.js";
import type { RouterContext, Submission, SubmissionStub, Navigator, NarrowResponse } from "../types.js";
import { mockBase } from "../utils.js";
import { cacheKeyOp, hashKey, revalidate, cache } from "./cache.js";
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";

export type Action<T extends Array<any>, U, V = T> = (T extends [FormData] | []
? JSX.SerializableAttributeValue
Expand Down Expand Up @@ -62,7 +62,15 @@ export function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>) {
export function action<T extends Array<any>, U = void>(
fn: (...args: T) => Promise<U>,
name?: string
): Action<T, U, T> {
): Action<T, U>
export function action<T extends Array<any>, U = void>(
fn: (...args: T) => Promise<U>,
options?: { name?: string; onComplete?: (s: Submission<T, U>) => void }
): Action<T, U>
export function action<T extends Array<any>, U = void>(
fn: (...args: T) => Promise<U>,
options: string | { name?: string; onComplete?: (s: Submission<T, U>) => void } = {}
): Action<T, U> {
function mutate(this: { r: RouterContext; f?: HTMLFormElement }, ...variables: T) {
const router = this.r;
const form = this.f;
Expand All @@ -76,6 +84,7 @@ export function action<T extends Array<any>, U = void>(
function handler(error?: boolean) {
return async (res: any) => {
const result = await handleResponse(res, error, router.navigatorFactory());
o.onComplete && o.onComplete(submission);
if (!result) return submission.clear();
setResult(result);
if (result.error && !form) throw result.error;
Expand Down Expand Up @@ -108,10 +117,10 @@ export function action<T extends Array<any>, U = void>(
]);
return p.then(handler(), handler(true));
}

const o = typeof options === "string" ? { name: options } : options;
const url: string =
(fn as any).url ||
(name && `https://action/${name}`) ||
(o.name && `https://action/${o.name}`) ||
(!isServer ? `https://action/${hashString(fn.toString())}` : "");
mutate.base = url;
return toAction(mutate, url);
Expand Down Expand Up @@ -177,7 +186,7 @@ async function handleResponse(response: unknown, error: boolean | undefined, nav
// invalidate
cacheKeyOp(keys, entry => (entry[0] = 0));
// set cache
flightKeys && flightKeys.forEach(k => cache.set(k, custom[k]));
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
// trigger revalidation
await revalidate(keys, false);
return data != null ? { data } : undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js";
export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
export { cache, revalidate, type CachedFunction } from "./cache.js";
export { query, revalidate, cache, type CachedFunction } from "./query.js";
export { redirect, reload, json } from "./response.js";

54 changes: 33 additions & 21 deletions src/data/cache.ts → src/data/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ if (!isServer) {
setInterval(() => {
const now = Date.now();
for (let [k, v] of cacheMap.entries()) {
if (!v[3].count && now - v[0] > CACHE_TIMEOUT) {
if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
cacheMap.delete(k);
}
}
Expand All @@ -40,7 +40,7 @@ export function revalidate(key?: string | string[] | void, force = true) {
const now = Date.now();
cacheKeyOp(key, entry => {
force && (entry[0] = 0); //force cache miss
entry[3][1](now); // retrigger live signals
entry[4][1](now); // retrigger live signals
});
});
}
Expand All @@ -67,7 +67,7 @@ export type CachedFunction<T extends (...args: any) => any> = T extends (
}
: never;

export function cache<T extends (...args: any) => any>(fn: T, name: string): CachedFunction<T> {
export function query<T extends (...args: any) => any>(fn: T, name: string): CachedFunction<T> {
// prioritize GET for server functions
if ((fn as any).GET) fn = (fn as any).GET;
const cachedFn = ((...args: Parameters<T>) => {
Expand Down Expand Up @@ -96,22 +96,22 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
}
if (getListener() && !isServer) {
tracking = true;
onCleanup(() => cached[3].count--);
onCleanup(() => cached[4].count--);
}

if (
cached &&
cached[0] &&
(isServer ||
intent === "native" ||
cached[3].count ||
cached[4].count ||
Date.now() - cached[0] < PRELOAD_TIMEOUT)
) {
if (tracking) {
cached[3].count++;
cached[3][0](); // track
cached[4].count++;
cached[4][0](); // track
}
if (cached[2] === "preload" && intent !== "preload") {
if (cached[3] === "preload" && intent !== "preload") {
cached[0] = now;
}
let res = cached[1];
Expand All @@ -120,7 +120,7 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
"then" in cached[1]
? cached[1].then(handleResponse(false), handleResponse(true))
: handleResponse(false)(cached[1]);
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
}
inPreloadFn && "then" in res && res.catch(() => {});
return res;
Expand All @@ -133,18 +133,18 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
if (cached) {
cached[0] = now;
cached[1] = res;
cached[2] = intent;
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
cached[3] = intent;
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
} else {
cache.set(
key,
(cached = [now, res, intent, createSignal(now) as Signal<number> & { count: number }])
(cached = [now, res, , intent, createSignal(now) as Signal<number> & { count: number }])
);
cached[3].count = 0;
cached[4].count = 0;
}
if (tracking) {
cached[3].count++;
cached[3][0](); // track
cached[4].count++;
cached[4][0](); // track
}
if (isServer) {
const e = getRequestEvent();
Expand Down Expand Up @@ -192,6 +192,7 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
if ((v as any).customBody) v = await (v as any).customBody();
}
if (error) throw v;
cached[2] = v;
return v;
};
}
Expand All @@ -201,24 +202,35 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
return cachedFn;
}

cache.set = (key: string, value: any) => {
query.get = (key: string) => {
const cached = getCache().get(key) as CacheEntry;
return cached[2];
}

query.set = <T>(key: string, value: T extends Promise<any> ? never : T) => {
const cache = getCache();
const now = Date.now();
let cached = cache.get(key);
if (cached) {
cached[0] = now;
cached[1] = value;
cached[2] = "preload";
cached[1] = Promise.resolve(value);
cached[2] = value;
cached[3] = "preload";
} else {
cache.set(
key,
(cached = [now, value, , createSignal(now) as Signal<number> & { count: number }])
(cached = [now, Promise.resolve(value), value, "preload", createSignal(now) as Signal<number> & { count: number }])
);
cached[3].count = 0;
cached[4].count = 0;
}
};

cache.clear = () => getCache().clear();
query.delete = (key: string) => getCache().delete(key);

query.clear = () => getCache().clear();

/** @deprecated use query instead */
export const cache = query;

function matchKey(key: string, keys: string[]) {
for (let k of keys) {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export interface MaybePreloadableComponent extends Component {
preload?: () => void;
}

export type CacheEntry = [number, any, Intent | undefined, Signal<number> & { count: number }];
export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & { count: number }];

export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
export type RouterResponseInit = Omit<ResponseInit, "body"> & { revalidate?: string | string[] };
Expand Down

0 comments on commit 6799556

Please sign in to comment.