Skip to content
Snippets Groups Projects
Commit 91d55998 authored by Brian Perry's avatar Brian Perry
Browse files

Default cache

parent 5fa162b3
No related branches found
No related tags found
2 merge requests!1101.1.0 release,!102Default cache
Pipeline #145786 passed
Showing
with 242 additions and 35 deletions
---
"@drupal-api-client/utils": minor
---
Create `@drupal-api-client/utils` for the defaultCache and other helpful utilities when using the api-client
......@@ -35,6 +35,10 @@ The `api-client` package includes the base `ApiClient` class which is meant to b
The `json-api-client` package includes the `JsonApiClient` class which extends the `ApiClient`, and makes it easy to fetch data from Drupal's JSON:API without deep knowledge of it.
#### utils
The `utils` package includes optional utilities that may be useful for working with the base class, but are not included in the base package.
### Examples
Examples show how the packages can be used in a variety of ways.
......
......
......@@ -89,6 +89,10 @@ export default defineConfig({
label: "Using Authentication",
link: "/jsonapi-tutorial/authentication/",
},
{
label: "Optimizing Performance",
link: "/jsonapi-tutorial/performance/",
},
],
},
{
......
......
......@@ -19,6 +19,7 @@
"@drupal-api-client/api-client": "workspace:*",
"@drupal-api-client/decoupled-router-client": "workspace:*",
"@drupal-api-client/json-api-client": "workspace:*",
"@drupal-api-client/utils": "workspace:*",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"astro": "^4.3.5",
......
......
---
title: Optimizing Performance
---
There are many approaches to optimize performance when interacting with Drupal's JSON:API endpoints. This guide will cover some of the most common strategies.
## Caching data
When creating an instance of the client, it is possible to provide a cache to store data returned from JSON:API. This can have a big impact on performance, especially in cases when the same data is requested multiple times.
### Using the default cache
For a simple way to cache responses, the API Client provides a default cache based on the [Nanostores](https://github.com/nanostores/nanostores) library. To use it, create an instance of the cache using the `createCache` function and pass it to the client when creating an instance.
```astro live
---
import { JsonApiClient, createCache } from "@drupal-api-client/json-api-client";
const client = new JsonApiClient(
"https://dev-drupal-api-client-poc.pantheonsite.io", {
cache: createCache(),
}
);
const recipes = await client.getCollection("node--recipe");
---
<h2>Umami Recipes</h2>
<ul>
{recipes.data.map((recipe) => (
<li key={recipe.id}>{recipe.attributes.title}</li>
))}
</ul>
```
The first call to `getCollection` will fetch the data from the server and store it in the cache. Subsequent calls to `getCollection` for `node--recipe` will return the data from the cache, which can significantly reduce the number of requests made to the server.
### Using a custom cache
In addition to the default cache, an alternative cache can be used. The provided cache needs to be compatible with the ApiClient `cache` interface. This interface expects the cache to have `get` and `set` methods.
As an example, let's look at how we could use the [node-cache](https://www.npmjs.com/package/node-cache) package.
```astro
---
import { JsonApiClient } from "@drupal-api-client/json-api-client";
import NodeCache from "node-cache";
const client = new JsonApiClient(
"https://dev-drupal-api-client-poc.pantheonsite.io", {
cache: new NodeCache(),
}
);
const recipes = await client.getCollection("node--recipe");
---
<h2>Umami Recipes</h2>
<ul>
{recipes.data.map((recipe) => (
<li key={recipe.id}>{recipe.attributes.title}</li>
))}
</ul>
```
......@@ -16,6 +16,7 @@
"dependencies": {
"@drupal-api-client/api-client": "workspace:*",
"@drupal-api-client/json-api-client": "workspace:*",
"@drupal-api-client/utils": "workspace:*",
"jsona": "^1.11.0",
"tslog": "^4.9.2"
}
......
......
import { Cache } from "@drupal-api-client/api-client";
import {
JsonApiClient,
RawApiResponseWithData,
} from "@drupal-api-client/json-api-client";
import { createCache } from "@drupal-api-client/utils";
import { Jsona } from "jsona";
import * as JSONAPI from "jsonapi-typescript";
import { Logger } from "tslog";
......@@ -42,24 +42,11 @@ type Recipe = {
};
};
// use sessionStorage for the cache.
const cache = {
get: async <T>(key: string, _ttl: number) => {
// ^define arbitrary arguments as needed
console.log(`Checking cache for ${key}...`);
// parse the JSON here so when we stringify it later it is not double stringified
return JSON.parse(sessionStorage.getItem(key) as string) as T;
},
set: async <T>(key: string, value: T) => {
console.log(`Setting ${key} in cache...`);
sessionStorage.setItem(key, JSON.stringify(value));
return;
},
} satisfies Cache;
// Example of using a custom logging library, in this case tslog.
const customLogger = new Logger({ name: "JsonApiClient" });
const cache = createCache();
async function main() {
const jsonApiClient = new JsonApiClient(baseUrl, {
customFetch,
......
......
......@@ -50,6 +50,7 @@
"tsconfig": "./tsconfig.typedoc.json"
},
"dependencies": {
"nanostores": "^0.10.2",
"uint8array-extras": "^0.5.0"
}
}
......@@ -11,8 +11,7 @@ npm i @drupal-api-client/json-api-client
## Usage
```ts
import JsonApiClient from "@drupal-api-client/json-api-client";
import NodeCache from "node-cache";
import { JsonApiClient, createCache } from "@drupal-api-client/json-api-client";
import Jsona from "jsona";
// the baseUrl to fetch data from
......@@ -35,13 +34,9 @@ const client = new JsonApiClient(myDrupalUrl, {
},
// the optional cache will cache a request and return the cached data if the request
// is made again with the same type same data.
// The default cache includes an interface that must be implemented.
// Here is an example using the node-cache package.
// See https://www.npmjs.com/package/node-cache for details on the node-cache package.
cache: {
get: async <T,>(key: string) => nodeCache.get(key) as T,
set: async (key: string, value: unknown) => nodeCache.set(key, value),
},
// The cache must implement the `Cache` interface.
// The `createCache` method provides a default cache that satisfies this interface.
cache: createCache(),
// the optional authentication object will be used to authenticate requests.
// Currently Basic auth is supported.
authentication: {
......
......
......@@ -47,6 +47,7 @@
"dependencies": {
"@aws-crypto/sha256-js": "^5.1.0",
"@drupal-api-client/api-client": "workspace:^1.0.0",
"@drupal-api-client/utils": "workspace:*",
"@drupal-api-client/decoupled-router-client": "workspace:*",
"@smithy/util-hex-encoding": "^2.0.0"
},
......
......
import { createCache } from "@drupal-api-client/utils";
import { JsonApiClient } from "./JsonApiClient";
export * from "./types";
export { JsonApiClient };
export { JsonApiClient, createCache };
import type { SpyInstance } from "vitest";
import { JsonApiClient } from "../src";
import { JsonApiClient, createCache } from "../src";
import notFound from "./mocks/data/404.json";
import index from "./mocks/data/index.json";
......@@ -18,12 +18,11 @@ interface CacheTestContext {
describe("Cache", () => {
beforeEach<CacheTestContext>((context) => {
const baseUrl = "https://dev-drupal-api-client-poc.pantheonsite.io";
const store = new Map();
const cache = {
get: async (key: string) => store.get(key),
set: async <T>(key: string, value: T) => store.set(key, value),
};
context.client = new JsonApiClient(baseUrl, { cache, debug: true });
const cache = createCache();
context.client = new JsonApiClient(baseUrl, {
cache,
debug: true,
});
context.indexClient = new JsonApiClient(baseUrl, {
cache,
debug: true,
......
......
# Utilities and other tools
This package contains optional utilities that may be useful for working with the Drupal API Client, but are not included in the base package.
## Installation
```shell
npm install @drupal-api-client/utils
```
## Usage
### createCache
```typescript
import { createCache } from "@drupal-api-client/utils";
const cache = createCache();
```
A cache based on [Nanostores](https://github.com/nanostores/nanostores) that satisfies the `@drupal-api-client/api-client` cache interface.
{
"name": "@drupal-api-client/utils",
"version": "0.1.0",
"license": "GPL-2.0-or-later",
"description": "",
"scripts": {
"arethetypeswrong": "pnpm attw --pack .",
"check:types": "pnpm arethetypeswrong && pnpm publint",
"build": "tsup src/index.ts",
"test": "vitest run --coverage",
"prettier": "prettier --check . --ignore-path ../../.prettierignore",
"prettier:fix": "prettier --check . --ignore-path ../../.prettierignore",
"eslint": "eslint --ext .ts src --ignore-path ../../.gitignore",
"eslint:fix": "eslint --ext .ts src --ignore-path ../../.gitignore --fix"
},
"keywords": [],
"author": "",
"files": [
"dist/*"
],
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.13.1",
"@drupal-api-client/api-client": "workspace:*",
"@vitest/coverage-v8": "^0.34.1",
"publint": "^0.2.0",
"tsup": "^7.3.0",
"vitest": "^0.34.6"
},
"typedocOptions": {
"entryPoints": [
"src/index.ts"
],
"tsconfig": "./tsconfig.typedoc.json"
},
"dependencies": {
"nanostores": "^0.10.2"
}
}
import { deepMap } from "nanostores";
import type { Cache } from "@drupal-api-client/api-client/src/types";
/**
* Factory function that created a default cache implementation that uses the
* nanostores library.
* @returns A cache object that satisfies the Cache interface.
*/
export const createCache = () => {
const store = deepMap<Record<string, unknown>>({});
return {
get: async <T>(key: string) => store.get()[key] as T,
set: async <T>(key: string, value: T) => {
store.setKey(key, value);
},
} satisfies Cache;
};
export * from "./defaultCache";
import type { Cache as ApiClientCache } from "@drupal-api-client/api-client";
import { createCache } from "../src/defaultCache";
describe("createCache", () => {
const cache = createCache();
it("should satisfy the Cache interface", () => {
const isCache = (t: unknown): t is ApiClientCache => {
if (typeof t !== "object" || t === null) {
return false;
}
return (
"get" in t &&
typeof t.get === "function" &&
"set" in t &&
typeof t.set === "function"
);
};
expect(isCache(cache)).toBe(true);
});
it("should store values and retrieve", async () => {
await cache.set("foo", "bar");
expect(await cache.get("foo")).toBe("bar");
});
});
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals"]
}
}
{
"extends": "../../tsconfig.json",
"include": ["src"]
}
import { defineConfig } from "tsup";
import pkgJson from "./package.json";
export default defineConfig({
entry: ["./src/**/*.ts"],
splitting: true,
treeshake: true,
dts: true,
clean: true,
outDir: "./dist",
format: ["esm", "cjs"],
external: [...Object.keys(pkgJson.devDependencies)],
minify: true,
platform: "neutral",
target: "es2020",
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment