diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..e82e6dbd --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - ./ +onlyBuiltDependencies: + - esbuild diff --git a/src/data/index.ts b/src/data/index.ts index c6209164..ad665aca 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,5 +1,5 @@ export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js"; export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js"; -export { query, revalidate, cache, type CachedFunction } from "./query.js"; +export { query, revalidate, cache, type CachedFunction, batchedQuery } from "./query.js"; export { redirect, reload, json } from "./response.js"; diff --git a/src/data/query.ts b/src/data/query.ts index 67ff9efd..d35a50c4 100644 --- a/src/data/query.ts +++ b/src/data/query.ts @@ -4,11 +4,11 @@ import { getOwner, onCleanup, sharedConfig, - type Signal, - startTransition + startTransition, + type Signal } from "solid-js"; import { getRequestEvent, isServer } from "solid-js/web"; -import { useNavigate, getIntent, getInPreloadFn } from "../routing.js"; +import { getInPreloadFn, getIntent, useNavigate } from "../routing.js"; import type { CacheEntry, NarrowResponse } from "../types.js"; const LocationHeader = "Location"; @@ -130,7 +130,7 @@ export function query any>(fn: T, name: string): Cac } let res; if (!isServer && sharedConfig.has && sharedConfig.has(key)) { - res = sharedConfig.load!(key) // hydrating + res = sharedConfig.load!(key); // hydrating // @ts-ignore at least until we add a delete method to sharedConfig delete globalThis._$HY.r[key]; } else res = fn(...(args as any)); @@ -178,16 +178,14 @@ export function query any>(fn: T, name: string): Cac return async (v: any | Response) => { if (v instanceof Response) { const e = getRequestEvent(); - + if (e) { - for (const [ key, value ] of v.headers) { - if (key == "set-cookie") - e.response.headers.append("set-cookie", value); - else - e.response.headers.set(key, value); + for (const [key, value] of v.headers) { + if (key == "set-cookie") e.response.headers.append("set-cookie", value); + else e.response.headers.set(key, value); } } - + const url = v.headers.get(LocationHeader); if (url !== null) { @@ -218,7 +216,7 @@ export function query any>(fn: T, name: string): Cac query.get = (key: string) => { const cached = getCache().get(key) as CacheEntry; return cached[2]; -} +}; query.set = (key: string, value: T extends Promise ? never : T) => { const cache = getCache(); @@ -232,7 +230,13 @@ query.set = (key: string, value: T extends Promise ? never : T) => { } else { cache.set( key, - (cached = [now, Promise.resolve(value), value, "preload", createSignal(now) as Signal & { count: number }]) + (cached = [ + now, + Promise.resolve(value), + value, + "preload", + createSignal(now) as Signal & { count: number } + ]) ); cached[4].count = 0; } @@ -275,3 +279,47 @@ function isPlainObject(obj: object) { (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype) ); } + +function throttle(callback: (value: T) => Promise): (value: T) => Promise { + let timeout: ReturnType | undefined; + let current: Promise | undefined; + let resolve: (value: R) => void; + let reject: (value: unknown) => void; + + return value => { + if (!timeout) { + timeout = setTimeout(() => { + callback(value).then(resolve, reject); + current = undefined; + timeout = undefined; + }); + } + if (!current) { + current = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + } + return current; + }; +} + +export function batchedQuery( + callback: (queries: Query[]) => Promise, + lookup: (data: Data, query: Query, index: number) => Return +): (query: Query) => Promise { + const debounced = throttle(async (queries: Query[]) => { + const currentQueries = [...queries]; + queries.length = 0; + const result = await callback(currentQueries); + return result; + }); + + const pendingQueries: Query[] = []; + + return async (query: Query) => { + const targetIndex = pendingQueries.push(query) - 1; + const data = await debounced(pendingQueries); + return lookup(data, query, targetIndex); + }; +}