Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nextjs] Add Vercel KV support for editing cache #1530

Merged
merged 16 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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