Skip to content

Commit

Permalink
Merge pull request #21 from oslabs-beta/refactored/cache_expiration
Browse files Browse the repository at this point in the history
Refactored cache expiration
  • Loading branch information
MrAgrali authored Sep 28, 2023
2 parents 1e71127 + 0113ea1 commit e0453ea
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 50 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @grpcexpress/grpcexpress

## 0.2.1

### Patch Changes

- Refactored the cache expiration from setTimeout to checking an expiration date

## 0.2.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grpcexpress/grpcexpress",
"version": "0.2.0",
"version": "0.2.1",
"description": "",
"private": false,
"main": "./dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions src/CacheStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CacheStore } from './CacheStore';
describe('CacheStore', () => {
let cacheStore: CacheStore;
beforeAll(() => {
cacheStore = new CacheStore();
cacheStore = new CacheStore(600000);
});

it('should create a cache store', () => {
Expand All @@ -16,7 +16,7 @@ describe('CacheStore', () => {
const buffer = new Uint8Array([1]);
cacheStore.subscribe('testKey', buffer);
const value = cacheStore.get('testKey');
expect(value).toEqual({ buffer: buffer });
expect(value).toEqual(buffer);
});

it('should be able to unsubscribe', () => {
Expand Down
56 changes: 40 additions & 16 deletions src/CacheStore.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
export class CacheStore {
#cache: {
data: Map<string, { buffer: Uint8Array }>;
capacity: number; // ~5MB
data: Map<
string,
{
buffer: Uint8Array;
expirationDate: Date;
}
>;
cacheDuration: number;
};

constructor() {
this.#cache = this.initStore();
constructor(cacheDuration: number) {
this.#cache = this.initStore(cacheDuration);
this.loadStore = this.loadStore.bind(this);
this.loadStore();
this.syncStore = this.syncStore.bind(this);
window.addEventListener('beforeunload', this.syncStore);
}

initStore(capacity: number = 2) {
initStore(cacheDuration: number) {
const cache = new Map();
return {
data: cache,
capacity: capacity,
cacheDuration,
};
}

subscribe(key: string, buffer: Uint8Array) {
subscribe(
key: string,
buffer: Uint8Array,
cacheDuration: number = this.#cache.cacheDuration
) {
if (this.#cache.data.has(key)) {
this.#cache.data.delete(key);
}

this.#cache.data.set(key, { buffer });
const expiration = new Date();
expiration.setTime(expiration.getTime() + cacheDuration);

this.#cache.data.set(key, {
buffer,
expirationDate: expiration,
});
}

unsubscribe(key: string) {
Expand All @@ -34,14 +50,21 @@ export class CacheStore {

get(key: string) {
const value = this.#cache.data.get(key);
return value;
// if the key doesn't exist return
if (!value) return;
// if the cache has expired, delete it and return
if (new Date() > value.expirationDate) {
this.unsubscribe(key);
return;
}
return value.buffer;
}

syncStore() {
const arr: [string, string][] = [];
const arr: [string, string, string][] = [];
// iterate through the map, push the key and buffer to an array
this.#cache.data.forEach((v, k) => {
arr.push([k, v.buffer.toString()]);
arr.push([k, v.buffer.toString(), v.expirationDate.toISOString()]);
});

// stringify the array so it can be saved to local storage
Expand All @@ -53,15 +76,16 @@ export class CacheStore {

if (!data) return;

const json = JSON.parse(data) as [string, string][];
const json = JSON.parse(data) as [string, string, string][];

json.forEach(array => {
const buffer = new Uint8Array(array[1].split(',').map(e => Number(e)));
this.subscribe(array[0], buffer);
const difference = Number(new Date(array[2])) - Number(new Date());
if (difference > 0) {
this.subscribe(array[0], buffer, difference);
}
});
}
}

const cacheStore = new CacheStore();

export default cacheStore;
export default CacheStore;
5 changes: 4 additions & 1 deletion src/PendingStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { describe, expect, it } from 'vitest';

import pendingStore from './PendingStore';
import CacheStore from './CacheStore';
import PendingStore from './PendingStore';

describe('PendingStore', () => {
const cacheStore = new CacheStore(600000);
const pendingStore = new PendingStore(cacheStore);
it('should be able to set a function call as pending', () => {
pendingStore.setPending('test');
expect(pendingStore.has('test')).toEqual(true);
Expand Down
12 changes: 6 additions & 6 deletions src/PendingStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cacheStore from './CacheStore';
import CacheStore from './CacheStore';

type item = {
isPending: boolean;
Expand All @@ -7,9 +7,11 @@ type item = {

class PendingStore {
#store: { [key: string]: item };
#cacheStore: CacheStore;

constructor() {
constructor(cacheStore: CacheStore) {
this.#store = {};
this.#cacheStore = cacheStore;
}

setPending(key: string) {
Expand All @@ -21,7 +23,7 @@ class PendingStore {

setDone(key: string) {
for (const resolve of this.#store[key].callbacks) {
resolve(cacheStore.get(key));
resolve(this.#cacheStore.get(key));
}
delete this.#store[key];
}
Expand All @@ -39,6 +41,4 @@ class PendingStore {
}
}

const pendingStore = new PendingStore();

export default pendingStore;
export default PendingStore;
27 changes: 12 additions & 15 deletions src/grpcExpressClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { StocksServiceClient } from '../../example/frontend_react/protos/StocksServiceClientPb';
import { User } from '../../example/frontend_react/protos/stocks_pb';
import cacheStore from './CacheStore';
import { StocksServiceClient } from '../example/frontend_react/protos/StocksServiceClientPb';
import { User } from '../example/frontend_react/protos/stocks_pb';
import grpcExpressClient from './grpcExpressClient';

describe('grpcExpressClient', () => {
let Client;
let user: User;

beforeAll(() => {
Client = grpcExpressClient(StocksServiceClient);
Client = grpcExpressClient(StocksServiceClient, 30000);
user = new User();
});

Expand All @@ -21,7 +20,12 @@ describe('grpcExpressClient', () => {
it('should be able to get stock list with existing user', async () => {
const client = new Client('http://localhost:8080');
user.setUsername('Murat');
const stocks = await client.getStocks(user, {});
const stocks = await client.getStocks(user, {
cacheOptions: {
cache: 'cache',
duration: 10000,
},
});
expect(stocks.toObject().stocksList.length).greaterThan(0);
});

Expand All @@ -32,15 +36,6 @@ describe('grpcExpressClient', () => {
expect(stocks.toObject().stocksList.length).toEqual(0);
});

it('should be able to cache a response', async () => {
const client = new Client('http://localhost:8080');
user.setUsername('Murat');
const stocks = await client.getStocks(user, {});
const key = `getStocks:${user.serializeBinary()}`;
const cachedBuffer = cacheStore.get(key)?.buffer;
expect(cachedBuffer).toEqual(stocks.serializeBinary());
});

it('should be able to return a cached response', async () => {
const client = new Client('http://localhost:8080');
user.setUsername('Arthur');
Expand All @@ -55,7 +50,9 @@ describe('grpcExpressClient', () => {
const client = new Client('http://localhost:8080');
user.setUsername('Shiyu');
await client.getStocks(user, {
cacheOption: 'nocache',
cacheOptions: {
cache: 'nocache',
},
});
const start = new Date();
await client.getStocks(user, {});
Expand Down
34 changes: 25 additions & 9 deletions src/grpcExpressClient.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { RpcError } from 'grpc-web';
import cacheStore from './CacheStore';
import CacheStore from './CacheStore';
import deserializerStore from './DeserializerStore';
import pendingStore from './PendingStore';
import PendingStore from './PendingStore';

export function grpcExpressClient<T extends { new (...args: any[]): object }>(
constructor: T
constructor: T,
cacheDuration: number = 600000 // defaults to 10 minutes
) {
const cacheStore = new CacheStore(cacheDuration);
const pendingStore = new PendingStore(cacheStore);

return class extends constructor {
constructor(...args: any[]) {
super(...args);
Expand All @@ -18,11 +22,16 @@ export function grpcExpressClient<T extends { new (...args: any[]): object }>(
for (const method of methods) {
const geMethod = async (
request: any,
metadata?: { [key: string]: string },
metadata?: { [key: string]: string } & {
cacheOptions?: {
cache: string;
duration: number;
};
},
callback?: (err: RpcError, response: any) => void
): Promise<any> => {
const { cacheOption } = metadata || {};
delete metadata?.cacheOption;
const { cacheOptions } = metadata || {};
delete metadata?.cacheOptions;

// we do not cache response when called using the callback method
if (callback) {
Expand All @@ -34,7 +43,8 @@ export function grpcExpressClient<T extends { new (...args: any[]): object }>(
);
}

switch (cacheOption) {
// if no cache is passed, skip the caching step
switch (cacheOptions?.cache) {
case 'nocache':
return await constructor.prototype[method].call(
this,
Expand All @@ -54,7 +64,7 @@ export function grpcExpressClient<T extends { new (...args: any[]): object }>(
if (deserializerStore.has(method)) {
const deserialize = deserializerStore.getDeserializer(method);

return deserialize(cache.buffer);
return deserialize(cache);
}
}

Expand All @@ -76,7 +86,13 @@ export function grpcExpressClient<T extends { new (...args: any[]): object }>(
);

const serialized = response.serializeBinary();
cacheStore.subscribe(key, serialized);
cacheStore.subscribe(
key,
serialized,
// if a duration is passed in the function call
// override the default duration
cacheOptions?.duration || cacheDuration
);

const deserializer =
response.__proto__.constructor.deserializeBinary;
Expand Down

0 comments on commit e0453ea

Please sign in to comment.