keyv-dir-store 0.0.6 → 0.0.7
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 -0
- package/KeyvDirStoreAsYaml.ts +11 -0
- package/LICENSE +21 -21
- package/README.md +33 -33
- package/dist/index.js +9 -8
- package/{index.test.ts → index.spec.ts} +67 -55
- package/index.ts +120 -118
- package/package.json +3 -2
- package/CHANGELOG.md +0 -31
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +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
|
+
};
|
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,34 @@
|
|
|
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
|
+
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
|
+
|
|
34
34
|
```
|
package/dist/index.js
CHANGED
|
@@ -343,7 +343,7 @@ class KeyvDirStore {
|
|
|
343
343
|
this.ext = ext ?? this.ext;
|
|
344
344
|
}
|
|
345
345
|
#defaultFilename(key) {
|
|
346
|
-
const readableName = import_sanitize_filename.default(key).slice(
|
|
346
|
+
const readableName = import_sanitize_filename.default(key).slice(0, 16);
|
|
347
347
|
const hashName = import_md5.default(key + "+SALT-poS1djRa4M2jXsWi").slice(0, 16);
|
|
348
348
|
const name = `${readableName}-${hashName}`;
|
|
349
349
|
return name;
|
|
@@ -352,15 +352,16 @@ class KeyvDirStore {
|
|
|
352
352
|
return path.join(this.#dir, import_sanitize_filename.default(this.#filename(key) + this.ext));
|
|
353
353
|
}
|
|
354
354
|
async get(key) {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
if (
|
|
355
|
+
const memCached = this.#cache.get(key);
|
|
356
|
+
if (memCached) {
|
|
357
|
+
if (memCached.expires && memCached.expires < Date.now()) {
|
|
358
358
|
await this.delete(key);
|
|
359
359
|
} else {
|
|
360
|
-
return
|
|
360
|
+
return memCached.value;
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
-
const
|
|
363
|
+
const path2 = this.#path(key);
|
|
364
|
+
const stats = await stat(path2).catch(() => null);
|
|
364
365
|
if (!stats)
|
|
365
366
|
return;
|
|
366
367
|
const expires = +stats.mtime;
|
|
@@ -371,7 +372,7 @@ class KeyvDirStore {
|
|
|
371
372
|
return;
|
|
372
373
|
}
|
|
373
374
|
}
|
|
374
|
-
return await readFile(
|
|
375
|
+
return await readFile(path2, "utf8").catch(() => {
|
|
375
376
|
return;
|
|
376
377
|
});
|
|
377
378
|
}
|
|
@@ -381,6 +382,7 @@ class KeyvDirStore {
|
|
|
381
382
|
const expires = ttl ? Date.now() + ttl : 0;
|
|
382
383
|
this.#cache.set(key, { value, expires });
|
|
383
384
|
await this.#ready;
|
|
385
|
+
await mkdir(this.#dir, { recursive: true });
|
|
384
386
|
await writeFile(this.#path(key), value);
|
|
385
387
|
await utimes(this.#path(key), new Date, new Date(expires ?? 0));
|
|
386
388
|
return true;
|
|
@@ -394,7 +396,6 @@ class KeyvDirStore {
|
|
|
394
396
|
await rm(this.#dir, { recursive: true }).catch(() => {
|
|
395
397
|
return;
|
|
396
398
|
});
|
|
397
|
-
await mkdir(this.#dir, { recursive: true });
|
|
398
399
|
}
|
|
399
400
|
async has(key) {
|
|
400
401
|
return await this.get(key) !== undefined;
|
|
@@ -1,55 +1,67 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
await kv.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
expect(
|
|
18
|
-
await kv.
|
|
19
|
-
expect(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
expect(await
|
|
28
|
-
|
|
29
|
-
expect(await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
37
|
-
await
|
|
38
|
-
await
|
|
39
|
-
expect(await
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
expect(await
|
|
53
|
-
|
|
54
|
-
expect(await
|
|
55
|
-
});
|
|
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,118 +1,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
|
-
* @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 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(
|
|
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
|
|
61
|
-
if (
|
|
62
|
-
// console.log("memory cache hit but expired", key, cached.expires, Date.now());
|
|
63
|
-
if (
|
|
64
|
-
await this.delete(key);
|
|
65
|
-
} else {
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// read file cache
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return await readFile(
|
|
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
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
await
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keyv-dir-store",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"author": "snomiao <snomiao@gmail.com>",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@types/node": "^20.14.2",
|
|
24
24
|
"md5": "^2.3.0",
|
|
25
|
-
"sanitize-filename": "^1.6.3"
|
|
25
|
+
"sanitize-filename": "^1.6.3",
|
|
26
|
+
"yaml": "^2.4.5"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/bun": "^1.1.3",
|
package/CHANGELOG.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
|
-
|
|
5
|
-
### [0.0.6](https://github.com/snomiao/keyv-dir-store/compare/v0.0.5...v0.0.6) (2024-06-27)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
### Bug Fixes
|
|
9
|
-
|
|
10
|
-
* - [`fs.utimes` doesn't support `number` or `string` on Windows · Issue [#33227](https://github.com/snomiao/keyv-dir-store/issues/33227) · nodejs/node]( https://github.com/nodejs/node/issues/33227 ) ([ff4981c](https://github.com/snomiao/keyv-dir-store/commit/ff4981cde2564eb9e49350d97149653d0066f850))
|
|
11
|
-
|
|
12
|
-
### [0.0.5](https://github.com/snomiao/keyv-dir-store/compare/v0.0.4...v0.0.5) (2024-06-26)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
### Bug Fixes
|
|
16
|
-
|
|
17
|
-
* **main:** sanitize ([ae7b162](https://github.com/snomiao/keyv-dir-store/commit/ae7b162cb8835bccda3c81769009ac0a78c7fac0))
|
|
18
|
-
* **main:** stage ([86d9193](https://github.com/snomiao/keyv-dir-store/commit/86d9193f5aa095ac77d006f081a3b0e801c1327e))
|
|
19
|
-
|
|
20
|
-
### [0.0.4](https://github.com/snomiao/keyv-dir-store/compare/v0.0.3...v0.0.4) (2024-06-26)
|
|
21
|
-
|
|
22
|
-
### [0.0.3](https://github.com/snomiao/keyv-dir-store/compare/v0.0.2...v0.0.3) (2024-06-12)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### Features
|
|
26
|
-
|
|
27
|
-
* **kds:** init ([7ef5f68](https://github.com/snomiao/keyv-dir-store/commit/7ef5f68430657a81dcace597148c7641032ed6ab))
|
|
28
|
-
|
|
29
|
-
### [0.0.2](https://github.com/snomiao/keyv-dir-store/compare/v0.0.1...v0.0.2) (2024-06-12)
|
|
30
|
-
|
|
31
|
-
### 0.0.1 (2024-06-12)
|