Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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;
51 changes: 51 additions & 0 deletions packages/memo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@rocket.chat/memo",
"version": "0.31.25",
"description": "Memoization utilities",
"keywords": [
"memoize"
],
"homepage": "https://github.com/RocketChat/fuselage#readme",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
"bugs": {
"url": "https://github.com/RocketChat/fuselage/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/RocketChat/fuselage.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:cjs": "tsc -p tsconfig.cjs.json",
".:build:esm": "tsc -p tsconfig.esm.json",
"build": "run .:build:esm && run .:build:cjs",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
"clean": "rm -rf dist",
"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"
},
"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