Skip to content

Commit cc89168

Browse files
authored
[Feature] Add compress lz4-napi (#1284)
* [Feature] Add compress lz4-napi * update * change name
1 parent 9c034e9 commit cc89168

File tree

14 files changed

+295
-4
lines changed

14 files changed

+295
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ If there is already an adapter that you would like to add, please post an issue
8181

8282
* [brotli](https://github.com/jaredwray/keyv/tree/main/packages/compress-brotli): Brotli compression adapter
8383
* [Gzip](https://github.com/jaredwray/keyv/tree/main/packages/compress-gzip): Gzip compression adapter
84+
* [lz4](https://github.com/jaredwray/keyv/tree/main/packages/compress-lz4): lz4-napi compression adapter
8485

8586
### Third-party Storage Adapters
8687

packages/compress-lz4/LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2017-2021 Luke Childs
4+
Copyright (c) 2021-2022 Jared Wray
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

packages/compress-lz4/README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# @keyv/compress-lz4 [<img width="100" align="right" src="https://jaredwray.com/images/keyv-symbol.svg" alt="keyv">](https://github.com/jaredwra/keyv)
2+
3+
> lz4-napi compression for Keyv
4+
5+
[![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
6+
[![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv)
7+
[![npm](https://img.shields.io/npm/v/@keyv/compress-lz4.svg)](https://www.npmjs.com/package/@keyv/compress-lz4)
8+
[![npm](https://img.shields.io/npm/dm/@keyv/compress-lz4)](https://npmjs.com/package/@keyv/compress-lz4)
9+
10+
lz4-napi compression for [Keyv](https://github.com/jaredwray/keyv).
11+
12+
lz4-napi is a data compression algorithm that is designed to be fast and efficient.
13+
14+
## Install
15+
16+
```shell
17+
npm install --save keyv @keyv/compress-lz4
18+
```
19+
20+
## Usage
21+
22+
```javascript
23+
import Keyv from 'keyv';
24+
import KeyvLz4 from '@keyv/compress-lz4';
25+
26+
const keyv = new Keyv({store: new Map(), compression: new KeyvLz4()});
27+
28+
```
29+
30+
## API
31+
32+
### @keyv/compress-lz4(\[options])
33+
34+
#### options
35+
36+
All options for `@keyv/compress-lz4` are based on the package [lz4-napi](https://github.com/antoniomuso/lz4-napi)
37+
38+
## License
39+
40+
[MIT © Jared Wray](LICENSE)

packages/compress-lz4/package.json

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "@keyv/compress-lz4",
3+
"version": "0.1.0",
4+
"description": "lz4-napi compression for keyv",
5+
"type": "module",
6+
"main": "dist/index.cjs",
7+
"module": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"require": "./dist/index.cjs",
12+
"import": "./dist/index.js"
13+
}
14+
},
15+
"scripts": {
16+
"build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
17+
"prepublishOnly": "pnpm build",
18+
"test": "xo --fix && vitest run --coverage",
19+
"test:ci": "xo && vitest --run --sequence.setupFiles=list",
20+
"clean": "rimraf ./node_modules ./coverage ./test/testdb.sqlite ./dist"
21+
},
22+
"repository": {
23+
"type": "git",
24+
"url": "git+https://github.com/jaredwray/keyv.git"
25+
},
26+
"keywords": [
27+
"compress",
28+
"lz4-napi",
29+
"lz4",
30+
"snappy",
31+
"keyv",
32+
"storage",
33+
"adapter",
34+
"key",
35+
"value",
36+
"store",
37+
"cache",
38+
"ttl"
39+
],
40+
"author": "Blockcoders Engineering <[email protected]>",
41+
"license": "MIT",
42+
"bugs": {
43+
"url": "https://github.com/jaredwray/keyv/issues"
44+
},
45+
"homepage": "https://github.com/jaredwray/keyv",
46+
"dependencies": {
47+
"@keyv/serialize": "workspace:^",
48+
"lz4-napi": "^2.8.0",
49+
"keyv": "workspace:^"
50+
},
51+
"devDependencies": {
52+
"@keyv/test-suite": "workspace:^",
53+
"@vitest/coverage-v8": "^2.1.8",
54+
"c8": "^10.1.3",
55+
"rimraf": "^6.0.1",
56+
"vitest": "^2.1.8",
57+
"xo": "^0.60.0"
58+
},
59+
"tsd": {
60+
"directory": "test"
61+
},
62+
"engines": {
63+
"node": ">= 18"
64+
},
65+
"files": [
66+
"dist",
67+
"LICENSE"
68+
]
69+
}

packages/compress-lz4/src/index.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {compress, uncompress} from 'lz4-napi';
2+
import {defaultDeserialize, defaultSerialize} from '@keyv/serialize';
3+
import {type Deserialize, type Serialize} from 'types';
4+
5+
export class KeyvLz4 {
6+
constructor(private readonly dictionary?: string) {}
7+
8+
async compress(data: string): Promise<Uint8Array> {
9+
return compress(Buffer.from(data), this.getDictionary());
10+
}
11+
12+
async decompress(data: Uint8Array): Promise<string> {
13+
const value = await uncompress(Buffer.from(data), this.getDictionary());
14+
15+
return value.toString('utf8');
16+
}
17+
18+
async serialize({value, expires}: Serialize): Promise<string> {
19+
const compressedUint8Array = await this.compress(value);
20+
21+
return defaultSerialize({value: compressedUint8Array, expires});
22+
}
23+
24+
async deserialize(data: string): Promise<Serialize> {
25+
const {value, expires}: Deserialize = defaultDeserialize(data);
26+
const uncompressedUint8Array = await this.decompress(value);
27+
28+
return {value: uncompressedUint8Array, expires};
29+
}
30+
31+
private getDictionary() {
32+
if (this.dictionary) {
33+
return Buffer.from(this.dictionary);
34+
}
35+
36+
return undefined;
37+
}
38+
}
39+
40+
export default KeyvLz4;

packages/compress-lz4/src/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type Serialize = {
2+
value: string;
3+
expires?: number;
4+
};
5+
6+
export type Deserialize = {
7+
value: Uint8Array;
8+
expires?: number;
9+
};

packages/compress-lz4/test/test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {keyvCompresstionTests} from '@keyv/test-suite';
2+
import {Keyv} from 'keyv';
3+
import * as test from 'vitest';
4+
import KeyvLz4 from '../src/index.js';
5+
6+
// @ts-expect-error - KeyvLz4 type
7+
keyvCompresstionTests(test, new KeyvLz4());
8+
9+
test.it('object type compression/decompression', async t => {
10+
const keyv = new KeyvLz4();
11+
const value = JSON.stringify({
12+
a: 1,
13+
b: 'test',
14+
c: true,
15+
});
16+
const compressed = await keyv.compress(value);
17+
const decompressed = await keyv.decompress(compressed);
18+
t.expect(decompressed).toEqual(value);
19+
});
20+
21+
test.it('compression with options', async t => {
22+
const keyv = new KeyvLz4('test');
23+
const keyvWithoutOptions = new KeyvLz4();
24+
const compressed = await keyv.compress('whatever');
25+
const compressedWithoutOptions = await keyvWithoutOptions.compress('whatever');
26+
t.expect(compressed).not.toBe(compressedWithoutOptions);
27+
});
28+
29+
test.it('decompress should not throw error when empty with gzip', async t => {
30+
const keyv = new Keyv({store: new Map(), compression: new KeyvLz4()});
31+
await t.expect(keyv.get('foo')).resolves.not.toThrowError();
32+
});
33+
34+
test.it('should not throw error when empty', async t => {
35+
const keyv = new Keyv({store: new Map()});
36+
await t.expect(keyv.get('foo')).resolves.not.toThrowError();
37+
});

packages/compress-lz4/tsconfig.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
5+
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
6+
7+
/* Emit */
8+
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
9+
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
10+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
11+
12+
/* Interop Constraints */
13+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
14+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
15+
16+
/* Type Checking */
17+
"strict": true, /* Enable all strict type-checking options. */
18+
19+
/* Completeness */
20+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
21+
"lib": [
22+
"ESNext", "DOM"
23+
]
24+
}
25+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {defineConfig, mergeConfig} from 'vitest/config';
2+
import vitestConfig from '../../vitest.general.config.js';
3+
4+
export default mergeConfig(vitestConfig, defineConfig({}));

packages/keyv/README.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -289,14 +289,30 @@ The following are third-party storage adapters compatible with Keyv:
289289

290290
# Compression
291291

292-
Keyv supports `gzip` and `brotli` compression. To enable compression, pass the `compress` option to the constructor.
292+
Keyv supports `gzip`, `brotli` and lz4-napi compression. To enable compression, pass the `compress` option to the constructor.
293293

294294
```js
295295
import Keyv from 'keyv';
296296
import KeyvGzip from '@keyv/compress-gzip';
297297

298298
const keyvGzip = new KeyvGzip();
299-
const keyv = new Keyv({ compression: KeyvGzip });
299+
const keyv = new Keyv({ compression: keyvGzip });
300+
```
301+
302+
```js
303+
import Keyv from 'keyv';
304+
import KeyvBrotli from '@keyv/compress-brotli';
305+
306+
const keyvBrotli = new KeyvBrotli();
307+
const keyv = new Keyv({ compression: keyvBrotli });
308+
```
309+
310+
```js
311+
import Keyv from 'keyv';
312+
import KeyvLz4 from '@keyv/compress-lz4';
313+
314+
const keyvLz4 = new KeyvLz4();
315+
const keyv = new Keyv({ compression: keyvLz4 });
300316
```
301317

302318
You can also pass a custom compression function to the `compression` option. Following the pattern of the official compression adapters.

packages/keyv/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
},
6666
"devDependencies": {
6767
"@keyv/compress-brotli": "workspace:^",
68+
"@keyv/compress-lz4": "workspace:^",
6869
"@keyv/compress-gzip": "workspace:^",
6970
"@keyv/memcache": "workspace:^",
7071
"@keyv/mongo": "workspace:^",

packages/keyv/test/test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {KeyvSqlite} from '@keyv/sqlite';
55
import {KeyvMongo} from '@keyv/mongo';
66
import {KeyvBrotli} from '@keyv/compress-brotli';
77
import {KeyvGzip} from '@keyv/compress-gzip';
8+
import {KeyvLz4} from '@keyv/compress-lz4';
89
import {KeyvMemcache} from '@keyv/memcache';
910
import Keyv, {type KeyvStoreAdapter, type StoredDataNoRaw, type CompressionAdapter} from '../src/index.js';
1011

@@ -397,6 +398,12 @@ test.it('compress/decompress with gzip', async t => {
397398
t.expect(await keyv.get('foo')).toBe('bar');
398399
});
399400

401+
test.it('compress/decompress with lz4-napi', async t => {
402+
const keyv = new Keyv({store: new Map(), compression: new KeyvLz4()});
403+
await keyv.set('foo', 'bar');
404+
t.expect(await keyv.get('foo')).toBe('bar');
405+
});
406+
400407
test.it('iterator should exists with url', t => {
401408
const store = new Keyv({store: new KeyvMongo({url: 'mongodb://127.0.0.1:27017'})});
402409
t.expect(typeof store.iterator).toBe('function');

packages/website/site/docs/index.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ await users.clear();
165165

166166
## 6. Advanced - Enable Compression
167167

168-
Keyv supports both `gzip` and `brotli` methods of compression. Before you can enable compression, you will need to install the compression package:
168+
Keyv supports both `gzip`, `brotli` and lz4-napi methods of compression. Before you can enable compression, you will need to install the compression package:
169169

170170
```sh
171171
npm install --save keyv @keyv/compress-gzip
@@ -182,6 +182,26 @@ const keyvGzip = new KeyvGzip();
182182
const keyv = new Keyv({ compression: KeyvGzip });
183183
```
184184

185+
### Example - Enable Brotli compression
186+
187+
```js
188+
import Keyv from 'keyv';
189+
import KeyvBrotli from '@keyv/compress-brotli';
190+
191+
const keyvBrotli = new KeyvBrotli();
192+
const keyv = new Keyv({ compression: keyvBrotli });
193+
```
194+
195+
### Example - Enable lz4-napi compression
196+
197+
```js
198+
import Keyv from 'keyv';
199+
import KeyvLz4 from '@keyv/compress-lz4';
200+
201+
const keyvLz4 = new KeyvLz4();
202+
const keyv = new Keyv({ compression: keyvLz4 });
203+
```
204+
185205
You can also pass a custom compression function to the compression option. Custom compression functions must follow the pattern of the official compression adapter (see below for further information).
186206

187207
### Want to build your own?

packages/website/src/docs.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async function main() {
1414
async function copyStorageAdapters() {
1515
const packagesPath = await getRelativePackagePath();
1616
const storageAdapters = await fs.promises.readdir(`${packagesPath}`);
17-
const filterList = ["keyv", "website", "compress-brotli", "compress-gzip", "test-suite", ".DS_Store", "serialize", "third-party"];
17+
const filterList = ["keyv", "website", "compress-brotli", "compress-gzip", "compress-lz4", "test-suite", ".DS_Store", "serialize", "third-party"];
1818

1919
for (const storageAdapter of storageAdapters) {
2020
if((filterList.indexOf(storageAdapter) > -1) !== true ) {

0 commit comments

Comments
 (0)