diff --git a/spec/common/misc/sequentialize.spec.ts b/spec/common/misc/sequentialize.spec.ts new file mode 100644 index 0000000000..33c8e958e4 --- /dev/null +++ b/spec/common/misc/sequentialize.spec.ts @@ -0,0 +1,141 @@ +import { sequentialize } from '../../../src/misc/sequentialize'; + +describe('sequentialize decorator', () => { + it('should call the function once', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.bar(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(1); + }); + + it('should call the function once for each instance of the object', async () => { + const foo = new Foo(); + const foo2 = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.bar(1)); + promises.push(foo2.bar(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(1); + expect(foo2.calls).toBe(1); + }); + + it('should call the function once with key function', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.baz(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(1); + }); + + it('should call the function again when already resolved', async () => { + const foo = new Foo(); + await foo.bar(1); + expect(foo.calls).toBe(1); + await foo.bar(1); + expect(foo.calls).toBe(2); + }); + + it('should call the function again when already resolved with a key function', async () => { + const foo = new Foo(); + await foo.baz(1); + expect(foo.calls).toBe(1); + await foo.baz(1); + expect(foo.calls).toBe(2); + }); + + it('should call the function for each argument', async () => { + const foo = new Foo(); + await Promise.all([ + foo.bar(1), + foo.bar(1), + foo.bar(2), + foo.bar(2), + foo.bar(3), + foo.bar(3), + ]); + expect(foo.calls).toBe(3); + }); + + it('should call the function for each argument with key function', async () => { + const foo = new Foo(); + await Promise.all([ + foo.baz(1), + foo.baz(1), + foo.baz(2), + foo.baz(2), + foo.baz(3), + foo.baz(3), + ]); + expect(foo.calls).toBe(3); + }); + + it('should return correct result for each call', async () => { + const foo = new Foo(); + const allRes = []; + + await Promise.all([ + foo.bar(1).then((res) => allRes.push(res)), + foo.bar(1).then((res) => allRes.push(res)), + foo.bar(2).then((res) => allRes.push(res)), + foo.bar(2).then((res) => allRes.push(res)), + foo.bar(3).then((res) => allRes.push(res)), + foo.bar(3).then((res) => allRes.push(res)), + ]); + expect(foo.calls).toBe(3); + expect(allRes.length).toBe(6); + allRes.sort(); + expect(allRes).toEqual([2, 2, 4, 4, 6, 6]); + }); + + it('should return correct result for each call with key function', async () => { + const foo = new Foo(); + const allRes = []; + + await Promise.all([ + foo.baz(1).then((res) => allRes.push(res)), + foo.baz(1).then((res) => allRes.push(res)), + foo.baz(2).then((res) => allRes.push(res)), + foo.baz(2).then((res) => allRes.push(res)), + foo.baz(3).then((res) => allRes.push(res)), + foo.baz(3).then((res) => allRes.push(res)), + ]); + expect(foo.calls).toBe(3); + expect(allRes.length).toBe(6); + allRes.sort(); + expect(allRes).toEqual([3, 3, 6, 6, 9, 9]); + }); +}); + +class Foo { + calls = 0; + + @sequentialize() + bar(a) { + this.calls++; + return new Promise((res) => { + setTimeout(() => { + res(a * 2); + }, Math.random() * 100); + }); + } + + @sequentialize((args) => args[0]) + baz(a) { + this.calls++; + return new Promise((res) => { + setTimeout(() => { + res(a * 3); + }, Math.random() * 100); + }); + } +} diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts new file mode 100644 index 0000000000..221cadd118 --- /dev/null +++ b/src/misc/sequentialize.ts @@ -0,0 +1,52 @@ +/** + * Use as a Decorator on async functions, it will prevent multiple 'active' calls as the same time + * + * If a promise was returned from a previous call to this function, that hasn't yet resolved it will + * be returned, instead of calling the original function again + * + * Results are not cached, once the promise has returned, the next call will result in a fresh call + */ +export function sequentialize(key: (args: any[]) => string = JSON.stringify) { + return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { + const originalMethod: () => Promise = descriptor.value; + + const caches = new Map>>(); + const getCache = (obj: any) => { + let cache = caches.get(obj); + if (cache) { + return cache; + } + cache = new Map>(); + caches.set(obj, cache); + + return cache; + }; + + return { + value: function(...args: any[]) { + const argsKey = key(args); + const cache = getCache(this); + + let res = cache.get(argsKey); + if (res) { + return res; + } + + res = originalMethod.apply(this, args) + .then((val: any) => { + cache.delete(argsKey); + + return val; + }) + .catch((err: any) => { + cache.delete(argsKey); + + throw err; + }); + cache.set(argsKey, res); + + return res; + }, + }; + }; +} diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 6d49d1724e..fdc4c9098c 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -2,6 +2,7 @@ import { EncryptionType } from '../../enums/encryptionType'; import { CryptoService } from '../../abstractions/crypto.service'; +import { sequentialize } from '../../misc/sequentialize'; import { Utils } from '../../misc/utils'; export class CipherString { @@ -89,6 +90,7 @@ export class CipherString { } } + @sequentialize((args) => args[0]) async decrypt(orgId: string): Promise { if (this.decryptedValue) { return Promise.resolve(this.decryptedValue); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index effb5825c8..54adfc7945 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -11,6 +11,7 @@ import { StorageService } from '../abstractions/storage.service'; import { ConstantsService } from './constants.service'; +import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; const Keys = { @@ -164,6 +165,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.privateKey; } + @sequentialize() async getOrgKeys(): Promise> { if (this.orgKeys != null && this.orgKeys.size > 0) { return this.orgKeys;