diff --git a/.changeset/great-kids-rhyme.md b/.changeset/great-kids-rhyme.md
new file mode 100644
index 000000000..5dd1e4654
--- /dev/null
+++ b/.changeset/great-kids-rhyme.md
@@ -0,0 +1,8 @@
+---
+"@supabase-cache-helpers/postgrest-react-query": patch
+"@supabase-cache-helpers/storage-react-query": patch
+"@supabase-cache-helpers/postgrest-swr": patch
+"@supabase-cache-helpers/storage-swr": patch
+---
+
+update readme to reflect new server-side package
diff --git a/.changeset/soft-trees-crash.md b/.changeset/soft-trees-crash.md
new file mode 100644
index 000000000..e8cb5b6eb
--- /dev/null
+++ b/.changeset/soft-trees-crash.md
@@ -0,0 +1,5 @@
+---
+"@supabase-cache-helpers/postgrest-server": patch
+---
+
+initial release
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 76590a078..57dd3f0c4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,37 +51,50 @@ jobs:
run: pnpm turbo run test --concurrency=1
- name: Upload postgrest-core coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- files: ./packages/postgrest-core/coverage/coverage-final.json
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/postgrest-core/coverage/coverage-final.json
flags: postgrest-core
- name: Upload postgrest-react-query coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- files: ./packages/postgrest-react-query/coverage/coverage-final.json
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/postgrest-react-query/coverage/coverage-final.json
flags: postgrest-react-query
- name: Upload postgrest-swr coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- files: ./packages/postgrest-swr/coverage/coverage-final.json
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/postgrest-swr/coverage/coverage-final.json
flags: postgrest-swr
- name: Upload storage-core coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- files: ./packages/storage-core/coverage/coverage-final.json
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/storage-core/coverage/coverage-final.json
flags: storage-core
- name: Upload storage-swr coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- files: ./packages/storage-swr/coverage/coverage-final.json
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/storage-swr/coverage/coverage-final.json
flags: storage-swr
- name: Upload storage-react-query coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
- files: ./packages/storage-react-query/coverage/coverage-final.json
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/storage-react-query/coverage/coverage-final.json
flags: storage-react-query
+
+ - name: Upload postgrest-server coverage
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./packages/postgrest-server/coverage/coverage-final.json
+ flags: postgrest-server
diff --git a/README.md b/README.md
index 9a734701c..1ecba47b3 100644
--- a/README.md
+++ b/README.md
@@ -10,13 +10,14 @@
## Introduction
-The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
+The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. It also provides a simple server-side abstraction to cache queries to the `PostgREST` API. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
## Features
With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box.
- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest)
+- Support for **Server-Side** queries
- **Automatic** cache key generation
- Easy **Pagination** and **Infinite Scroll** queries
- **Insert**, **update**, **upsert** and **delete** mutations
@@ -42,6 +43,7 @@ The cache helpers are split up into reusable libraries.
- [`storage-swr`](./packages/storage-swr/README.md): [SWR](https://swr.vercel.app) wrapper for storage [storage-js](https://github.com/supabase/storage-js)
- [`postgrest-react-query`](./packages/postgrest-react-query/README.md): [React Query](https://tanstack.com/query/latest) wrapper for [postgrest-js](https://github.com/supabase/postgrest-js)
- [`storage-react-query`](./packages/storage-react-query/README.md): [React Query](https://tanstack.com/query/latest) wrapper for storage [storage-js](https://github.com/supabase/storage-js)
+- [`postgrest-server`](./packages/postgrest-server/README.md): Server-side caching wrapper for [postgrest-js](https://github.com/supabase/postgrest-js).
### Shared Packages
@@ -67,25 +69,5 @@ Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
(we are hiring!)
-
-
-
-
-
- Supabase
-
- https://supabase.com
-
- |
-
-
-
-
-
- Luke Bechtel
-
- @Marviel
-
- |
diff --git a/docs/pages/postgrest/_meta.ts b/docs/pages/postgrest/_meta.ts
index bb0117403..dde329c44 100644
--- a/docs/pages/postgrest/_meta.ts
+++ b/docs/pages/postgrest/_meta.ts
@@ -5,4 +5,5 @@ export default {
subscriptions: 'Subscriptions',
'custom-cache-updates': 'Custom Cache Updates',
ssr: 'Server Side Rendering',
+ server: 'Server Side Caching',
};
diff --git a/docs/pages/postgrest/server.mdx b/docs/pages/postgrest/server.mdx
new file mode 100644
index 000000000..d4ead9875
--- /dev/null
+++ b/docs/pages/postgrest/server.mdx
@@ -0,0 +1,157 @@
+import { Tabs } from 'nextra/components';
+
+# Server-Side Caching
+
+Cache helpers also provides a simple caching abstraction to be used server-side via `@supabase-cache-helpers/postgrest-server`.
+
+## Motivation
+
+At some point, you might want to cache your PostgREST requests on the server-side too. Most users either do not cache at all, or caching might look like this:
+
+
+```ts
+const cache = new Some3rdPartyCache(...)
+
+let contact = await cache.get(contactId) as Tables<"contact"> | undefined | null;
+if (!contact){
+ const { data } = await supabase.from("contact").select("*").eq("id", contactId).throwOnError()
+ contact = data
+ await cache.set(contactId, contact, Date.now() + 60_000)
+}
+
+// use contact
+```
+
+There are a few annoying things about this code:
+
+- Manual type casting
+- No support for stale-while-revalidate
+
+Most people would build a small wrapper around this to make it easier to use and so did we: This library is the result of a rewrite of our own caching layer after some developers were starting to replicate it. It’s used in production by Hellomateo any others.
+
+## Features
+
+- **Typescript**: Fully typesafe
+- **Tiered Cache**: Multiple caches in series to fall back on
+- **Stale-While-Revalidate**: Async loading of data from your origin
+- **Deduping**: Prevents multiple requests for the same data from being made at the same time
+
+## Getting Started
+
+Fist, install the dependency:
+
+
+ `npm install @supabase-cache-helpers/postgrest-server`
+ `pnpm add @supabase-cache-helpers/postgrest-server`
+ `yarn add @supabase-cache-helpers/postgrest-server`
+ `bun install @supabase-cache-helpers/postgrest-server`
+
+
+This is how you can make your first cached query:
+
+```ts
+import { QueryCache } from '@supabase-cache-helpers/postgrest-server';
+import { MemoryStore } from '@supabase-cache-helpers/postgrest-server/stores';
+import { createClient } from '@supabase/supabase-js';
+import { Database } from './types';
+
+const client = createClient(
+ process.env.SUPABASE_URL,
+ process.env.SUPABASE_ANON_KEY
+);
+
+const map = new Map();
+
+const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ // Configure the defaults
+ fresh: 1000,
+ stale: 2000,
+});
+
+const res = await cache.query(
+ client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .single(),
+ // overwrite the default per query
+ { fresh: 100, stale : 200 }
+);
+
+```
+
+### Context
+
+You may wonder what `ctx` is passed above. In serverless functions it’s not always trivial to run some code after you have returned a response. This is where the context comes in. It allows you to register promises that should be awaited before the function is considered done. Fortunately many providers offer a way to do this.
+
+In order to be used in this cache library, the context must implement the following interface:
+
+```ts
+export interface Context {
+ waitUntil: (p: Promise) => void;
+}
+```
+
+For stateful applications, you can use the `DefaultStatefulContext`:
+
+```ts
+import { DefaultStatefulContext } from "@unkey/cache";
+const ctx = new DefaultStatefulContext()
+```
+
+## Tiered Cache
+
+Different caches have different characteristics, some may be fast but volatile, others may be slow but persistent. By using a tiered cache, you can combine the best of both worlds. In almost every case, you want to use a fast in-memory cache as the first tier. There is no reason not to use it, as it doesn’t add any latency to your application.
+
+The goal of this implementation is that it’s invisible to the user. Everything behaves like a single cache. You can add as many tiers as you want.
+
+### Example
+
+```ts
+import { QueryCache } from '@supabase-cache-helpers/postgrest-server';
+import { MemoryStore, RedisStore } from '@supabase-cache-helpers/postgrest-server/stores';
+import { Redis } from 'ioredis';
+import { createClient } from '@supabase/supabase-js';
+import { Database } from './types';
+
+const client = createClient(
+ process.env.SUPABASE_URL,
+ process.env.SUPABASE_ANON_KEY
+);
+
+const map = new Map();
+
+const redis = new Redis({...});
+
+const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map }), new RedisStore({ redis })],
+ fresh: 1000,
+ stale: 2000
+});
+
+const res = await cache.query(
+ client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .single()
+);
+
+```
+
+
+## Stale-While-Revalidate
+
+To make data fetching as easy as possible, the cache offers a swr method, that acts as a pull through cache. If the data is fresh, it will be returned from the cache, if it’s stale it will be returned from the cache and a background refresh will be triggered and if it’s not in the cache, the data will be synchronously fetched from the origin.
+
+```ts
+const res = await cache.swr(
+ client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .single()
+);
+```
+
diff --git a/packages/postgrest-react-query/README.md b/packages/postgrest-react-query/README.md
index 800b3e94a..acd411394 100644
--- a/packages/postgrest-react-query/README.md
+++ b/packages/postgrest-react-query/README.md
@@ -5,16 +5,16 @@ A collection of React Query utilities for working with Supabase.
[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers)
-
## Introduction
-The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-react-query.vercel.app/) and find out how it feels like for your users.
+The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. It also provides a simple server-side abstraction to cache queries to the `PostgREST` API. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
## Features
With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box.
- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest)
+- Support for **Server-Side** queries
- **Automatic** cache key generation
- Easy **Pagination** and **Infinite Scroll** queries
- **Insert**, **update**, **upsert** and **delete** mutations
@@ -27,3 +27,4 @@ And a lot [more](https://supabase-cache-helpers.vercel.app).
---
**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).**
+
diff --git a/packages/postgrest-server/README.md b/packages/postgrest-server/README.md
new file mode 100644
index 000000000..dfde33da9
--- /dev/null
+++ b/packages/postgrest-server/README.md
@@ -0,0 +1,34 @@
+# PostgREST Server Cache
+
+A collection of server-side caching utilities for working with Supabase.
+
+
+
+[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers)
+
+## Introduction
+
+The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. It also provides a simple server-side abstraction to cache queries to the `PostgREST` API. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
+
+## Features
+
+With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box.
+
+- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest)
+- Support for **Server-Side** queries
+- **Automatic** cache key generation
+- Easy **Pagination** and **Infinite Scroll** queries
+- **Insert**, **update**, **upsert** and **delete** mutations
+- **Auto-populate** cache after mutations and subscriptions
+- **Auto-expand** mutation queries based on existing cache data to keep app up-to-date
+- One-liner to upload, download and remove **Supabase Storage** objects
+
+And a lot [more](https://supabase-cache-helpers.vercel.app).
+
+---
+
+**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).**
+
+## Acknowledgement
+
+The hard part of this package has been extracted from `@unkey/cache`. If you are on the lookout for a generic typesafe caching solution be sure to check it out.
diff --git a/packages/postgrest-server/package.json b/packages/postgrest-server/package.json
new file mode 100644
index 000000000..6dd7cfca9
--- /dev/null
+++ b/packages/postgrest-server/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@supabase-cache-helpers/postgrest-server",
+ "version": "0.0.0",
+ "author": "Philipp Steinrötter ",
+ "homepage": "https://supabase-cache-helpers.vercel.app",
+ "bugs": {
+ "url": "https://github.com/psteinroe/supabase-cache-helpers/issues"
+ },
+ "type": "module",
+ "main": "./dist/index.js",
+ "source": "./src/index.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "./stores": {
+ "types": "./dist/stores.d.ts",
+ "import": "./dist/stores.js",
+ "require": "./dist/stores.cjs"
+ },
+ "./package.json": "./package.json"
+ },
+ "types": "./dist/index.d.ts",
+ "files": ["dist/**"],
+ "publishConfig": {
+ "access": "public"
+ },
+ "license": "MIT",
+ "scripts": {
+ "build": "tsup",
+ "test": "vitest --coverage --no-file-parallelism --dangerouslyIgnoreUnhandledErrors",
+ "clean": "rm -rf .turbo && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist",
+ "typecheck": "tsc --pretty --noEmit"
+ },
+ "keywords": ["Supabase", "PostgREST", "Cache", "SWR"],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git",
+ "directory": "packages/postgrest-server"
+ },
+ "peerDependencies": {
+ "@supabase/postgrest-js": "^1.9.0"
+ },
+ "devDependencies": {
+ "@supabase-cache-helpers/tsconfig": "workspace:*",
+ "@supabase/postgrest-js": "1.16.1",
+ "@supabase/supabase-js": "2.45.4",
+ "@vitest/coverage-istanbul": "^2.0.2",
+ "ioredis": "5.4.1",
+ "dotenv": "16.4.0",
+ "tsup": "8.2.0",
+ "typescript": "5.5.3",
+ "vitest": "2.0.2"
+ },
+ "dependencies": {
+ "@supabase-cache-helpers/postgrest-core": "workspace:*"
+ }
+}
diff --git a/packages/postgrest-server/src/context.ts b/packages/postgrest-server/src/context.ts
new file mode 100644
index 000000000..40fce295d
--- /dev/null
+++ b/packages/postgrest-server/src/context.ts
@@ -0,0 +1,9 @@
+export interface Context {
+ waitUntil: (p: Promise) => void;
+}
+
+export class DefaultStatefulContext implements Context {
+ public waitUntil(_p: Promise) {
+ // do nothing, the promise will resolve on its own
+ }
+}
diff --git a/packages/postgrest-server/src/index.ts b/packages/postgrest-server/src/index.ts
new file mode 100644
index 000000000..4777d0833
--- /dev/null
+++ b/packages/postgrest-server/src/index.ts
@@ -0,0 +1,2 @@
+export * from './context';
+export * from './query-cache';
diff --git a/packages/postgrest-server/src/key.ts b/packages/postgrest-server/src/key.ts
new file mode 100644
index 000000000..e6644cfdc
--- /dev/null
+++ b/packages/postgrest-server/src/key.ts
@@ -0,0 +1,26 @@
+import {
+ AnyPostgrestResponse,
+ PostgrestParser,
+ isPostgrestBuilder,
+} from '@supabase-cache-helpers/postgrest-core';
+
+const SEPARATOR = '$';
+
+export function encode(
+ query: PromiseLike>,
+) {
+ if (!isPostgrestBuilder(query)) {
+ throw new Error('Query is not a PostgrestBuilder');
+ }
+
+ const parser = new PostgrestParser(query);
+ return [
+ parser.schema,
+ parser.table,
+ parser.queryKey,
+ parser.bodyKey ?? 'null',
+ `count=${parser.count}`,
+ `head=${parser.isHead}`,
+ parser.orderByKey,
+ ].join(SEPARATOR);
+}
diff --git a/packages/postgrest-server/src/query-cache.ts b/packages/postgrest-server/src/query-cache.ts
new file mode 100644
index 000000000..4d40f4003
--- /dev/null
+++ b/packages/postgrest-server/src/query-cache.ts
@@ -0,0 +1,136 @@
+import { type AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core';
+import type {
+ PostgrestMaybeSingleResponse,
+ PostgrestResponse,
+ PostgrestSingleResponse,
+} from '@supabase/postgrest-js';
+
+import { Context } from './context';
+import { encode } from './key';
+import { Value } from './stores/entry';
+import { Store } from './stores/interface';
+import { SwrCache } from './swr-cache';
+import { TieredStore } from './tiered-store';
+
+export type QueryCacheOpts = {
+ stores: Store[];
+ fresh: number;
+ stale: number;
+};
+
+export type OperationOpts = Pick;
+
+export class QueryCache {
+ private readonly inner: SwrCache;
+
+ /**
+ * To prevent concurrent requests of the same data, all queries are deduplicated using
+ * this map.
+ */
+ private readonly runningQueries: Map<
+ string,
+ PromiseLike>
+ > = new Map();
+
+ constructor(ctx: Context, opts: QueryCacheOpts) {
+ const tieredStore = new TieredStore(ctx, opts.stores);
+
+ this.inner = new SwrCache({
+ ctx,
+ store: tieredStore,
+ fresh: opts.fresh,
+ stale: opts.stale,
+ });
+ }
+
+ /**
+ * Perform a cached postgrest query
+ */
+ query(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise>;
+ /**
+ * Perform a cached postgrest query
+ */
+ query(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise>;
+ /**
+ * Perform a cached postgrest query
+ */
+ query(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise>;
+ async query(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise> {
+ const key = encode(query);
+
+ const value = await this.inner.get(key);
+
+ if (value) return value;
+
+ const result = await this.dedupeQuery(query);
+
+ await this.inner.set(key, result, opts);
+
+ return result;
+ }
+
+ /**
+ * Perform a cached postgrest query
+ */
+ swr(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise>;
+ /**
+ * Perform a cached postgrest query
+ */
+ swr(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise>;
+ /**
+ * Perform a cached postgrest query
+ */
+ swr(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise>;
+ async swr(
+ query: PromiseLike>,
+ opts?: OperationOpts,
+ ): Promise> {
+ return await this.inner.swr(
+ encode(query),
+ () => this.dedupeQuery(query),
+ opts,
+ );
+ }
+
+ /**
+ * Deduplicating the origin load helps when the same value is requested many times at once and is
+ * not yet in the cache. If we don't deduplicate, we'd create a lot of unnecessary load on the db.
+ */
+ private async dedupeQuery(
+ query: PromiseLike>,
+ ): Promise> {
+ const key = encode(query);
+ try {
+ const querying = this.runningQueries.get(key);
+ if (querying) {
+ return querying;
+ }
+
+ this.runningQueries.set(key, query);
+ return await query;
+ } finally {
+ this.runningQueries.delete(key);
+ }
+ }
+}
diff --git a/packages/postgrest-server/src/stores/entry.ts b/packages/postgrest-server/src/stores/entry.ts
new file mode 100644
index 000000000..789886cc4
--- /dev/null
+++ b/packages/postgrest-server/src/stores/entry.ts
@@ -0,0 +1,20 @@
+import { type AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core';
+
+export type Value = AnyPostgrestResponse;
+
+export type Entry = {
+ value: Value;
+
+ // Before this time the entry is considered fresh and valid
+ // UnixMilli
+ freshUntil: number;
+
+ /**
+ * Unix timestamp in milliseconds.
+ *
+ * Do not use data after this point as it is considered no longer valid.
+ *
+ * You can use this field to configure automatic eviction in your store implementation. *
+ */
+ staleUntil: number;
+};
diff --git a/packages/postgrest-server/src/stores/index.ts b/packages/postgrest-server/src/stores/index.ts
new file mode 100644
index 000000000..f9ff0902c
--- /dev/null
+++ b/packages/postgrest-server/src/stores/index.ts
@@ -0,0 +1,4 @@
+export * from './interface';
+export * from './entry';
+export * from './memory';
+export * from './redis';
diff --git a/packages/postgrest-server/src/stores/interface.ts b/packages/postgrest-server/src/stores/interface.ts
new file mode 100644
index 000000000..e1234d03f
--- /dev/null
+++ b/packages/postgrest-server/src/stores/interface.ts
@@ -0,0 +1,35 @@
+import { Entry } from './entry';
+
+/**
+ * A store is a common interface for storing, reading and deleting key-value pairs.
+ *
+ * The store implementation is responsible for cleaning up expired data on its own.
+ */
+export interface Store {
+ /**
+ * A name for metrics/tracing.
+ *
+ * @example: memory | zone
+ */
+ name: string;
+
+ /**
+ * Return the cached value
+ *
+ * The response must be `undefined` for cache misses
+ */
+ get(key: string): Promise | undefined>;
+
+ /**
+ * Sets the value for the given key.
+ *
+ * You are responsible for evicting expired values in your store implementation.
+ * Use the `entry.staleUntil` (unix milli timestamp) field to configure expiration
+ */
+ set(key: string, value: Entry): Promise;
+
+ /**
+ * Removes the key from the store.
+ */
+ remove(key: string | string[]): Promise;
+}
diff --git a/packages/postgrest-server/src/stores/memory.ts b/packages/postgrest-server/src/stores/memory.ts
new file mode 100644
index 000000000..ac380e4eb
--- /dev/null
+++ b/packages/postgrest-server/src/stores/memory.ts
@@ -0,0 +1,47 @@
+import type { Entry } from './entry';
+import type { Store } from './interface';
+
+export type MemoryStoreConfig = {
+ persistentMap: Map;
+};
+
+export class MemoryStore implements Store {
+ private readonly state: Map }>;
+
+ public readonly name = 'memory';
+
+ constructor(
+ config: MemoryStoreConfig<{ expires: number; entry: Entry }>,
+ ) {
+ this.state = config.persistentMap;
+ }
+
+ public async get(key: string): Promise | undefined> {
+ const value = this.state.get(key);
+ if (!value) {
+ return Promise.resolve(undefined);
+ }
+ if (value.expires <= Date.now()) {
+ await this.remove(key);
+ }
+ return Promise.resolve(value.entry);
+ }
+
+ public async set(key: string, entry: Entry): Promise {
+ this.state.set(key, {
+ expires: entry.staleUntil,
+ entry,
+ });
+
+ return Promise.resolve();
+ }
+
+ public async remove(keys: string | string[]): Promise {
+ const cacheKeys = Array.isArray(keys) ? keys : [keys];
+
+ for (const key of cacheKeys) {
+ this.state.delete(key);
+ }
+ return Promise.resolve();
+ }
+}
diff --git a/packages/postgrest-server/src/stores/redis.ts b/packages/postgrest-server/src/stores/redis.ts
new file mode 100644
index 000000000..3b321e347
--- /dev/null
+++ b/packages/postgrest-server/src/stores/redis.ts
@@ -0,0 +1,47 @@
+import type { Redis } from 'ioredis';
+
+import type { Entry } from './entry';
+import type { Store } from './interface';
+
+export type RedisStoreConfig = {
+ redis: Redis;
+ prefix?: string;
+};
+
+export class RedisStore implements Store {
+ private readonly redis: Redis;
+ public readonly name = 'redis';
+ private readonly prefix: string;
+
+ constructor(config: RedisStoreConfig) {
+ this.redis = config.redis;
+ this.prefix = config.prefix || 'sbch';
+ }
+
+ private buildCacheKey(key: string): string {
+ return [this.prefix, key].join('::');
+ }
+
+ public async get(key: string): Promise | undefined> {
+ const res = await this.redis.get(this.buildCacheKey(key));
+ if (!res) return;
+
+ return JSON.parse(res) as Entry;
+ }
+
+ public async set(key: string, entry: Entry): Promise {
+ await this.redis.set(
+ this.buildCacheKey(key),
+ JSON.stringify(entry),
+ 'PXAT',
+ entry.staleUntil,
+ );
+ }
+
+ public async remove(keys: string | string[]): Promise {
+ const cacheKeys = (Array.isArray(keys) ? keys : [keys]).map((key) =>
+ this.buildCacheKey(key).toString(),
+ );
+ this.redis.del(...cacheKeys);
+ }
+}
diff --git a/packages/postgrest-server/src/swr-cache.ts b/packages/postgrest-server/src/swr-cache.ts
new file mode 100644
index 000000000..4ad82911b
--- /dev/null
+++ b/packages/postgrest-server/src/swr-cache.ts
@@ -0,0 +1,111 @@
+import type { Context } from './context';
+import { Value } from './stores/entry';
+import { Store } from './stores/interface';
+
+export type SwrCacheOpts = {
+ ctx: Context;
+ store: Store;
+ fresh: number;
+ stale: number;
+};
+
+/**
+ * Internal cache implementation for an individual namespace
+ */
+export class SwrCache {
+ private readonly ctx: Context;
+ private readonly store: Store;
+ private readonly fresh: number;
+ private readonly stale: number;
+
+ constructor({ ctx, store, fresh, stale }: SwrCacheOpts) {
+ this.ctx = ctx;
+ this.store = store;
+ this.fresh = fresh;
+ this.stale = stale;
+ }
+
+ /**
+ * Return the cached value
+ *
+ * The response will be `undefined` for cache misses or `null` when the key was not found in the origin
+ */
+ public async get(key: string): Promise | undefined> {
+ const res = await this._get(key);
+ return res.value;
+ }
+
+ private async _get(
+ key: string,
+ ): Promise<{ value: Value | undefined; revalidate?: boolean }> {
+ const res = await this.store.get(key);
+
+ const now = Date.now();
+ if (!res) {
+ return { value: undefined };
+ }
+
+ if (now >= res.staleUntil) {
+ this.ctx.waitUntil(this.remove(key));
+ return { value: undefined };
+ }
+ if (now >= res.freshUntil) {
+ return { value: res.value, revalidate: true };
+ }
+
+ return { value: res.value };
+ }
+
+ /**
+ * Set the value
+ */
+ public async set(
+ key: string,
+ value: Value,
+ opts?: {
+ fresh: number;
+ stale: number;
+ },
+ ): Promise {
+ const now = Date.now();
+ return this.store.set(key, {
+ value,
+ freshUntil: now + (opts?.fresh ?? this.fresh),
+ staleUntil: now + (opts?.stale ?? this.stale),
+ });
+ }
+
+ /**
+ * Removes the key from the cache.
+ */
+ public async remove(key: string): Promise {
+ return this.store.remove(key);
+ }
+
+ public async swr(
+ key: string,
+ loadFromOrigin: (key: string) => Promise>,
+ opts?: {
+ fresh: number;
+ stale: number;
+ },
+ ): Promise> {
+ const res = await this._get(key);
+
+ const { value, revalidate } = res;
+
+ if (typeof value !== 'undefined') {
+ if (revalidate) {
+ this.ctx.waitUntil(
+ loadFromOrigin(key).then((res) => this.set(key, res, opts)),
+ );
+ }
+
+ return value;
+ }
+
+ const loadedValue = await loadFromOrigin(key);
+ this.ctx.waitUntil(this.set(key, loadedValue));
+ return loadedValue;
+ }
+}
diff --git a/packages/postgrest-server/src/tiered-store.ts b/packages/postgrest-server/src/tiered-store.ts
new file mode 100644
index 000000000..9a0875f6e
--- /dev/null
+++ b/packages/postgrest-server/src/tiered-store.ts
@@ -0,0 +1,74 @@
+import type { Context } from './context';
+import { Entry } from './stores/entry';
+import { Store } from './stores/interface';
+
+/**
+ * TieredCache is a cache that will first check the memory cache, then the zone cache.
+ */
+export class TieredStore implements Store {
+ private ctx: Context;
+ private readonly tiers: Store[];
+ public readonly name = 'tiered';
+
+ /**
+ * Create a new tiered store
+ * Stored are checked in the order they are provided
+ * The first store to return a value will be used to populate all previous stores
+ *
+ *
+ * `stores` can accept `undefined` as members to allow you to construct the tiers dynamically
+ * @example
+ * ```ts
+ * new TieredStore(ctx, [
+ * new MemoryStore(..),
+ * process.env.ENABLE_X_STORE ? new XStore(..) : undefined
+ * ])
+ * ```
+ */
+ constructor(ctx: Context, stores: (Store | undefined)[]) {
+ this.ctx = ctx;
+ this.tiers = stores.filter(Boolean) as Store[];
+ }
+
+ /**
+ * Return the cached value
+ *
+ * The response will be `undefined` for cache misses or `null` when the key was not found in the origin
+ */
+ public async get(key: string): Promise | undefined> {
+ if (this.tiers.length === 0) {
+ return;
+ }
+
+ for (let i = 0; i < this.tiers.length; i++) {
+ const res = await this.tiers[i].get(key);
+
+ if (!res) {
+ return;
+ }
+
+ // Fill all lower caches
+ this.ctx.waitUntil(
+ Promise.all(
+ this.tiers.filter((_, j) => j < i).map((t) => () => t.set(key, res)),
+ ),
+ );
+
+ return res;
+ }
+ }
+
+ /**
+ * Sets the value for the given key.
+ */
+ public async set(key: string, value: Entry): Promise {
+ await Promise.all(this.tiers.map((t) => t.set(key, value)));
+ }
+
+ /**
+ * Removes the key from the cache.
+ */
+ public async remove(key: string): Promise {
+ await Promise.all(this.tiers.map((t) => t.remove(key)));
+ }
+}
diff --git a/packages/postgrest-server/tests/database.types.ts b/packages/postgrest-server/tests/database.types.ts
new file mode 100644
index 000000000..0331d5d7b
--- /dev/null
+++ b/packages/postgrest-server/tests/database.types.ts
@@ -0,0 +1,376 @@
+export type Json =
+ | string
+ | number
+ | boolean
+ | null
+ | { [key: string]: Json | undefined }
+ | Json[];
+
+export interface Database {
+ public: {
+ Tables: {
+ contact: {
+ Row: {
+ age_range: unknown | null;
+ catchphrase: unknown | null;
+ continent: string | null;
+ country: string | null;
+ created_at: string;
+ golden_ticket: boolean | null;
+ id: string;
+ metadata: Json | null;
+ tags: string[] | null;
+ ticket_number: number | null;
+ username: string | null;
+ has_low_ticket_number: unknown | null;
+ };
+ Insert: {
+ age_range?: unknown | null;
+ catchphrase?: unknown | null;
+ continent?: string | null;
+ country?: string | null;
+ created_at?: string;
+ golden_ticket?: boolean | null;
+ id?: string;
+ metadata?: Json | null;
+ tags?: string[] | null;
+ ticket_number?: number | null;
+ username?: string | null;
+ };
+ Update: {
+ age_range?: unknown | null;
+ catchphrase?: unknown | null;
+ continent?: string | null;
+ country?: string | null;
+ created_at?: string;
+ golden_ticket?: boolean | null;
+ id?: string;
+ metadata?: Json | null;
+ tags?: string[] | null;
+ ticket_number?: number | null;
+ username?: string | null;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'contact_continent_fkey';
+ columns: ['continent'];
+ referencedRelation: 'continent';
+ referencedColumns: ['code'];
+ },
+ {
+ foreignKeyName: 'contact_country_fkey';
+ columns: ['country'];
+ referencedRelation: 'country';
+ referencedColumns: ['code'];
+ },
+ ];
+ };
+ contact_note: {
+ Row: {
+ contact_id: string;
+ created_at: string;
+ id: string;
+ text: string;
+ };
+ Insert: {
+ contact_id: string;
+ created_at?: string;
+ id?: string;
+ text: string;
+ };
+ Update: {
+ contact_id?: string;
+ created_at?: string;
+ id?: string;
+ text?: string;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'contact_note_contact_id_fkey';
+ columns: ['contact_id'];
+ referencedRelation: 'contact';
+ referencedColumns: ['id'];
+ },
+ ];
+ };
+ continent: {
+ Row: {
+ code: string;
+ name: string | null;
+ };
+ Insert: {
+ code: string;
+ name?: string | null;
+ };
+ Update: {
+ code?: string;
+ name?: string | null;
+ };
+ Relationships: [];
+ };
+ country: {
+ Row: {
+ code: string;
+ continent_code: string;
+ full_name: string;
+ iso3: string;
+ name: string;
+ number: string;
+ };
+ Insert: {
+ code: string;
+ continent_code: string;
+ full_name: string;
+ iso3: string;
+ name: string;
+ number: string;
+ };
+ Update: {
+ code?: string;
+ continent_code?: string;
+ full_name?: string;
+ iso3?: string;
+ name?: string;
+ number?: string;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'country_continent_code_fkey';
+ columns: ['continent_code'];
+ referencedRelation: 'continent';
+ referencedColumns: ['code'];
+ },
+ ];
+ };
+ multi_pk: {
+ Row: {
+ id_1: number;
+ id_2: number;
+ name: string | null;
+ };
+ Insert: {
+ id_1: number;
+ id_2: number;
+ name?: string | null;
+ };
+ Update: {
+ id_1?: number;
+ id_2?: number;
+ name?: string | null;
+ };
+ Relationships: [];
+ };
+ serial_key_table: {
+ Row: {
+ id: number;
+ value: string | null;
+ };
+ Insert: {
+ id?: number;
+ value?: string | null;
+ };
+ Update: {
+ id?: number;
+ value?: string | null;
+ };
+ Relationships: [];
+ };
+ };
+ Views: {
+ [_ in never]: never;
+ };
+ Functions: {
+ has_low_ticket_number: {
+ Args: {
+ '': unknown;
+ };
+ Returns: boolean;
+ };
+ };
+ Enums: {
+ [_ in never]: never;
+ };
+ CompositeTypes: {
+ [_ in never]: never;
+ };
+ };
+ storage: {
+ Tables: {
+ buckets: {
+ Row: {
+ allowed_mime_types: string[] | null;
+ avif_autodetection: boolean | null;
+ created_at: string | null;
+ file_size_limit: number | null;
+ id: string;
+ name: string;
+ owner: string | null;
+ public: boolean | null;
+ updated_at: string | null;
+ };
+ Insert: {
+ allowed_mime_types?: string[] | null;
+ avif_autodetection?: boolean | null;
+ created_at?: string | null;
+ file_size_limit?: number | null;
+ id: string;
+ name: string;
+ owner?: string | null;
+ public?: boolean | null;
+ updated_at?: string | null;
+ };
+ Update: {
+ allowed_mime_types?: string[] | null;
+ avif_autodetection?: boolean | null;
+ created_at?: string | null;
+ file_size_limit?: number | null;
+ id?: string;
+ name?: string;
+ owner?: string | null;
+ public?: boolean | null;
+ updated_at?: string | null;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'buckets_owner_fkey';
+ columns: ['owner'];
+ referencedRelation: 'users';
+ referencedColumns: ['id'];
+ },
+ ];
+ };
+ migrations: {
+ Row: {
+ executed_at: string | null;
+ hash: string;
+ id: number;
+ name: string;
+ };
+ Insert: {
+ executed_at?: string | null;
+ hash: string;
+ id: number;
+ name: string;
+ };
+ Update: {
+ executed_at?: string | null;
+ hash?: string;
+ id?: number;
+ name?: string;
+ };
+ Relationships: [];
+ };
+ objects: {
+ Row: {
+ bucket_id: string | null;
+ created_at: string | null;
+ id: string;
+ last_accessed_at: string | null;
+ metadata: Json | null;
+ name: string | null;
+ owner: string | null;
+ path_tokens: string[] | null;
+ updated_at: string | null;
+ version: string | null;
+ };
+ Insert: {
+ bucket_id?: string | null;
+ created_at?: string | null;
+ id?: string;
+ last_accessed_at?: string | null;
+ metadata?: Json | null;
+ name?: string | null;
+ owner?: string | null;
+ path_tokens?: string[] | null;
+ updated_at?: string | null;
+ version?: string | null;
+ };
+ Update: {
+ bucket_id?: string | null;
+ created_at?: string | null;
+ id?: string;
+ last_accessed_at?: string | null;
+ metadata?: Json | null;
+ name?: string | null;
+ owner?: string | null;
+ path_tokens?: string[] | null;
+ updated_at?: string | null;
+ version?: string | null;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'objects_bucketId_fkey';
+ columns: ['bucket_id'];
+ referencedRelation: 'buckets';
+ referencedColumns: ['id'];
+ },
+ ];
+ };
+ };
+ Views: {
+ [_ in never]: never;
+ };
+ Functions: {
+ can_insert_object: {
+ Args: {
+ bucketid: string;
+ name: string;
+ owner: string;
+ metadata: Json;
+ };
+ Returns: undefined;
+ };
+ extension: {
+ Args: {
+ name: string;
+ };
+ Returns: string;
+ };
+ filename: {
+ Args: {
+ name: string;
+ };
+ Returns: string;
+ };
+ foldername: {
+ Args: {
+ name: string;
+ };
+ Returns: unknown;
+ };
+ get_size_by_bucket: {
+ Args: Record;
+ Returns: {
+ size: number;
+ bucket_id: string;
+ }[];
+ };
+ search: {
+ Args: {
+ prefix: string;
+ bucketname: string;
+ limits?: number;
+ levels?: number;
+ offsets?: number;
+ search?: string;
+ sortcolumn?: string;
+ sortorder?: string;
+ };
+ Returns: {
+ name: string;
+ id: string;
+ updated_at: string;
+ created_at: string;
+ last_accessed_at: string;
+ metadata: Json;
+ }[];
+ };
+ };
+ Enums: {
+ [_ in never]: never;
+ };
+ CompositeTypes: {
+ [_ in never]: never;
+ };
+ };
+}
diff --git a/packages/postgrest-server/tests/query-cache.test.ts b/packages/postgrest-server/tests/query-cache.test.ts
new file mode 100644
index 000000000..fd6a81b01
--- /dev/null
+++ b/packages/postgrest-server/tests/query-cache.test.ts
@@ -0,0 +1,227 @@
+import { type SupabaseClient, createClient } from '@supabase/supabase-js';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { DefaultStatefulContext, QueryCache } from '../src';
+import type { Database } from './database.types';
+import './utils';
+import { MemoryStore } from '../src/stores';
+
+const TEST_PREFIX = 'postgrest-server-query';
+
+const ctx = new DefaultStatefulContext();
+
+describe('QueryCache', () => {
+ let client: SupabaseClient;
+ let provider: Map;
+ let testRunPrefix: string;
+ let contacts: Database['public']['Tables']['contact']['Row'][];
+
+ beforeAll(async () => {
+ testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 1000)}`;
+ client = createClient(
+ process.env.SUPABASE_URL as string,
+ process.env.SUPABASE_ANON_KEY as string,
+ );
+ await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`);
+
+ const { data } = await client
+ .from('contact')
+ .insert([
+ { username: `${testRunPrefix}-username-1` },
+ { username: `${testRunPrefix}-username-2` },
+ { username: `${testRunPrefix}-username-3` },
+ { username: `${testRunPrefix}-username-4` },
+ ])
+ .select('*')
+ .throwOnError();
+ contacts = data ?? [];
+ expect(contacts).toHaveLength(4);
+ });
+ beforeEach(() => {
+ provider = new Map();
+ });
+
+ describe('.query()', () => {
+ it('should work for single', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .single();
+
+ const spy = vi.spyOn(query, 'then');
+
+ const res = await cache.query(query);
+
+ const res2 = await cache.query(query);
+
+ expect(res.data?.username).toEqual(contacts[0].username);
+ expect(res2.data?.username).toEqual(contacts[0].username);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should work for maybeSingle', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .maybeSingle();
+
+ const spy = vi.spyOn(query, 'then');
+
+ const res = await cache.query(query);
+
+ const res2 = await cache.query(query);
+
+ expect(res.data?.username).toEqual(contacts[0].username);
+ expect(res2.data?.username).toEqual(contacts[0].username);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should work for multiple', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username', { count: 'exact' })
+ .ilike('username', `${testRunPrefix}%`);
+
+ const spy = vi.spyOn(query, 'then');
+
+ const res = await cache.query(query);
+
+ const res2 = await cache.query(query);
+
+ expect(res.count).toEqual(4);
+ expect(res2.count).toEqual(4);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('.swr()', () => {
+ it('should work for single', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .single();
+
+ const spy = vi.spyOn(query, 'then');
+
+ const res = await cache.swr(query);
+
+ const res2 = await cache.swr(query);
+
+ expect(res.data?.username).toEqual(contacts[0].username);
+ expect(res2.data?.username).toEqual(contacts[0].username);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should work for maybeSingle', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username')
+ .eq('username', contacts[0].username!)
+ .maybeSingle();
+
+ const spy = vi.spyOn(query, 'then');
+
+ const res = await cache.swr(query);
+
+ const res2 = await cache.swr(query);
+
+ expect(res.data?.username).toEqual(contacts[0].username);
+ expect(res2.data?.username).toEqual(contacts[0].username);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should work for multiple', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username', { count: 'exact' })
+ .ilike('username', `${testRunPrefix}%`);
+
+ const spy = vi.spyOn(query, 'then');
+
+ const res = await cache.swr(query);
+
+ const res2 = await cache.swr(query);
+
+ expect(res.count).toEqual(4);
+ expect(res2.count).toEqual(4);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('should dedupe', async () => {
+ const map = new Map();
+
+ const cache = new QueryCache(ctx, {
+ stores: [new MemoryStore({ persistentMap: map })],
+ fresh: 1000,
+ stale: 2000,
+ });
+
+ const query = client
+ .from('contact')
+ .select('id,username', { count: 'exact' })
+ .ilike('username', `${testRunPrefix}%`);
+
+ const spy = vi.spyOn(query, 'then');
+
+ const p1 = cache.query(query);
+ const p2 = cache.query(query);
+
+ await Promise.all([p1, p2]);
+
+ // TOOD figure out how we can test this from the outside
+ // i confirmed that this works via manual testing with logs
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/postgrest-server/tests/stores/memory.test.ts b/packages/postgrest-server/tests/stores/memory.test.ts
new file mode 100644
index 000000000..069a89ee2
--- /dev/null
+++ b/packages/postgrest-server/tests/stores/memory.test.ts
@@ -0,0 +1,43 @@
+import { beforeEach, describe, expect, test } from 'vitest';
+import { MemoryStore, Value } from '../../src/stores';
+
+const createCacheValue = (value: string): Value => ({
+ data: value,
+ error: null,
+ count: 0,
+ status: 200,
+ statusText: 'OK',
+});
+
+describe('MemoryStore', () => {
+ let memoryStore: MemoryStore;
+
+ beforeEach(() => {
+ memoryStore = new MemoryStore({ persistentMap: new Map() });
+ });
+
+ test('should store value in the cache', async () => {
+ const key = 'key';
+ const entry = {
+ value: createCacheValue('name'),
+ freshUntil: Date.now() + 1000000,
+ staleUntil: Date.now() + 100000000,
+ };
+ await memoryStore.set(key, entry);
+ expect(await memoryStore.get(key)).toEqual(entry);
+ });
+
+ test('should return undefined if key does not exist in cache', async () => {
+ expect(await memoryStore.get('doesnotexist')).toEqual(undefined);
+ });
+
+ test('should remove value from cache', async () => {
+ memoryStore.set('key', {
+ value: createCacheValue('name'),
+ freshUntil: Date.now() + 10000000,
+ staleUntil: Date.now() + 12312412512515,
+ });
+ memoryStore.remove('key');
+ expect(await memoryStore.get('key')).toEqual(undefined);
+ });
+});
diff --git a/packages/postgrest-server/tests/swr-cache.test.ts b/packages/postgrest-server/tests/swr-cache.test.ts
new file mode 100644
index 000000000..6289626ef
--- /dev/null
+++ b/packages/postgrest-server/tests/swr-cache.test.ts
@@ -0,0 +1,118 @@
+import { randomUUID } from 'node:crypto';
+import { beforeEach, describe, expect, test } from 'vitest';
+import { DefaultStatefulContext } from '../src/context';
+import { MemoryStore, Value } from '../src/stores';
+import { SwrCache } from '../src/swr-cache';
+
+const fresh = 1000;
+const stale = 2000;
+const key = 'key';
+const value = randomUUID();
+
+let cache: SwrCache;
+
+const createCacheValue = (value: string): Value => ({
+ data: value,
+ error: null,
+ count: 0,
+ status: 200,
+ statusText: 'OK',
+});
+
+beforeEach(() => {
+ const memoryStore = new MemoryStore({ persistentMap: new Map() });
+ cache = new SwrCache({
+ ctx: new DefaultStatefulContext(),
+ store: memoryStore,
+ fresh,
+ stale,
+ });
+});
+
+test('should store value in the cache', async () => {
+ await cache.set(key, createCacheValue(value));
+ expect((await cache.get(key))?.data).toEqual(value);
+});
+
+test('should return undefined if key does not exist in cache', async () => {
+ expect(await cache.get('doesnotexist')).toEqual(undefined);
+});
+
+test('should remove value from cache', async () => {
+ await cache.set(key, createCacheValue(value));
+ await cache.remove(key);
+ expect(await cache.get(key)).toEqual(undefined);
+});
+
+test('evicts outdated data', async () => {
+ await cache.set(key, createCacheValue(value));
+ await new Promise((r) => setTimeout(r, 3000));
+ const res = await cache.get(key);
+ expect(res).toEqual(undefined);
+});
+
+test('returns stale data', async () => {
+ await cache.set(key, createCacheValue(value));
+ await new Promise((r) => setTimeout(r, 1500));
+ const res = await cache.get(key);
+ expect(res?.data).toEqual(value);
+});
+
+describe('with fresh data', () => {
+ test('does not fetch from origin', async () => {
+ await cache.set(key, createCacheValue(value));
+ await new Promise((r) => setTimeout(r, 500));
+
+ let fetchedFromOrigin = false;
+ const stale = await cache.swr(key, () => {
+ fetchedFromOrigin = true;
+ return Promise.resolve(createCacheValue('fresh_data'));
+ });
+ expect(stale?.data).toEqual(value);
+
+ await new Promise((r) => setTimeout(r, 500));
+ const res = await cache.get(key);
+ expect(res?.data).toEqual(value);
+ expect(fetchedFromOrigin).toBe(false);
+ });
+});
+
+describe('with stale data', () => {
+ test('fetches from origin', async () => {
+ await cache.set(key, createCacheValue(value));
+ await new Promise((r) => setTimeout(r, 1500));
+ const stale = await cache.swr(key, () =>
+ Promise.resolve(createCacheValue('fresh_data')),
+ );
+ expect(stale?.data).toEqual(value);
+
+ await new Promise((r) => setTimeout(r, 1500));
+ const res = await cache.get(key);
+ expect(res).toEqual(createCacheValue('fresh_data'));
+ });
+});
+
+describe('with fresh=0', () => {
+ test('revalidates every time', async () => {
+ const memoryStore = new MemoryStore({ persistentMap: new Map() });
+ const cache = new SwrCache({
+ ctx: new DefaultStatefulContext(),
+ store: memoryStore,
+ fresh: 0,
+ stale: 800000,
+ });
+
+ let revalidated = 0;
+ for (let i = 0; i < 100; i++) {
+ const res = await cache.swr(key, async () => {
+ revalidated++;
+ return createCacheValue(i.toString());
+ });
+ if (i > 1) {
+ expect(Number(res?.data)).toEqual(i - 1);
+ }
+ }
+
+ expect(revalidated).toBe(100);
+ });
+});
diff --git a/packages/postgrest-server/tests/utils.ts b/packages/postgrest-server/tests/utils.ts
new file mode 100644
index 000000000..78d993a14
--- /dev/null
+++ b/packages/postgrest-server/tests/utils.ts
@@ -0,0 +1,4 @@
+import { resolve } from 'node:path';
+import * as dotenv from 'dotenv';
+
+dotenv.config({ path: resolve(__dirname, '../../../.env.local') });
diff --git a/packages/postgrest-server/tsconfig.json b/packages/postgrest-server/tsconfig.json
new file mode 100644
index 000000000..59bf36db1
--- /dev/null
+++ b/packages/postgrest-server/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "@supabase-cache-helpers/tsconfig/node.json",
+ "include": ["**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/postgrest-server/tsup.config.ts b/packages/postgrest-server/tsup.config.ts
new file mode 100644
index 000000000..a6f3e1c8d
--- /dev/null
+++ b/packages/postgrest-server/tsup.config.ts
@@ -0,0 +1,18 @@
+import type { Options } from 'tsup';
+
+export const tsup: Options = {
+ dts: true,
+ entry: {
+ index: 'src/index.ts',
+ stores: 'src/stores/index.ts',
+ },
+ external: [/^@supabase\//],
+ format: ['cjs', 'esm'],
+ // inject: ['src/react-shim.js'],
+ // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default!
+ legacyOutput: false,
+ sourcemap: true,
+ splitting: false,
+ bundle: true,
+ clean: true,
+};
diff --git a/packages/postgrest-server/vitest.config.ts b/packages/postgrest-server/vitest.config.ts
new file mode 100644
index 000000000..1c60dfe2a
--- /dev/null
+++ b/packages/postgrest-server/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ typecheck: { enabled: true },
+ coverage: {
+ provider: 'istanbul',
+ },
+ },
+});
diff --git a/packages/postgrest-swr/README.md b/packages/postgrest-swr/README.md
index 9a17c9372..b44ad1d3e 100644
--- a/packages/postgrest-swr/README.md
+++ b/packages/postgrest-swr/README.md
@@ -5,16 +5,16 @@ A collection of SWR utilities for working with Supabase.
[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers)
-
## Introduction
-The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app/) and find out how it feels like for your users.
+The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. It also provides a simple server-side abstraction to cache queries to the `PostgREST` API. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
## Features
With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box.
- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest)
+- Support for **Server-Side** queries
- **Automatic** cache key generation
- Easy **Pagination** and **Infinite Scroll** queries
- **Insert**, **update**, **upsert** and **delete** mutations
@@ -27,3 +27,5 @@ And a lot [more](https://supabase-cache-helpers.vercel.app).
---
**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).**
+
+
diff --git a/packages/storage-react-query/README.md b/packages/storage-react-query/README.md
index 6aee97531..ffa6c6489 100644
--- a/packages/storage-react-query/README.md
+++ b/packages/storage-react-query/README.md
@@ -5,16 +5,16 @@ A collection of React Query utilities for working with Supabase.
[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers)
-
## Introduction
-The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-react-query.vercel.app/) and find out how it feels like for your users.
+The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. It also provides a simple server-side abstraction to cache queries to the `PostgREST` API. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
## Features
With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box.
- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest)
+- Support for **Server-Side** queries
- **Automatic** cache key generation
- Easy **Pagination** and **Infinite Scroll** queries
- **Insert**, **update**, **upsert** and **delete** mutations
@@ -27,3 +27,6 @@ And a lot [more](https://supabase-cache-helpers.vercel.app).
---
**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).**
+
+
+
diff --git a/packages/storage-swr/README.md b/packages/storage-swr/README.md
index 67f4839ba..93aa60856 100644
--- a/packages/storage-swr/README.md
+++ b/packages/storage-swr/README.md
@@ -5,16 +5,16 @@ A collection of SWR utilities for working with Supabase.
[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers)
-
## Introduction
-The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app/) and find out how it feels like for your users.
+The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. It also provides a simple server-side abstraction to cache queries to the `PostgREST` API. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-swr.vercel.app) and find out how it feels like for your users.
## Features
With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box.
- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest)
+- Support for **Server-Side** queries
- **Automatic** cache key generation
- Easy **Pagination** and **Infinite Scroll** queries
- **Insert**, **update**, **upsert** and **delete** mutations
@@ -27,3 +27,7 @@ And a lot [more](https://supabase-cache-helpers.vercel.app).
---
**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).**
+
+
+
+
diff --git a/packages/tsconfig/node.json b/packages/tsconfig/node.json
new file mode 100644
index 000000000..5509c105e
--- /dev/null
+++ b/packages/tsconfig/node.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Node",
+ "extends": "./base.json",
+ "compilerOptions": {
+ "lib": ["ESNext"],
+ "module": "ESNext",
+ "target": "ESNext",
+ "strict": true,
+ "esModuleInterop": false,
+ "forceConsistentCasingInFileNames": true,
+ "allowSyntheticDefaultImports": true
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e81b07177..9992a87ad 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,7 +31,7 @@ importers:
version: 14.2.0(react-dom@18.3.1)(react@18.3.1)
nextra:
specifier: 3.2.0
- version: 3.2.0(@types/react@18.3.3)(acorn@8.10.0)(next@14.2.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)
+ version: 3.2.0(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)
nextra-theme-docs:
specifier: 3.2.0
version: 3.2.0(next@14.2.0)(nextra@3.2.0)(react-dom@18.3.1)(react@18.3.1)
@@ -530,6 +530,40 @@ importers:
specifier: 2.0.2
version: 2.0.2(happy-dom@15.0.0)
+ packages/postgrest-server:
+ dependencies:
+ '@supabase-cache-helpers/postgrest-core':
+ specifier: workspace:*
+ version: link:../postgrest-core
+ devDependencies:
+ '@supabase-cache-helpers/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+ '@supabase/postgrest-js':
+ specifier: 1.16.1
+ version: 1.16.1
+ '@supabase/supabase-js':
+ specifier: 2.45.4
+ version: 2.45.4
+ '@vitest/coverage-istanbul':
+ specifier: ^2.0.2
+ version: 2.0.2(vitest@2.0.2)
+ dotenv:
+ specifier: 16.4.0
+ version: 16.4.0
+ ioredis:
+ specifier: 5.4.1
+ version: 5.4.1
+ tsup:
+ specifier: 8.2.0
+ version: 8.2.0(typescript@5.5.3)
+ typescript:
+ specifier: 5.5.3
+ version: 5.5.3
+ vitest:
+ specifier: 2.0.2
+ version: 2.0.2(happy-dom@15.0.0)
+
packages/postgrest-swr:
dependencies:
'@supabase-cache-helpers/postgrest-core':
@@ -776,7 +810,7 @@ packages:
'@babel/traverse': 7.24.8
'@babel/types': 7.24.8
convert-source-map: 2.0.0
- debug: 4.3.5
+ debug: 4.3.7
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -974,7 +1008,7 @@ packages:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.24.8
'@babel/types': 7.24.8
- debug: 4.3.5
+ debug: 4.3.7
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -1826,6 +1860,10 @@ packages:
- supports-color
dev: false
+ /@ioredis/commands@1.2.0:
+ resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
+ dev: true
+
/@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1925,7 +1963,7 @@ packages:
read-yaml-file: 1.1.0
dev: true
- /@mdx-js/mdx@3.1.0(acorn@8.10.0):
+ /@mdx-js/mdx@3.1.0(acorn@8.14.0):
resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==}
dependencies:
'@types/estree': 1.0.5
@@ -1940,7 +1978,7 @@ packages:
hast-util-to-jsx-runtime: 2.3.2
markdown-extensions: 2.0.0
recma-build-jsx: 1.0.0
- recma-jsx: 1.0.0(acorn@8.10.0)
+ recma-jsx: 1.0.0(acorn@8.14.0)
recma-stringify: 1.0.0
rehype-recma: 1.0.0
remark-mdx: 3.1.0
@@ -3983,7 +4021,7 @@ packages:
peerDependencies:
typescript: '*'
dependencies:
- debug: 4.3.5
+ debug: 4.3.7
typescript: 5.6.3
transitivePeerDependencies:
- supports-color
@@ -4090,6 +4128,14 @@ packages:
acorn: 8.10.0
dev: false
+ /acorn-jsx@5.3.2(acorn@8.14.0):
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ acorn: 8.14.0
+ dev: false
+
/acorn@8.10.0:
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
engines: {node: '>=0.4.0'}
@@ -4106,7 +4152,7 @@ packages:
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
engines: {node: '>= 14'}
dependencies:
- debug: 4.3.5
+ debug: 4.3.7
transitivePeerDependencies:
- supports-color
dev: true
@@ -4606,6 +4652,11 @@ packages:
engines: {node: '>=6'}
dev: false
+ /cluster-key-slot@1.1.2:
+ resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/cmd-shim@6.0.2:
resolution: {integrity: sha512-+FFYbB0YLaAkhkcrjkyNLYDiOsFSfRjwjY19LXk/psmMx1z00xlCv7hhQoTGXXIKi+YXHL/iiFo8NqMVQX9nOw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -5091,6 +5142,7 @@ packages:
optional: true
dependencies:
ms: 2.1.2
+ dev: true
/debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
@@ -5102,7 +5154,6 @@ packages:
optional: true
dependencies:
ms: 2.1.3
- dev: false
/decamelize-keys@1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
@@ -5192,6 +5243,11 @@ packages:
robust-predicates: 3.0.2
dev: false
+ /denque@2.1.0:
+ resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+ engines: {node: '>=0.10'}
+ dev: true
+
/dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -6173,7 +6229,7 @@ packages:
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
- debug: 4.3.5
+ debug: 4.3.7
transitivePeerDependencies:
- supports-color
dev: true
@@ -6270,6 +6326,23 @@ packages:
loose-envify: 1.4.0
dev: false
+ /ioredis@5.4.1:
+ resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
+ engines: {node: '>=12.22.0'}
+ dependencies:
+ '@ioredis/commands': 1.2.0
+ cluster-key-slot: 1.1.2
+ debug: 4.3.7
+ denque: 2.1.0
+ lodash.defaults: 4.2.0
+ lodash.isarguments: 3.1.0
+ redis-errors: 1.2.0
+ redis-parser: 3.0.0
+ standard-as-callback: 2.1.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
dev: false
@@ -6700,6 +6773,14 @@ packages:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
+ /lodash.defaults@4.2.0:
+ resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
+ dev: true
+
+ /lodash.isarguments@3.1.0:
+ resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
+ dev: true
+
/lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
dev: true
@@ -7437,7 +7518,7 @@ packages:
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
dependencies:
'@types/debug': 4.1.7
- debug: 4.3.5
+ debug: 4.3.7
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.1
@@ -7552,10 +7633,10 @@ packages:
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+ dev: true
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- dev: false
/mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -7707,14 +7788,14 @@ packages:
flexsearch: 0.7.43
next: 14.2.0(react-dom@18.3.1)(react@18.3.1)
next-themes: 0.3.0(react-dom@18.3.1)(react@18.3.1)
- nextra: 3.2.0(@types/react@18.3.3)(acorn@8.10.0)(next@14.2.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)
+ nextra: 3.2.0(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
scroll-into-view-if-needed: 3.1.0
zod: 3.22.4
dev: false
- /nextra@3.2.0(@types/react@18.3.3)(acorn@8.10.0)(next@14.2.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3):
+ /nextra@3.2.0(@types/react@18.3.3)(acorn@8.14.0)(next@14.2.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3):
resolution: {integrity: sha512-Gi+Q6BI9rFmQdy3e4FXCqgaSUcv8CGRVOVWbgY6/GTAWKMKK4v5M1gxYNhxdOywSkIoXrxSeXbIaj0qgNdjv3A==}
engines: {node: '>=18'}
peerDependencies:
@@ -7724,7 +7805,7 @@ packages:
dependencies:
'@formatjs/intl-localematcher': 0.5.7
'@headlessui/react': 2.2.0(react-dom@18.3.1)(react@18.3.1)
- '@mdx-js/mdx': 3.1.0(acorn@8.10.0)
+ '@mdx-js/mdx': 3.1.0(acorn@8.14.0)
'@mdx-js/react': 3.1.0(@types/react@18.3.3)(react@18.3.1)
'@napi-rs/simple-git': 0.1.9
'@shikijs/twoslash': 1.22.2(typescript@5.6.3)
@@ -8503,10 +8584,10 @@ packages:
vfile: 6.0.1
dev: false
- /recma-jsx@1.0.0(acorn@8.10.0):
+ /recma-jsx@1.0.0(acorn@8.14.0):
resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==}
dependencies:
- acorn-jsx: 5.3.2(acorn@8.10.0)
+ acorn-jsx: 5.3.2(acorn@8.14.0)
estree-util-to-js: 2.0.0
recma-parse: 1.0.0
recma-stringify: 1.0.0
@@ -8541,6 +8622,18 @@ packages:
strip-indent: 3.0.0
dev: true
+ /redis-errors@1.2.0:
+ resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /redis-parser@3.0.0:
+ resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
+ engines: {node: '>=4'}
+ dependencies:
+ redis-errors: 1.2.0
+ dev: true
+
/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: false
@@ -9090,6 +9183,10 @@ packages:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
dev: true
+ /standard-as-callback@2.1.0:
+ resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
+ dev: true
+
/std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
dev: true
@@ -9598,7 +9695,7 @@ packages:
cac: 6.7.14
chokidar: 3.6.0
consola: 3.2.3
- debug: 4.3.5
+ debug: 4.3.7
esbuild: 0.23.0
execa: 5.1.1
globby: 11.1.0
@@ -10027,7 +10124,7 @@ packages:
hasBin: true
dependencies:
cac: 6.7.14
- debug: 4.3.5
+ debug: 4.3.7
pathe: 1.1.2
tinyrainbow: 1.2.0
vite: 5.3.3
@@ -10110,7 +10207,7 @@ packages:
'@vitest/spy': 2.0.2
'@vitest/utils': 2.0.2
chai: 5.1.1
- debug: 4.3.5
+ debug: 4.3.7
execa: 8.0.1
happy-dom: 15.0.0
magic-string: 0.30.10