Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ’‘ [major] Prefer MSGPack over JSON #93

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
125 changes: 125 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
},
"dependencies": {
"ioredis": "5.x",
"msgpackr": "1.x",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christianblais looks sane and no-dependencies. However, usual introducing-new-depencency questions: how did you pick it? Were there alternatives, and if yes, what led you to think it's the best one?

Copy link
Contributor Author

@christianblais christianblais Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MessagePack is a well defined standard, so the question was β€” "which implementation should I pick?". There aren't a ton of options to pick from, and this one stands out as the clear winner in terms of love (number/frequency of commits), popularity, and speed.

Screenshot 2024-08-19 at 11 33 58β€―AM

Now, I'm aware a lib might be feature-complete with no bugs, hence the lack of activity. But looking at the downloads per week, once more, msgpackr stood out as the clear winner. That was the end of my investigation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yip that's what I meant. Okay with your annotated screenshot!

"lru-cache": "10.x",
"redlock": "4.x"
}
Expand Down
19 changes: 8 additions & 11 deletions src/lib/RedisCache.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Redis from 'ioredis';
import * as Redlock from 'redlock';
import { Packr } from 'msgpackr';

import { CachableValue, CacheInstance } from './CacheInstance';

const MPACK = new Packr({ moreTypes: true }); // `moreTypes: true` to get free support for Set & Map

/**
* Wrapper class for using Redis as a cache.
Expand All @@ -21,6 +23,7 @@ export class RedisCache extends CacheInstance {
public static TRUE_VALUE = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-TRUE';
public static FALSE_VALUE = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-FALSE';
public static JSON_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-JSON';
public static MSGP_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-MSGP';
public static ERROR_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-ERROR';
public static NUMBER_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-NUMBER';

Expand Down Expand Up @@ -166,19 +169,10 @@ export class RedisCache extends CacheInstance {
}

if (value instanceof Object) {
return RedisCache.JSON_PREFIX + JSON.stringify(value, (key, value) => {
if (value instanceof Set) {
return { __dataType: 'Set', value: Array.from(value) };
} else if (value instanceof Map) {
return { __dataType: 'Map', value: Array.from(value) };
} else {
return value;
}
});
return `${RedisCache.MSGP_PREFIX}${MPACK.pack(value).toString('binary')}`;
}

return value;

}

/**
Expand Down Expand Up @@ -212,6 +206,10 @@ export class RedisCache extends CacheInstance {
return false;
}

if (value.startsWith(RedisCache.MSGP_PREFIX)) {
return MPACK.unpack(Buffer.from(value.substring(RedisCache.MSGP_PREFIX.length), 'binary'));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘ Can you also fix the type of the function param on line 190?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it isn't related to this changeset per se, I did it in another PR.


if (value.startsWith(RedisCache.ERROR_PREFIX)) {
const deserializedError = JSON.parse(value.substring(RedisCache.ERROR_PREFIX.length));
// return error, restoring potential Error metadata set as object properties
Expand Down Expand Up @@ -240,7 +238,6 @@ export class RedisCache extends CacheInstance {
}

return value;

}

/**
Expand Down
53 changes: 47 additions & 6 deletions test/RedisCache_test.ts
christianblais marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ describe('RedisCache', () => {
},
};
let value = RedisCache.serializeValue(obj);
expect(value.startsWith(RedisCache.JSON_PREFIX)).to.be.true;
expect(value.startsWith(RedisCache.MSGP_PREFIX)).to.be.true;
value = RedisCache.deserializeValue(value);
expect(value).to.deep.equal(obj);
});

it('can serialize an object with a nested map', () => {
const mapStructure: Map<string, {
checksum: number;
Expand All @@ -78,11 +78,11 @@ describe('RedisCache', () => {
},
};
let value = RedisCache.serializeValue(obj);
expect(value.startsWith(RedisCache.JSON_PREFIX)).to.be.true;
expect(value.startsWith(RedisCache.MSGP_PREFIX)).to.be.true;
value = RedisCache.deserializeValue(value);
expect(value).to.deep.equal(obj);
});

it('can serialize an object with a nested set', () => {
const setStructure: Set<string> = new Set();
setStructure.add('key1');
Expand All @@ -100,7 +100,7 @@ describe('RedisCache', () => {
},
};
let value = RedisCache.serializeValue(obj);
expect(value.startsWith(RedisCache.JSON_PREFIX)).to.be.true;
expect(value.startsWith(RedisCache.MSGP_PREFIX)).to.be.true;
value = RedisCache.deserializeValue(value);
expect(value).to.deep.equal(obj);
});
Expand Down Expand Up @@ -222,6 +222,47 @@ describe('RedisCache', () => {

expect(await cache.itemCount()).to.equal(1);
});

it('can set a msgpacked object', async function (): Promise<void> {
if (!process.env.TEST_REDIS_URL) {
this.skip();
}

const cache = new RedisCache(process.env.TEST_REDIS_URL as string);
await cache.isReady();

// Just to be sure that the cache is really empty...
await cache.clear();

const wasSet = await cache.setValue('key', { '🍌': 'πŸ₯”' });
expect(wasSet).to.be.true;

const value = await cache.getValue('key');
expect(value).to.deep.equal({ '🍌': 'πŸ₯”' });

expect(await cache.itemCount()).to.equal(1);
});

it('can get a JSON value', async function (): Promise<void> {
if (!process.env.TEST_REDIS_URL) {
this.skip();
}

const cache = new RedisCache(process.env.TEST_REDIS_URL as string);
await cache.isReady();

// Just to be sure that the cache is really empty...
await cache.clear();

// Manual serialization here to avoid the automatic serialization of the setValue method.
const wasSet = await cache.setValue('key', `${RedisCache.JSON_PREFIX}${JSON.stringify({ '🍌': 'πŸ₯”' })}`);
expect(wasSet).to.be.true;

const value = await cache.getValue('key');
expect(value).to.deep.equal({ '🍌': 'πŸ₯”' });

expect(await cache.itemCount()).to.equal(1);
});
});

describe('itemCount', async () => {
Expand Down Expand Up @@ -258,7 +299,7 @@ describe('RedisCache', () => {
await cache.clear();

await cache.setValue('test1', 'value1');

const replicationAcknowledged = await cache.waitForReplication(0, 50);

// No replicas so we expect 0. This test basically confirms that waitForReplication doesn't crash. πŸ€·β€β™‚οΈ
Expand Down
Loading