Skip to content

Commit

Permalink
[nextjs] Add Vercel KV support for editing cache (#1530)
Browse files Browse the repository at this point in the history
* add kv-focused cache provider
  • Loading branch information
art-alexeyenko authored Jun 28, 2023
1 parent d036314 commit 48164d4
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Our versioning strategy is as follows:

## Unreleased

### 🎉 New Features & Improvements

* `[templates/nextjs]` `[sitecore-jss-nextjs]` Support for out-of-process editing data caches was added. Vercel KV or a custom Redis cache can be used to improve editing in Pages and Experience Editor when using Vercel deployment as editing/rendering host ([#1530](https://github.com/Sitecore/jss/pull/1530))

### 🐛 Bug Fixes

* `[sitecore-jss-nextjs]` Referrer is not captured by Personalize middleware ([#1542](https://github.com/Sitecore/jss/pull/1542))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { EditingDataMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing';

/**
* This Next.js API route is used to handle Sitecore editor data storage and retrieval by key
* on serverless deployment architectures (e.g. Vercel) via the `ServerlessEditingDataService`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { EditingRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editi
* 5. Return the rendered page HTML to the Sitecore editor
*/

/**
* For Vercel deployments:
* if you experience crashes in editing, you may need to use VercelEditingDataCache or a custom Redis data cache implementation with EditingRenderMiddleware
* Please refer to documentation for a detailed guide.
*/

// Bump body size limit (1mb by default) and disable response limit for Sitecore editor payloads
// See https://nextjs.org/docs/api-routes/request-helpers#custom-config
export const config = {
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@sitecore-jss/sitecore-jss": "^21.3.0-canary.10",
"@sitecore-jss/sitecore-jss-dev-tools": "^21.3.0-canary.10",
"@sitecore-jss/sitecore-jss-react": "^21.3.0-canary.10",
"@vercel/kv": "^0.2.1",
"node-html-parser": "^6.1.4",
"prop-types": "^15.8.1",
"regex-parser": "^2.2.11",
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/src/editing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export {
ServerlessEditingDataServiceConfig,
editingDataService,
} from './editing-data-service';
export { VercelEditingDataCache } from './vercel-editing-data-cache';
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, use } from 'chai';
import * as vercelKv from '@vercel/kv';
import sinon from 'sinon';
import { EditingData } from './editing-data';
import { VercelEditingDataCache } from './vercel-editing-data-cache';
import sinonChai from 'sinon-chai';

use(sinonChai);
const sandbox = sinon.createSandbox();

describe('vercel editing data cache', () => {
const setup = (key: string, value: Record<string, unknown> | null) => {
const kvStub: vercelKv.VercelKV = new vercelKv.VercelKV({
url: 'test',
token: 'test',
});
sandbox.stub(kvStub, 'set').resolves();
sandbox
.stub(kvStub, 'get')
.withArgs(key)
.resolves(value);
sandbox.stub(kvStub, 'expire').resolves();
sandbox.stub(vercelKv, 'createClient').returns(kvStub);
return kvStub;
};

afterEach(() => {
sandbox.restore();
});

it('should get entries from storage', async () => {
const key = 'top-secret';
const expectedResult: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
JSON.stringify(expectedResult);
setup(key, expectedResult);

const result = await new VercelEditingDataCache('test', 'tset').get(key);

expect(result as EditingData).to.deep.equal(expectedResult);
});

it('should return undefined on cache miss', async () => {
const key = 'no-such-key';
setup(key, null);
const result = await new VercelEditingDataCache('test', 'tset').get('no-such-key');
expect(result).to.deep.equal(undefined);
});

it('should invalidate entry after get', async () => {
const key = 'top-secret';
const expectedResult: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
JSON.stringify(expectedResult);
const kvStub = setup(key, expectedResult);

await new VercelEditingDataCache('test', 'tset').get(key);

expect(kvStub.expire).to.have.been.calledWith(key, 0);
});

it('should put entries into storage', async () => {
const key = 'top-secret';
const entry: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
const kvStub = setup('key', {});

await new VercelEditingDataCache('test', 'tset').set(key, entry);

expect(kvStub.set).to.have.been.calledWith(key, JSON.stringify(entry));
});

it('should put entries into storage with ttl', async () => {
const key = 'top-secret';
const entry: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
const kvStub = setup('key', {});

await new VercelEditingDataCache('test', 'tset').set(key, entry);

expect(kvStub.set).to.have.been.calledWith(key, JSON.stringify(entry), { ex: 120 });
});

it('should throw if initialized without API URL and token', () => {
expect(() => new VercelEditingDataCache('', '')).to.throw();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { VercelKV, createClient } from '@vercel/kv';
import { EditingDataCache } from './editing-data-cache';
import { EditingData } from './editing-data';
import { debug } from '@sitecore-jss/sitecore-jss';

/**
* Implementation of editing cache for Vercel deployments
* Uses Vercel KV database and client to store data
* Set TTL for cache data in constructor (default: 60 seconds)
*/
export class VercelEditingDataCache implements EditingDataCache {
protected redisCache: VercelKV;
private defaultTtl = 120;

/**
* @param {string} redisUrl KV endpoint URL. Usually stored in process.env.KV_REST_API_URL
* @param {string} redisToken KV endpoint tokem. Usually stored in process.env.KV_REST_API_TOKEN
*/
constructor(redisUrl: string | undefined, redisToken: string | undefined) {
if (!redisUrl || !redisToken) {
throw Error(
'API URL or token are missing, ensure you have set the KV or Upstash storage correctly.'
);
}
this.redisCache = createClient({
url: redisUrl,
token: redisToken,
});
}

set(key: string, editingData: EditingData): Promise<void> {
debug.editing(`Putting editing data for ${key} into redis storage...`);
return new Promise<void>((resolve, reject) => {
this.redisCache
.set(key, JSON.stringify(editingData), { ex: this.defaultTtl })
.then(() => resolve())
.catch((err) => reject(err));
});
}

get(key: string): Promise<EditingData | undefined> {
debug.editing(`Getting editing data for ${key} from redis storage...`);
return new Promise<EditingData | undefined>((resolve, reject) => {
this.redisCache
.get(key)
.then((entry) => {
const result = (entry || undefined) as EditingData;
this.redisCache.expire(key, 0).then(() => resolve(result));
})
.catch((err) => reject(err));
});
}
}
31 changes: 30 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6105,6 +6105,7 @@ __metadata:
"@types/react-dom": ^18.0.10
"@types/sinon": ^10.0.13
"@types/sinon-chai": ^3.2.9
"@vercel/kv": ^0.2.1
"@wojtekmaj/enzyme-adapter-react-17": ^0.8.0
chai: ^4.3.7
chai-as-promised: ^7.1.1
Expand Down Expand Up @@ -7770,6 +7771,24 @@ __metadata:
languageName: node
linkType: hard

"@upstash/redis@npm:1.20.6":
version: 1.20.6
resolution: "@upstash/redis@npm:1.20.6"
dependencies:
isomorphic-fetch: ^3.0.0
checksum: e4bcf1d61b0fa3dec5b2b730831a0d6939416bdafe1af753479e53b035aa70be49ca96a6f3f52473d488ec896588a0134ee71b4c71f0caecb6454648f2e3bd29
languageName: node
linkType: hard

"@vercel/kv@npm:^0.2.1":
version: 0.2.1
resolution: "@vercel/kv@npm:0.2.1"
dependencies:
"@upstash/redis": 1.20.6
checksum: 156058160caf151ee0a64204d83d1d6fa3fe9c568de2cb9c89f2e809855fda9ec26976c99a1f1fa74043f1fb00102561909608c6ea8f2ab4399b9663561d00b4
languageName: node
linkType: hard

"@vue/compiler-core@npm:3.2.45":
version: 3.2.45
resolution: "@vue/compiler-core@npm:3.2.45"
Expand Down Expand Up @@ -17223,6 +17242,16 @@ __metadata:
languageName: node
linkType: hard

"isomorphic-fetch@npm:^3.0.0":
version: 3.0.0
resolution: "isomorphic-fetch@npm:3.0.0"
dependencies:
node-fetch: ^2.6.1
whatwg-fetch: ^3.4.1
checksum: e5ab79a56ce5af6ddd21265f59312ad9a4bc5a72cebc98b54797b42cb30441d5c5f8d17c5cd84a99e18101c8af6f90c081ecb8d12fd79e332be1778d58486d75
languageName: node
linkType: hard

"isstream@npm:~0.1.2":
version: 0.1.2
resolution: "isstream@npm:0.1.2"
Expand Down Expand Up @@ -27897,7 +27926,7 @@ __metadata:
languageName: node
linkType: hard

"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0":
"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1":
version: 3.6.2
resolution: "whatwg-fetch@npm:3.6.2"
checksum: ee976b7249e7791edb0d0a62cd806b29006ad7ec3a3d89145921ad8c00a3a67e4be8f3fb3ec6bc7b58498724fd568d11aeeeea1f7827e7e1e5eae6c8a275afed
Expand Down

0 comments on commit 48164d4

Please sign in to comment.