diff --git a/CHANGELOG.md b/CHANGELOG.md index de2fcec..f00698f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 56b34f3..a34b19c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@grpcexpress/grpcexpress", - "version": "0.2.0", + "version": "0.2.1", "description": "", "private": false, "main": "./dist/index.js", diff --git a/src/CacheStore.test.ts b/src/CacheStore.test.ts index 5fa0353..7dfac88 100644 --- a/src/CacheStore.test.ts +++ b/src/CacheStore.test.ts @@ -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', () => { @@ -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', () => { diff --git a/src/CacheStore.ts b/src/CacheStore.ts index 561f5b8..8a1009b 100644 --- a/src/CacheStore.ts +++ b/src/CacheStore.ts @@ -1,31 +1,47 @@ export class CacheStore { #cache: { - data: Map; - 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) { @@ -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 @@ -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; diff --git a/src/PendingStore.test.ts b/src/PendingStore.test.ts index 8d8a036..32c35c4 100644 --- a/src/PendingStore.test.ts +++ b/src/PendingStore.test.ts @@ -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); diff --git a/src/PendingStore.ts b/src/PendingStore.ts index 47cc20f..eed2166 100644 --- a/src/PendingStore.ts +++ b/src/PendingStore.ts @@ -1,4 +1,4 @@ -import cacheStore from './CacheStore'; +import CacheStore from './CacheStore'; type item = { isPending: boolean; @@ -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) { @@ -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]; } @@ -39,6 +41,4 @@ class PendingStore { } } -const pendingStore = new PendingStore(); - -export default pendingStore; +export default PendingStore; diff --git a/src/grpcExpressClient.test.ts b/src/grpcExpressClient.test.ts index 707c840..6899281 100644 --- a/src/grpcExpressClient.test.ts +++ b/src/grpcExpressClient.test.ts @@ -1,7 +1,6 @@ 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', () => { @@ -9,7 +8,7 @@ describe('grpcExpressClient', () => { let user: User; beforeAll(() => { - Client = grpcExpressClient(StocksServiceClient); + Client = grpcExpressClient(StocksServiceClient, 30000); user = new User(); }); @@ -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); }); @@ -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'); @@ -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, {}); diff --git a/src/grpcExpressClient.ts b/src/grpcExpressClient.ts index 550e909..0bd95da 100644 --- a/src/grpcExpressClient.ts +++ b/src/grpcExpressClient.ts @@ -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( - 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); @@ -18,11 +22,16 @@ export function grpcExpressClient( 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 => { - 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) { @@ -34,7 +43,8 @@ export function grpcExpressClient( ); } - switch (cacheOption) { + // if no cache is passed, skip the caching step + switch (cacheOptions?.cache) { case 'nocache': return await constructor.prototype[method].call( this, @@ -54,7 +64,7 @@ export function grpcExpressClient( if (deserializerStore.has(method)) { const deserialize = deserializerStore.getDeserializer(method); - return deserialize(cache.buffer); + return deserialize(cache); } } @@ -76,7 +86,13 @@ export function grpcExpressClient( ); 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;