keyv-dir-store 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/KeyvDirStoreAsJSON.ts +10 -10
- package/KeyvDirStoreAsYaml.ts +11 -11
- package/LICENSE +21 -21
- package/README.md +40 -33
- package/dist/index.js +23 -21
- package/index.spec.ts +67 -67
- package/index.ts +127 -120
- package/package.json +23 -4
package/KeyvDirStoreAsJSON.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { DeserializedData } from "keyv";
|
|
2
|
-
|
|
3
|
-
export const KeyvDirStoreAsJSON = {
|
|
4
|
-
serialize({ value }: DeserializedData<any>): string {
|
|
5
|
-
return JSON.stringify(value, null, 2);
|
|
6
|
-
},
|
|
7
|
-
deserialize(str: string): DeserializedData<any> {
|
|
8
|
-
return { value: JSON.parse(str), expires: undefined };
|
|
9
|
-
},
|
|
10
|
-
};
|
|
1
|
+
import type { DeserializedData } from "keyv";
|
|
2
|
+
|
|
3
|
+
export const KeyvDirStoreAsJSON = {
|
|
4
|
+
serialize({ value }: DeserializedData<any>): string {
|
|
5
|
+
return JSON.stringify(value, null, 2);
|
|
6
|
+
},
|
|
7
|
+
deserialize(str: string): DeserializedData<any> {
|
|
8
|
+
return { value: JSON.parse(str), expires: undefined };
|
|
9
|
+
},
|
|
10
|
+
};
|
package/KeyvDirStoreAsYaml.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { DeserializedData } from "keyv";
|
|
2
|
-
import yaml from "yaml";
|
|
3
|
-
|
|
4
|
-
export const KeyvDirStoreAsYaml = {
|
|
5
|
-
serialize({ value }: DeserializedData<any>): string {
|
|
6
|
-
return yaml.stringify(value);
|
|
7
|
-
},
|
|
8
|
-
deserialize(str: string): DeserializedData<any> {
|
|
9
|
-
return { value: yaml.parse(str), expires: undefined };
|
|
10
|
-
},
|
|
11
|
-
};
|
|
1
|
+
import type { DeserializedData } from "keyv";
|
|
2
|
+
import yaml from "yaml";
|
|
3
|
+
|
|
4
|
+
export const KeyvDirStoreAsYaml = {
|
|
5
|
+
serialize({ value }: DeserializedData<any>): string {
|
|
6
|
+
return yaml.stringify(value);
|
|
7
|
+
},
|
|
8
|
+
deserialize(str: string): DeserializedData<any> {
|
|
9
|
+
return { value: yaml.parse(str), expires: undefined };
|
|
10
|
+
},
|
|
11
|
+
};
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 snomiao
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 snomiao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
1
|
-
# Keyv Directory Store
|
|
2
|
-
|
|
3
|
-
Store key into a
|
|
4
|
-
|
|
5
|
-
## Usages
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
// Default: Store each value with JSON in format "{value: ..., expires: ...}"
|
|
9
|
-
new Keyv({
|
|
10
|
-
store: new KeyvDirStore(".cache/kv")
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
// Store each
|
|
14
|
-
new Keyv({
|
|
15
|
-
store: new KeyvDirStore(".cache/kv", { ext: ".csv" }),
|
|
16
|
-
serialize: ({ value }) => d3.csvFormat(value),
|
|
17
|
-
deserialize: (str) => ({ value: d3.csvParse(str), expires: undefined }),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// Store each
|
|
21
|
-
new Keyv({
|
|
22
|
-
store: new KeyvDirStore(".cache/kv", { ext: ".
|
|
23
|
-
serialize: ({ value }) =>
|
|
24
|
-
deserialize: (str) => ({ value:
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Store each value with
|
|
28
|
-
new Keyv({
|
|
29
|
-
store: new KeyvDirStore(".cache/kv", { ext: ".json" }),
|
|
30
|
-
serialize: ({ value }) =>
|
|
31
|
-
deserialize: (str) => ({ value:
|
|
32
|
-
});
|
|
33
|
-
|
|
1
|
+
# Keyv Directory Store
|
|
2
|
+
|
|
3
|
+
High performance Filesystem Keyv Store, caches each key-value pair into a $key.json. and more! *.JSON, *.YAML, *.CSV is also avaliable.
|
|
4
|
+
|
|
5
|
+
## Usages
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// Default: Store each value with JSON in format "{value: ..., expires: ...}"
|
|
9
|
+
new Keyv({
|
|
10
|
+
store: new KeyvDirStore(".cache/kv")
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Store each object list with CSV
|
|
14
|
+
new Keyv({
|
|
15
|
+
store: new KeyvDirStore(".cache/kv", { ext: ".csv" }),
|
|
16
|
+
serialize: ({ value }) => d3.csvFormat(value),
|
|
17
|
+
deserialize: (str) => ({ value: d3.csvParse(str), expires: undefined }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Store each object list with TSV
|
|
21
|
+
new Keyv({
|
|
22
|
+
store: new KeyvDirStore(".cache/kv", { ext: ".tsv" }),
|
|
23
|
+
serialize: ({ value }) => d3.tsvFormat(value),
|
|
24
|
+
deserialize: (str) => ({ value: d3.tsvParse(str), expires: undefined }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Store each value with YAML
|
|
28
|
+
new Keyv({
|
|
29
|
+
store: new KeyvDirStore(".cache/kv", { ext: ".json" }),
|
|
30
|
+
serialize: ({ value }) => yaml.stringify(value),
|
|
31
|
+
deserialize: (str) => ({ value: yaml.parse(str), expires: undefined }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Store each value with JSON
|
|
35
|
+
new Keyv({
|
|
36
|
+
store: new KeyvDirStore(".cache/kv", { ext: ".json" }),
|
|
37
|
+
serialize: ({ value }) => JSON.stringify(value, null, 2),
|
|
38
|
+
deserialize: (str) => ({ value: JSON.parse(str), expires: undefined }),
|
|
39
|
+
});
|
|
40
|
+
|
|
34
41
|
```
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
7
|
var __toESM = (mod, isNodeMode, target) => {
|
|
@@ -116,12 +116,6 @@ var require_charenc = __commonJS((exports, module) => {
|
|
|
116
116
|
|
|
117
117
|
// node_modules/is-buffer/index.js
|
|
118
118
|
var require_is_buffer = __commonJS((exports, module) => {
|
|
119
|
-
var isBuffer = function(obj) {
|
|
120
|
-
return !!obj.constructor && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj);
|
|
121
|
-
};
|
|
122
|
-
var isSlowBuffer = function(obj) {
|
|
123
|
-
return typeof obj.readFloatLE === "function" && typeof obj.slice === "function" && isBuffer(obj.slice(0, 0));
|
|
124
|
-
};
|
|
125
119
|
/*!
|
|
126
120
|
* Determine if an object is a Buffer
|
|
127
121
|
*
|
|
@@ -131,6 +125,12 @@ var require_is_buffer = __commonJS((exports, module) => {
|
|
|
131
125
|
module.exports = function(obj) {
|
|
132
126
|
return obj != null && (isBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer);
|
|
133
127
|
};
|
|
128
|
+
function isBuffer(obj) {
|
|
129
|
+
return !!obj.constructor && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj);
|
|
130
|
+
}
|
|
131
|
+
function isSlowBuffer(obj) {
|
|
132
|
+
return typeof obj.readFloatLE === "function" && typeof obj.slice === "function" && isBuffer(obj.slice(0, 0));
|
|
133
|
+
}
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
// node_modules/md5/md5.js
|
|
@@ -255,12 +255,12 @@ var require_md5 = __commonJS((exports, module) => {
|
|
|
255
255
|
|
|
256
256
|
// node_modules/truncate-utf8-bytes/lib/truncate.js
|
|
257
257
|
var require_truncate = __commonJS((exports, module) => {
|
|
258
|
-
|
|
258
|
+
function isHighSurrogate(codePoint) {
|
|
259
259
|
return codePoint >= 55296 && codePoint <= 56319;
|
|
260
|
-
}
|
|
261
|
-
|
|
260
|
+
}
|
|
261
|
+
function isLowSurrogate(codePoint) {
|
|
262
262
|
return codePoint >= 56320 && codePoint <= 57343;
|
|
263
|
-
}
|
|
263
|
+
}
|
|
264
264
|
module.exports = function truncate(getLength, string, byteLength) {
|
|
265
265
|
if (typeof string !== "string") {
|
|
266
266
|
throw new Error("Input must be string");
|
|
@@ -296,19 +296,19 @@ var require_truncate_utf8_bytes = __commonJS((exports, module) => {
|
|
|
296
296
|
|
|
297
297
|
// node_modules/sanitize-filename/index.js
|
|
298
298
|
var require_sanitize_filename = __commonJS((exports, module) => {
|
|
299
|
-
var sanitize = function(input, replacement) {
|
|
300
|
-
if (typeof input !== "string") {
|
|
301
|
-
throw new Error("Input must be string");
|
|
302
|
-
}
|
|
303
|
-
var sanitized = input.replace(illegalRe, replacement).replace(controlRe, replacement).replace(reservedRe, replacement).replace(windowsReservedRe, replacement).replace(windowsTrailingRe, replacement);
|
|
304
|
-
return truncate(sanitized, 255);
|
|
305
|
-
};
|
|
306
299
|
var truncate = require_truncate_utf8_bytes();
|
|
307
300
|
var illegalRe = /[\/\?<>\\:\*\|"]/g;
|
|
308
301
|
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
|
309
302
|
var reservedRe = /^\.+$/;
|
|
310
303
|
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
|
311
304
|
var windowsTrailingRe = /[\. ]+$/;
|
|
305
|
+
function sanitize(input, replacement) {
|
|
306
|
+
if (typeof input !== "string") {
|
|
307
|
+
throw new Error("Input must be string");
|
|
308
|
+
}
|
|
309
|
+
var sanitized = input.replace(illegalRe, replacement).replace(controlRe, replacement).replace(reservedRe, replacement).replace(windowsReservedRe, replacement).replace(windowsTrailingRe, replacement);
|
|
310
|
+
return truncate(sanitized, 255);
|
|
311
|
+
}
|
|
312
312
|
module.exports = function(input, options) {
|
|
313
313
|
var replacement = options && options.replacement || "";
|
|
314
314
|
var output = sanitize(input, replacement);
|
|
@@ -322,7 +322,7 @@ var require_sanitize_filename = __commonJS((exports, module) => {
|
|
|
322
322
|
// index.ts
|
|
323
323
|
var import_md5 = __toESM(require_md5(), 1);
|
|
324
324
|
var import_sanitize_filename = __toESM(require_sanitize_filename(), 1);
|
|
325
|
-
import {mkdir, readFile, rm, stat, utimes, writeFile} from "fs/promises";
|
|
325
|
+
import { mkdir, readFile, rm, stat, utimes, writeFile } from "fs/promises";
|
|
326
326
|
import path from "path";
|
|
327
327
|
|
|
328
328
|
class KeyvDirStore {
|
|
@@ -336,7 +336,8 @@ class KeyvDirStore {
|
|
|
336
336
|
filename,
|
|
337
337
|
ext
|
|
338
338
|
} = {}) {
|
|
339
|
-
this.#ready = mkdir(dir, { recursive: true })
|
|
339
|
+
this.#ready = mkdir(dir, { recursive: true }).catch(() => {
|
|
340
|
+
});
|
|
340
341
|
this.#cache = cache;
|
|
341
342
|
this.#dir = dir;
|
|
342
343
|
this.#filename = filename ?? this.#defaultFilename;
|
|
@@ -382,7 +383,8 @@ class KeyvDirStore {
|
|
|
382
383
|
const expires = ttl ? Date.now() + ttl : 0;
|
|
383
384
|
this.#cache.set(key, { value, expires });
|
|
384
385
|
await this.#ready;
|
|
385
|
-
await mkdir(this.#dir, { recursive: true })
|
|
386
|
+
await mkdir(this.#dir, { recursive: true }).catch(() => {
|
|
387
|
+
});
|
|
386
388
|
await writeFile(this.#path(key), value);
|
|
387
389
|
await utimes(this.#path(key), new Date, new Date(expires ?? 0));
|
|
388
390
|
return true;
|
package/index.spec.ts
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import Keyv from "keyv";
|
|
3
|
-
import { KeyvDirStore } from ".";
|
|
4
|
-
import { KeyvDirStoreAsJSON } from "./KeyvDirStoreAsJSON";
|
|
5
|
-
|
|
6
|
-
it("KeyvDirStore works", async () => {
|
|
7
|
-
// store test
|
|
8
|
-
const kv = new Keyv<number | string | { obj: boolean }>({
|
|
9
|
-
store: new KeyvDirStore(".cache/test1", { filename: (x) => x }),
|
|
10
|
-
namespace: "",
|
|
11
|
-
deserialize: KeyvDirStore.deserialize,
|
|
12
|
-
serialize: KeyvDirStore.serialize,
|
|
13
|
-
});
|
|
14
|
-
await kv.clear();
|
|
15
|
-
await kv.set("a", 1234, -86400e3); // already expired
|
|
16
|
-
|
|
17
|
-
expect(existsSync(".cache/test1/a.json")).toEqual(true);
|
|
18
|
-
expect(await kv.get("a")).toEqual(undefined); // will delete file before get
|
|
19
|
-
expect(existsSync(".cache/test1/a.json")).toEqual(false);
|
|
20
|
-
|
|
21
|
-
expect(existsSync(".cache/test1/b.json")).toEqual(false);
|
|
22
|
-
await kv.set("b", 1234); // never expired
|
|
23
|
-
expect(existsSync(".cache/test1/b.json")).toEqual(true);
|
|
24
|
-
expect(await kv.get("b")).toEqual(1234);
|
|
25
|
-
|
|
26
|
-
await kv.set("c", "b", 86400e3); // 1 day
|
|
27
|
-
expect(await kv.get("c")).toEqual("b");
|
|
28
|
-
await kv.set("d", { obj: false }, 86400e3); // obj store
|
|
29
|
-
expect(await kv.get("d")).toEqual({ obj: false });
|
|
30
|
-
|
|
31
|
-
// new instance with no cache Obj, to test file cache
|
|
32
|
-
const kv2 = new Keyv<number | string | { obj: boolean }>({
|
|
33
|
-
store: new KeyvDirStore(".cache/test1", { filename: (x) => x }),
|
|
34
|
-
namespace: "",
|
|
35
|
-
...KeyvDirStoreAsJSON,
|
|
36
|
-
});
|
|
37
|
-
expect(await kv2.get("a")).toEqual(undefined); // will delete file before get
|
|
38
|
-
expect(await kv2.get("b")).toEqual(1234);
|
|
39
|
-
expect(await kv2.get("c")).toEqual("b");
|
|
40
|
-
expect(await kv2.get("d")).toEqual({ obj: false });
|
|
41
|
-
});
|
|
42
|
-
it("KeyvDirStore works without deserialize", async () => {
|
|
43
|
-
// store test
|
|
44
|
-
const kv = new Keyv<number | string | { obj: boolean }>({
|
|
45
|
-
store: new KeyvDirStore(".cache/test2", { filename: (x) => x }),
|
|
46
|
-
namespace: "",
|
|
47
|
-
});
|
|
48
|
-
await kv.clear();
|
|
49
|
-
await kv.set("a", 1234, -86400e3); // already expired
|
|
50
|
-
expect(await kv.get("a")).toEqual(undefined); // will delete file before get
|
|
51
|
-
await kv.set("b", 1234); // never expired
|
|
52
|
-
expect(await kv.get("b")).toEqual(1234);
|
|
53
|
-
await kv.set("c", "b", 86400e3); // 1 day
|
|
54
|
-
expect(await kv.get("c")).toEqual("b");
|
|
55
|
-
await kv.set("d", { obj: false }, 86400e3); // obj store
|
|
56
|
-
expect(await kv.get("d")).toEqual({ obj: false });
|
|
57
|
-
|
|
58
|
-
// new instance with no cache Obj, to test file cache
|
|
59
|
-
const kv2 = new Keyv<number | string | { obj: boolean }>({
|
|
60
|
-
store: new KeyvDirStore(".cache/test2", { filename: (x) => x }),
|
|
61
|
-
namespace: "",
|
|
62
|
-
});
|
|
63
|
-
expect(await kv2.get("a")).toEqual(undefined); // will delete file before get
|
|
64
|
-
expect(await kv2.get("b")).toEqual(1234);
|
|
65
|
-
expect(await kv2.get("c")).toEqual("b");
|
|
66
|
-
expect(await kv2.get("d")).toEqual({ obj: false });
|
|
67
|
-
});
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import Keyv from "keyv";
|
|
3
|
+
import { KeyvDirStore } from ".";
|
|
4
|
+
import { KeyvDirStoreAsJSON } from "./KeyvDirStoreAsJSON";
|
|
5
|
+
|
|
6
|
+
it("KeyvDirStore works", async () => {
|
|
7
|
+
// store test
|
|
8
|
+
const kv = new Keyv<number | string | { obj: boolean }>({
|
|
9
|
+
store: new KeyvDirStore(".cache/test1", { filename: (x) => x }),
|
|
10
|
+
namespace: "",
|
|
11
|
+
deserialize: KeyvDirStore.deserialize,
|
|
12
|
+
serialize: KeyvDirStore.serialize,
|
|
13
|
+
});
|
|
14
|
+
await kv.clear();
|
|
15
|
+
await kv.set("a", 1234, -86400e3); // already expired
|
|
16
|
+
|
|
17
|
+
expect(existsSync(".cache/test1/a.json")).toEqual(true);
|
|
18
|
+
expect(await kv.get("a")).toEqual(undefined); // will delete file before get
|
|
19
|
+
expect(existsSync(".cache/test1/a.json")).toEqual(false);
|
|
20
|
+
|
|
21
|
+
expect(existsSync(".cache/test1/b.json")).toEqual(false);
|
|
22
|
+
await kv.set("b", 1234); // never expired
|
|
23
|
+
expect(existsSync(".cache/test1/b.json")).toEqual(true);
|
|
24
|
+
expect(await kv.get("b")).toEqual(1234);
|
|
25
|
+
|
|
26
|
+
await kv.set("c", "b", 86400e3); // 1 day
|
|
27
|
+
expect(await kv.get("c")).toEqual("b");
|
|
28
|
+
await kv.set("d", { obj: false }, 86400e3); // obj store
|
|
29
|
+
expect(await kv.get("d")).toEqual({ obj: false });
|
|
30
|
+
|
|
31
|
+
// new instance with no cache Obj, to test file cache
|
|
32
|
+
const kv2 = new Keyv<number | string | { obj: boolean }>({
|
|
33
|
+
store: new KeyvDirStore(".cache/test1", { filename: (x) => x }),
|
|
34
|
+
namespace: "",
|
|
35
|
+
...KeyvDirStoreAsJSON,
|
|
36
|
+
});
|
|
37
|
+
expect(await kv2.get("a")).toEqual(undefined); // will delete file before get
|
|
38
|
+
expect(await kv2.get("b")).toEqual(1234);
|
|
39
|
+
expect(await kv2.get("c")).toEqual("b");
|
|
40
|
+
expect(await kv2.get("d")).toEqual({ obj: false });
|
|
41
|
+
});
|
|
42
|
+
it("KeyvDirStore works without deserialize", async () => {
|
|
43
|
+
// store test
|
|
44
|
+
const kv = new Keyv<number | string | { obj: boolean }>({
|
|
45
|
+
store: new KeyvDirStore(".cache/test2", { filename: (x) => x }),
|
|
46
|
+
namespace: "",
|
|
47
|
+
});
|
|
48
|
+
await kv.clear();
|
|
49
|
+
await kv.set("a", 1234, -86400e3); // already expired
|
|
50
|
+
expect(await kv.get("a")).toEqual(undefined); // will delete file before get
|
|
51
|
+
await kv.set("b", 1234); // never expired
|
|
52
|
+
expect(await kv.get("b")).toEqual(1234);
|
|
53
|
+
await kv.set("c", "b", 86400e3); // 1 day
|
|
54
|
+
expect(await kv.get("c")).toEqual("b");
|
|
55
|
+
await kv.set("d", { obj: false }, 86400e3); // obj store
|
|
56
|
+
expect(await kv.get("d")).toEqual({ obj: false });
|
|
57
|
+
|
|
58
|
+
// new instance with no cache Obj, to test file cache
|
|
59
|
+
const kv2 = new Keyv<number | string | { obj: boolean }>({
|
|
60
|
+
store: new KeyvDirStore(".cache/test2", { filename: (x) => x }),
|
|
61
|
+
namespace: "",
|
|
62
|
+
});
|
|
63
|
+
expect(await kv2.get("a")).toEqual(undefined); // will delete file before get
|
|
64
|
+
expect(await kv2.get("b")).toEqual(1234);
|
|
65
|
+
expect(await kv2.get("c")).toEqual("b");
|
|
66
|
+
expect(await kv2.get("d")).toEqual({ obj: false });
|
|
67
|
+
});
|
package/index.ts
CHANGED
|
@@ -1,120 +1,127 @@
|
|
|
1
|
-
import type { DeserializedData, default as Keyv } from "keyv";
|
|
2
|
-
import md5 from "md5";
|
|
3
|
-
import { mkdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import sanitizeFilename from "sanitize-filename";
|
|
6
|
-
|
|
7
|
-
type CacheMap<Value> = Map<string, DeserializedData<Value>>;
|
|
8
|
-
/**
|
|
9
|
-
* KeyvDirStore is a Keyv.Store<string> implementation that stores data in files.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
1
|
+
import type { DeserializedData, default as Keyv } from "keyv";
|
|
2
|
+
import md5 from "md5";
|
|
3
|
+
import { mkdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import sanitizeFilename from "sanitize-filename";
|
|
6
|
+
|
|
7
|
+
type CacheMap<Value> = Map<string, DeserializedData<Value>>;
|
|
8
|
+
/**
|
|
9
|
+
* KeyvDirStore is a Keyv.Store<string> implementation that stores data in files.
|
|
10
|
+
*
|
|
11
|
+
* learn more [README](./README.md)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const kv = new Keyv<number | string | { obj: boolean }>({
|
|
15
|
+
* store: new KeyvDirStore("cache/test"),
|
|
16
|
+
* deserialize: KeyvDirStore.deserialize,
|
|
17
|
+
* serialize: KeyvDirStore.serialize,
|
|
18
|
+
* });
|
|
19
|
+
* await kv.set("a", 1234, -86400e3); // already expired
|
|
20
|
+
* expect(await kv.get("a")).toEqual(undefined); // will delete file before get
|
|
21
|
+
* await kv.set("b", 1234); // never expired
|
|
22
|
+
* expect(await kv.get("b")).toEqual(1234);
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
|
|
26
|
+
#dir: string;
|
|
27
|
+
#cache: CacheMap<Value>;
|
|
28
|
+
#ready: Promise<unknown>;
|
|
29
|
+
#filename: (key: string) => string;
|
|
30
|
+
ext = ".json";
|
|
31
|
+
constructor(
|
|
32
|
+
/** dir to cache store
|
|
33
|
+
* WARN: dont share this dir with other purpose
|
|
34
|
+
* it will be rm -f when keyv.clear() is called
|
|
35
|
+
*/
|
|
36
|
+
dir: string,
|
|
37
|
+
{
|
|
38
|
+
cache = new Map(),
|
|
39
|
+
filename,
|
|
40
|
+
ext,
|
|
41
|
+
}: {
|
|
42
|
+
cache?: CacheMap<Value>;
|
|
43
|
+
filename?: (key: string) => string;
|
|
44
|
+
ext?: string;
|
|
45
|
+
} = {}
|
|
46
|
+
) {
|
|
47
|
+
this.#ready = mkdir(dir, { recursive: true }).catch(() => {});
|
|
48
|
+
this.#cache = cache;
|
|
49
|
+
this.#dir = dir;
|
|
50
|
+
this.#filename = filename ?? this.#defaultFilename;
|
|
51
|
+
this.ext = ext ?? this.ext;
|
|
52
|
+
}
|
|
53
|
+
#defaultFilename(key: string) {
|
|
54
|
+
// use dir as hash salt to avoid collisions
|
|
55
|
+
const readableName = sanitizeFilename(key).slice(0, 16);
|
|
56
|
+
const hashName = md5(key + "+SALT-poS1djRa4M2jXsWi").slice(0, 16);
|
|
57
|
+
const name = `${readableName}-${hashName}`;
|
|
58
|
+
return name;
|
|
59
|
+
}
|
|
60
|
+
#path(key: string) {
|
|
61
|
+
return path.join(
|
|
62
|
+
this.#dir,
|
|
63
|
+
sanitizeFilename(this.#filename(key) + this.ext)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
async get(key: string) {
|
|
67
|
+
// read memory
|
|
68
|
+
const memCached = this.#cache.get(key);
|
|
69
|
+
if (memCached) {
|
|
70
|
+
// console.log("memory cache hit but expired", key, cached.expires, Date.now());
|
|
71
|
+
if (memCached.expires && memCached.expires < Date.now()) {
|
|
72
|
+
await this.delete(key);
|
|
73
|
+
} else {
|
|
74
|
+
return memCached.value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// read file cache
|
|
78
|
+
const path = this.#path(key);
|
|
79
|
+
const stats = await stat(path).catch(() => null);
|
|
80
|
+
if (!stats) return undefined; // stat not found
|
|
81
|
+
const expires = +stats.mtime;
|
|
82
|
+
if (expires !== 0) {
|
|
83
|
+
const expired = +stats.mtime < +Date.now();
|
|
84
|
+
if (expired) {
|
|
85
|
+
// console.log("file cache hit expired", key, expires, Date.now(), expired);
|
|
86
|
+
await this.delete(key);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return await readFile(path, "utf8").catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
async set(key: string, value: Value, ttl?: number) {
|
|
93
|
+
if (!value) return await this.delete(key);
|
|
94
|
+
// const { value, expires } = JSON.parse(stored) as DeserializedData<Value>;
|
|
95
|
+
const expires = ttl ? Date.now() + ttl : 0;
|
|
96
|
+
// save to memory
|
|
97
|
+
this.#cache.set(key, { value, expires });
|
|
98
|
+
// save to file
|
|
99
|
+
await this.#ready;
|
|
100
|
+
// console.log({ key, value, expires });
|
|
101
|
+
await mkdir(this.#dir, { recursive: true }).catch(() => {});
|
|
102
|
+
await writeFile(this.#path(key), value); // create a expired file
|
|
103
|
+
await utimes(this.#path(key), new Date(), new Date(expires ?? 0)); // set expires time as mtime (0 as never expired)
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
async delete(key: string) {
|
|
107
|
+
// delete memory
|
|
108
|
+
this.#cache.delete(key);
|
|
109
|
+
await rm(this.#path(key), { force: true });
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
async clear() {
|
|
113
|
+
await rm(this.#dir, { recursive: true }).catch(() => void 0);
|
|
114
|
+
}
|
|
115
|
+
async has(key: string) {
|
|
116
|
+
return undefined !== (await this.get(key));
|
|
117
|
+
}
|
|
118
|
+
// Save expires into mtime, and value into file
|
|
119
|
+
/** @deprecated use KeyvDirStoreJSON */
|
|
120
|
+
static serialize({ value }: DeserializedData<any>): string {
|
|
121
|
+
return JSON.stringify(value, null, 2);
|
|
122
|
+
}
|
|
123
|
+
/** @deprecated use KeyvDirStoreJSON */
|
|
124
|
+
static deserialize(str: string): DeserializedData<any> {
|
|
125
|
+
return { value: JSON.parse(str), expires: undefined };
|
|
126
|
+
}
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keyv-dir-store",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "High performance Filesystem Keyv Store, caches each key-value pair into a $key.json. and more! *.JSON, *.YAML, *.CSV is also avaliable.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"keyv",
|
|
7
|
+
"keyv-store",
|
|
8
|
+
"dir"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/snomiao/keyv-dir-store#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/snomiao/keyv-dir-store/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/snomiao/keyv-dir-store.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
4
19
|
"author": "snomiao <snomiao@gmail.com>",
|
|
5
20
|
"type": "module",
|
|
6
21
|
"exports": {
|
|
7
22
|
"import": "./dist/index.js",
|
|
8
23
|
"types": "./index.ts"
|
|
9
24
|
},
|
|
25
|
+
"main": "index.js",
|
|
10
26
|
"module": "index.ts",
|
|
11
27
|
"types": "./index.ts",
|
|
12
28
|
"files": [
|
|
@@ -15,6 +31,7 @@
|
|
|
15
31
|
],
|
|
16
32
|
"scripts": {
|
|
17
33
|
"build": "bun build index.ts --outdir=dist --target=bun",
|
|
34
|
+
"prepare": "husky",
|
|
18
35
|
"prerelease": "bun run build && bun run test",
|
|
19
36
|
"release": "bunx standard-version && git push --follow-tags && npm publish",
|
|
20
37
|
"test": "bun test"
|
|
@@ -26,10 +43,12 @@
|
|
|
26
43
|
"yaml": "^2.4.5"
|
|
27
44
|
},
|
|
28
45
|
"devDependencies": {
|
|
29
|
-
"@types/bun": "^1.1.
|
|
30
|
-
"@types/jest": "^29.5.
|
|
46
|
+
"@types/bun": "^1.1.17",
|
|
47
|
+
"@types/jest": "^29.5.14",
|
|
31
48
|
"@types/md5": "^2.3.5",
|
|
32
|
-
"
|
|
49
|
+
"husky": "^9.1.7",
|
|
50
|
+
"semantic-release": "^24.2.1",
|
|
51
|
+
"typescript": "^5.7.3"
|
|
33
52
|
},
|
|
34
53
|
"peerDependencies": {
|
|
35
54
|
"keyv": "^4.5.4"
|