keyv-dir-store 0.0.7 → 0.0.8

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.
@@ -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
+ };
@@ -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 file per value
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 value 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 value with YAML
21
- new Keyv({
22
- store: new KeyvDirStore(".cache/kv", { ext: ".json" }),
23
- serialize: ({ value }) => yaml.stringify(value),
24
- deserialize: (str) => ({ value: yaml.parse(str), expires: undefined }),
25
- });
26
-
27
- // Store each value with JSON
28
- new Keyv({
29
- store: new KeyvDirStore(".cache/kv", { ext: ".json" }),
30
- serialize: ({ value }) => JSON.stringify(value, null, 2),
31
- deserialize: (str) => ({ value: JSON.parse(str), expires: undefined }),
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
- var isHighSurrogate = function(codePoint) {
258
+ function isHighSurrogate(codePoint) {
259
259
  return codePoint >= 55296 && codePoint <= 56319;
260
- };
261
- var isLowSurrogate = function(codePoint) {
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,129 @@
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
- * @example
11
- * const kv = new Keyv<number | string | { obj: boolean }>({
12
- * store: new KeyvDirStore("cache/test"),
13
- * deserialize: KeyvDirStore.deserialize,
14
- * serialize: KeyvDirStore.serialize,
15
- * });
16
- * await kv.set("a", 1234, -86400e3); // already expired
17
- * expect(await kv.get("a")).toEqual(undefined); // will delete file before get
18
- * await kv.set("b", 1234); // never expired
19
- * expect(await kv.get("b")).toEqual(1234);
20
- */
21
- export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
22
- #dir: string;
23
- #cache: CacheMap<Value>;
24
- #ready: Promise<unknown>;
25
- #filename: (key: string) => string;
26
- ext = ".json";
27
- constructor(
28
- dir: string,
29
- {
30
- cache = new Map(),
31
- filename,
32
- ext,
33
- }: {
34
- cache?: CacheMap<Value>;
35
- filename?: (key: string) => string;
36
- ext?: string;
37
- } = {}
38
- ) {
39
- this.#ready = mkdir(dir, { recursive: true });
40
- this.#cache = cache;
41
- this.#dir = dir;
42
- this.#filename = filename ?? this.#defaultFilename;
43
- this.ext = ext ?? this.ext;
44
- }
45
- #defaultFilename(key: string) {
46
- // use dir as hash salt to avoid collisions
47
- const readableName = sanitizeFilename(key).slice(0, 16);
48
- const hashName = md5(key + "+SALT-poS1djRa4M2jXsWi").slice(0, 16);
49
- const name = `${readableName}-${hashName}`;
50
- return name;
51
- }
52
- #path(key: string) {
53
- return path.join(
54
- this.#dir,
55
- sanitizeFilename(this.#filename(key) + this.ext)
56
- );
57
- }
58
- async get(key: string) {
59
- // read memory
60
- const memCached = this.#cache.get(key);
61
- if (memCached) {
62
- // console.log("memory cache hit but expired", key, cached.expires, Date.now());
63
- if (memCached.expires && memCached.expires < Date.now()) {
64
- await this.delete(key);
65
- } else {
66
- return memCached.value;
67
- }
68
- }
69
- // read file cache
70
- const path = this.#path(key);
71
- const stats = await stat(path).catch(() => null);
72
- if (!stats) return undefined; // stat not found
73
- const expires = +stats.mtime;
74
- if (expires !== 0) {
75
- const expired = +stats.mtime < +Date.now();
76
- if (expired) {
77
- // console.log("file cache hit expired", key, expires, Date.now(), expired);
78
- await this.delete(key);
79
- return undefined;
80
- }
81
- }
82
- return await readFile(path, "utf8").catch(() => undefined);
83
- }
84
- async set(key: string, value: Value, ttl?: number) {
85
- if (!value) return await this.delete(key);
86
- // const { value, expires } = JSON.parse(stored) as DeserializedData<Value>;
87
- const expires = ttl ? Date.now() + ttl : 0;
88
- // save to memory
89
- this.#cache.set(key, { value, expires });
90
- // save to file
91
- await this.#ready;
92
- // console.log({ key, value, expires });
93
- await mkdir(this.#dir, { recursive: true });
94
- await writeFile(this.#path(key), value); // create a expired file
95
- await utimes(this.#path(key), new Date(), new Date(expires ?? 0)); // set expires time as mtime (0 as never expired)
96
- return true;
97
- }
98
- async delete(key: string) {
99
- // delete memory
100
- this.#cache.delete(key);
101
- await rm(this.#path(key), { force: true });
102
- return true;
103
- }
104
- async clear() {
105
- await rm(this.#dir, { recursive: true }).catch(() => void 0);
106
- }
107
- async has(key: string) {
108
- return undefined !== (await this.get(key));
109
- }
110
-
111
- // Save expires into mtime, and value into file
112
- /** @deprecated use KeyvDirStoreJSON */
113
- static serialize({ value }: DeserializedData<any>): string {
114
- return JSON.stringify(value, null, 2);
115
- }
116
- /** @deprecated use KeyvDirStoreJSON */
117
- static deserialize(str: string): DeserializedData<any> {
118
- return { value: JSON.parse(str), expires: undefined };
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
+
50
+ this.#dir = dir;
51
+ this.#filename = filename ?? this.#defaultFilename;
52
+ this.ext = ext ?? this.ext;
53
+ }
54
+ #defaultFilename(key: string) {
55
+ // use dir as hash salt to avoid collisions
56
+ const readableName = sanitizeFilename(key).slice(0, 16);
57
+ const hashName = md5(key + "+SALT-poS1djRa4M2jXsWi").slice(0, 16);
58
+ const name = `${readableName}-${hashName}`;
59
+ return name;
60
+ }
61
+ #path(key: string) {
62
+ return path.join(
63
+ this.#dir,
64
+ sanitizeFilename(this.#filename(key) + this.ext)
65
+ );
66
+ }
67
+ async get(key: string) {
68
+ // read memory
69
+ const memCached = this.#cache.get(key);
70
+ if (memCached) {
71
+ // console.log("memory cache hit but expired", key, cached.expires, Date.now());
72
+ if (memCached.expires && memCached.expires < Date.now()) {
73
+ await this.delete(key);
74
+ } else {
75
+ return memCached.value;
76
+ }
77
+ }
78
+ // read file cache
79
+ const path = this.#path(key);
80
+ const stats = await stat(path).catch(() => null);
81
+ if (!stats) return undefined; // stat not found
82
+ const expires = +stats.mtime;
83
+ if (expires !== 0) {
84
+ const expired = +stats.mtime < +Date.now();
85
+ if (expired) {
86
+ // console.log("file cache hit expired", key, expires, Date.now(), expired);
87
+ await this.delete(key);
88
+ return undefined;
89
+ }
90
+ }
91
+ return await readFile(path, "utf8").catch(() => undefined);
92
+ }
93
+ async set(key: string, value: Value, ttl?: number) {
94
+ if (!value) return await this.delete(key);
95
+ // const { value, expires } = JSON.parse(stored) as DeserializedData<Value>;
96
+ const expires = ttl ? Date.now() + ttl : 0;
97
+ // save to memory
98
+ this.#cache.set(key, { value, expires });
99
+ // save to file
100
+ await this.#ready;
101
+ // console.log({ key, value, expires });
102
+ await mkdir(this.#dir, { recursive: true }).catch(() => {});
103
+ await writeFile(this.#path(key), value); // create a expired file
104
+ await utimes(this.#path(key), new Date(), new Date(expires ?? 0)); // set expires time as mtime (0 as never expired)
105
+ return true;
106
+ }
107
+ async delete(key: string) {
108
+ // delete memory
109
+ this.#cache.delete(key);
110
+ await rm(this.#path(key), { force: true });
111
+ return true;
112
+ }
113
+ async clear() {
114
+ await rm(this.#dir, { recursive: true }).catch(() => void 0);
115
+ }
116
+ async has(key: string) {
117
+ return undefined !== (await this.get(key));
118
+ }
119
+
120
+ // Save expires into mtime, and value into file
121
+ /** @deprecated use KeyvDirStoreJSON */
122
+ static serialize({ value }: DeserializedData<any>): string {
123
+ return JSON.stringify(value, null, 2);
124
+ }
125
+ /** @deprecated use KeyvDirStoreJSON */
126
+ static deserialize(str: string): DeserializedData<any> {
127
+ return { value: JSON.parse(str), expires: undefined };
128
+ }
129
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keyv-dir-store",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "author": "snomiao <snomiao@gmail.com>",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,8 @@
17
17
  "build": "bun build index.ts --outdir=dist --target=bun",
18
18
  "prerelease": "bun run build && bun run test",
19
19
  "release": "bunx standard-version && git push --follow-tags && npm publish",
20
- "test": "bun test"
20
+ "test": "bun test",
21
+ "prepare": "husky"
21
22
  },
22
23
  "dependencies": {
23
24
  "@types/node": "^20.14.2",
@@ -29,6 +30,8 @@
29
30
  "@types/bun": "^1.1.3",
30
31
  "@types/jest": "^29.5.12",
31
32
  "@types/md5": "^2.3.5",
33
+ "husky": "^9.1.7",
34
+ "semantic-release": "^24.2.1",
32
35
  "typescript": "^5.4.5"
33
36
  },
34
37
  "peerDependencies": {