Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"@rocket.chat/logo": "~0.33.1",
"@rocket.chat/media-calls": "workspace:^",
"@rocket.chat/media-signaling": "workspace:^",
"@rocket.chat/memo": "~0.31.25",
"@rocket.chat/memo": "workspace:~",
"@rocket.chat/message-parser": "workspace:^",
"@rocket.chat/message-types": "workspace:~",
"@rocket.chat/model-typings": "workspace:^",
Expand Down
3 changes: 1 addition & 2 deletions fuselage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,10 @@ if [[ $action == "next-all" || $action == "latest-all" ]]; then
📦 @rocket.chat/fuselage-hooks [UPDATING to $targetVersion version...]
📦 @rocket.chat/icons [UPDATING to $targetVersion version...]
📦 @rocket.chat/logo [UPDATING to $targetVersion version...]
📦 @rocket.chat/memo [UPDATING to $targetVersion version...]
📦 @rocket.chat/onboarding-ui [UPDATING to $targetVersion version...]
📦 @rocket.chat/layout [UPDATING to $targetVersion version...]"

eval "yarn up @rocket.chat/emitter@$targetVersion @rocket.chat/fuselage-toastbar@$targetVersion @rocket.chat/fuselage-tokens@$targetVersion @rocket.chat/css-in-js@$targetVersion @rocket.chat/styled@$targetVersion @rocket.chat/fuselage@$targetVersion @rocket.chat/fuselage-hooks@$targetVersion @rocket.chat/icons@$targetVersion @rocket.chat/logo@$targetVersion @rocket.chat/memo@$targetVersion @rocket.chat/onboarding-ui@$targetVersion @rocket.chat/layout@$targetVersion"
eval "yarn up @rocket.chat/emitter@$targetVersion @rocket.chat/fuselage-toastbar@$targetVersion @rocket.chat/fuselage-tokens@$targetVersion @rocket.chat/css-in-js@$targetVersion @rocket.chat/styled@$targetVersion @rocket.chat/fuselage@$targetVersion @rocket.chat/fuselage-hooks@$targetVersion @rocket.chat/icons@$targetVersion @rocket.chat/logo@$targetVersion @rocket.chat/onboarding-ui@$targetVersion @rocket.chat/layout@$targetVersion"
exit 1
fi

Expand Down
6 changes: 6 additions & 0 deletions packages/memo/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import server from '@rocket.chat/jest-presets/server';
import type { Config } from 'jest';

export default {
preset: server.preset,
} satisfies Config;
50 changes: 50 additions & 0 deletions packages/memo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@rocket.chat/memo",
"version": "0.31.25",
"description": "Memoization utilities",
"keywords": [
"memoize"
],
"bugs": {
"url": "https://github.com/RocketChat/Rocket.Chat/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/RocketChat/Rocket.Chat.git",
"directory": "packages/memo"
},
"license": "MIT",
"author": {
"name": "Rocket.Chat",
"url": "https://rocket.chat/"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest",
"testunit": "jest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/tsconfig": "workspace:~",
"eslint": "~9.39.5",
"jest": "~30.2.0",
"prettier": "~3.3.3",
"ts-jest": "~29.4.11",
"typescript": "~5.9.3"
},
"volta": {
"extends": "../../package.json"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/memo/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { memoize, clear, type MemoizableFunction, type MemoizedFunction, type Options } from './memoize';
105 changes: 105 additions & 0 deletions packages/memo/src/memoize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { memoize, clear } from './memoize';

it('should memoize a function that takes no parameter', () => {
const fn = jest.fn(() => 'foo');
const memoized = jest.fn(memoize(fn));

memoized(undefined);
memoized(undefined);

expect(memoized).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledTimes(1);

expect(fn).toHaveReturnedWith('foo');
expect(memoized).toHaveReturnedWith('foo');
});

it('should memoize a function that takes one parameter', () => {
const fn = jest.fn((i: number) => i + 1);
const memoized = jest.fn(memoize(fn));

memoized(5);
memoized(5);
memoized(2);

expect(memoized).toHaveBeenCalledTimes(3);
expect(fn).toHaveBeenCalledTimes(2);

expect(fn).toHaveNthReturnedWith(1, 6);
expect(fn).toHaveNthReturnedWith(2, 3);

expect(memoized).toHaveNthReturnedWith(1, 6);
expect(memoized).toHaveNthReturnedWith(2, 6);
expect(memoized).toHaveNthReturnedWith(3, 3);
});

describe('clear', () => {
it('should discard cached values of a memoized function', () => {
const fn = jest.fn(() => 'foo');
const memoized = memoize(fn);
const spiedMemoized = jest.fn(memoized);

spiedMemoized(undefined);
spiedMemoized(undefined);
clear(memoized);
spiedMemoized(undefined);

expect(spiedMemoized).toHaveBeenCalledTimes(3);
expect(fn).toHaveBeenCalledTimes(2);

expect(fn).toHaveReturnedWith('foo');
expect(spiedMemoized).toHaveReturnedWith('foo');
});

it('should do nothing when a non-memoized function is passed', () => {
const fn = jest.fn(() => 'foo');

expect(() => clear(fn)).not.toThrow();
});
});

describe('timeout', () => {
it('should memoize a function that takes one parameter and clear after x ms', () => {
jest.useFakeTimers();

const fn = jest.fn((i: number) => i + 1);
const memoized = jest.fn(memoize(fn, { maxAge: 3000 }));

memoized(5);
jest.advanceTimersByTime(2000);
memoized(5);
jest.advanceTimersByTime(2000);
memoized(5);
jest.advanceTimersByTime(3000);
memoized(5);

expect(fn).toHaveBeenCalledTimes(2);

expect(memoized).toHaveNthReturnedWith(1, 6);
expect(memoized).toHaveNthReturnedWith(2, 6);
expect(memoized).toHaveNthReturnedWith(3, 6);
expect(memoized).toHaveNthReturnedWith(4, 6);
});

it('should memoize a function caching for two parameters and clearing both after x ms each one', () => {
jest.useFakeTimers();

const fn = jest.fn((i: number) => i + 1);
const memoized = jest.fn(memoize(fn, { maxAge: 3000 }));

memoized(5);
jest.advanceTimersByTime(2000);
memoized(6);
jest.advanceTimersByTime(2000);

memoized(6);
memoized(5);

expect(fn).toHaveBeenCalledTimes(3);

expect(memoized).toHaveNthReturnedWith(1, 6);
expect(memoized).toHaveNthReturnedWith(2, 7);
expect(memoized).toHaveNthReturnedWith(3, 7);
expect(memoized).toHaveNthReturnedWith(4, 6);
});
});
59 changes: 59 additions & 0 deletions packages/memo/src/memoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export type MemoizableFunction<T, A, R> = (this: T, arg: A) => R;
export type MemoizedFunction<T, A, R> = (this: T, arg: A) => R;

export type Options = {
maxAge: number;
};

const store = new WeakMap<MemoizableFunction<unknown, unknown, unknown>, Map<unknown, unknown>>();
Comment thread
tassoevan marked this conversation as resolved.

const isCachedValue = <A, R>(cachedValue: R | undefined, arg: A, cache: Map<A, R>): cachedValue is R =>
cache.has(arg) && cache.get(arg) === cachedValue;
Comment thread
tassoevan marked this conversation as resolved.

export const memoize = <T, A, R>(fn: MemoizableFunction<T, A, R>, _options?: Options): MemoizedFunction<T, A, R> => {
const cache = new Map<A, R>();
const cacheTimers = new Map<A, ReturnType<typeof setTimeout>>();

const memoized: MemoizedFunction<T, A, R> = function (this, arg) {
const cleanUp = (): void => {
cache.delete(arg);
cacheTimers.delete(arg);
};

const cachedValue = cache.get(arg);

if (isCachedValue(cachedValue, arg, cache)) {
const oldTimer = cacheTimers.get(arg);
if (oldTimer) {
clearTimeout(oldTimer);
}

if (_options) {
const timer = setTimeout(cleanUp, _options.maxAge);
cacheTimers.set(arg, timer);
}

return cachedValue;
}

const result = fn.call(this, arg);

cache.set(arg, result);

if (_options) {
const timer = setTimeout(cleanUp, _options.maxAge);
cacheTimers.set(arg, timer);
}

return result;
};

store.set(memoized as MemoizableFunction<unknown, unknown, unknown>, cache);

return memoized;
};

export const clear = (fn: MemoizedFunction<unknown, unknown, unknown>): void => {
Comment thread
tassoevan marked this conversation as resolved.
const cache = store.get(fn);
cache?.clear();
Comment thread
tassoevan marked this conversation as resolved.
};
12 changes: 12 additions & 0 deletions packages/memo/tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist/cjs"
},
"exclude": ["**/*.spec.*", "jest.config.*"]
}
12 changes: 12 additions & 0 deletions packages/memo/tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist/esm"
},
"exclude": ["**/*.spec.*", "jest.config.*"]
}
4 changes: 4 additions & 0 deletions packages/memo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "@rocket.chat/tsconfig/base.json",
"include": ["src/**/*"]
}
21 changes: 14 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9743,12 +9743,19 @@ __metadata:
languageName: unknown
linkType: soft

"@rocket.chat/memo@npm:^0.31.25, @rocket.chat/memo@npm:~0.31.25":
version: 0.31.25
resolution: "@rocket.chat/memo@npm:0.31.25"
checksum: 10/92d595c68d76a5258fb37ed4639e2709ba290c5d240df1272d81a2ab6b4be28ee2dd5b721dad940fe2638a89e8d14e684a970c59890003a06ce6088c655b7c0e
languageName: node
linkType: hard
"@rocket.chat/memo@npm:^0.31.25, @rocket.chat/memo@workspace:packages/memo, @rocket.chat/memo@workspace:~":
version: 0.0.0-use.local
resolution: "@rocket.chat/memo@workspace:packages/memo"
dependencies:
"@rocket.chat/jest-presets": "workspace:~"
"@rocket.chat/tsconfig": "workspace:~"
eslint: "npm:~9.39.5"
jest: "npm:~30.2.0"
prettier: "npm:~3.3.3"
ts-jest: "npm:~29.4.11"
typescript: "npm:~5.9.3"
languageName: unknown
linkType: soft

"@rocket.chat/message-parser@workspace:^, @rocket.chat/message-parser@workspace:packages/message-parser, @rocket.chat/message-parser@workspace:~":
version: 0.0.0-use.local
Expand Down Expand Up @@ -9867,7 +9874,7 @@ __metadata:
"@rocket.chat/logo": "npm:~0.33.1"
"@rocket.chat/media-calls": "workspace:^"
"@rocket.chat/media-signaling": "workspace:^"
"@rocket.chat/memo": "npm:~0.31.25"
"@rocket.chat/memo": "workspace:~"
"@rocket.chat/message-parser": "workspace:^"
"@rocket.chat/message-types": "workspace:~"
"@rocket.chat/mock-providers": "workspace:^"
Expand Down
Loading