keyv-dir-store 0.0.6 → 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.
@@ -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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Keyv Directory Store
2
2
 
3
- Store key into a file per value
3
+ High performance Filesystem Keyv Store, caches each key-value pair into a $key.json. and more! *.JSON, *.YAML, *.CSV is also avaliable.
4
4
 
5
5
  ## Usages
6
6
 
@@ -10,13 +10,20 @@ new Keyv({
10
10
  store: new KeyvDirStore(".cache/kv")
11
11
  });
12
12
 
13
- // Store each value with CSV
13
+ // Store each object list with CSV
14
14
  new Keyv({
15
15
  store: new KeyvDirStore(".cache/kv", { ext: ".csv" }),
16
16
  serialize: ({ value }) => d3.csvFormat(value),
17
17
  deserialize: (str) => ({ value: d3.csvParse(str), expires: undefined }),
18
18
  });
19
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
+
20
27
  // Store each value with YAML
21
28
  new Keyv({
22
29
  store: new KeyvDirStore(".cache/kv", { ext: ".json" }),
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,14 +336,15 @@ 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;
343
344
  this.ext = ext ?? this.ext;
344
345
  }
345
346
  #defaultFilename(key) {
346
- const readableName = import_sanitize_filename.default(key).slice(4, 16);
347
+ const readableName = import_sanitize_filename.default(key).slice(0, 16);
347
348
  const hashName = import_md5.default(key + "+SALT-poS1djRa4M2jXsWi").slice(0, 16);
348
349
  const name = `${readableName}-${hashName}`;
349
350
  return name;
@@ -352,15 +353,16 @@ class KeyvDirStore {
352
353
  return path.join(this.#dir, import_sanitize_filename.default(this.#filename(key) + this.ext));
353
354
  }
354
355
  async get(key) {
355
- const cached = this.#cache.get(key);
356
- if (cached) {
357
- if (cached.expires && cached.expires < Date.now()) {
356
+ const memCached = this.#cache.get(key);
357
+ if (memCached) {
358
+ if (memCached.expires && memCached.expires < Date.now()) {
358
359
  await this.delete(key);
359
360
  } else {
360
- return cached.value;
361
+ return memCached.value;
361
362
  }
362
363
  }
363
- const stats = await stat(this.#path(key)).catch(() => null);
364
+ const path2 = this.#path(key);
365
+ const stats = await stat(path2).catch(() => null);
364
366
  if (!stats)
365
367
  return;
366
368
  const expires = +stats.mtime;
@@ -371,7 +373,7 @@ class KeyvDirStore {
371
373
  return;
372
374
  }
373
375
  }
374
- return await readFile(this.#path(key), "utf8").catch(() => {
376
+ return await readFile(path2, "utf8").catch(() => {
375
377
  return;
376
378
  });
377
379
  }
@@ -381,6 +383,8 @@ class KeyvDirStore {
381
383
  const expires = ttl ? Date.now() + ttl : 0;
382
384
  this.#cache.set(key, { value, expires });
383
385
  await this.#ready;
386
+ await mkdir(this.#dir, { recursive: true }).catch(() => {
387
+ });
384
388
  await writeFile(this.#path(key), value);
385
389
  await utimes(this.#path(key), new Date, new Date(expires ?? 0));
386
390
  return true;
@@ -394,7 +398,6 @@ class KeyvDirStore {
394
398
  await rm(this.#dir, { recursive: true }).catch(() => {
395
399
  return;
396
400
  });
397
- await mkdir(this.#dir, { recursive: true });
398
401
  }
399
402
  async has(key) {
400
403
  return await this.get(key) !== undefined;
@@ -1,18 +1,28 @@
1
+ import { existsSync } from "fs";
1
2
  import Keyv from "keyv";
2
3
  import { KeyvDirStore } from ".";
4
+ import { KeyvDirStoreAsJSON } from "./KeyvDirStoreAsJSON";
3
5
 
4
6
  it("KeyvDirStore works", async () => {
5
7
  // store test
6
8
  const kv = new Keyv<number | string | { obj: boolean }>({
7
- store: new KeyvDirStore(".cache/test1"),
9
+ store: new KeyvDirStore(".cache/test1", { filename: (x) => x }),
10
+ namespace: "",
8
11
  deserialize: KeyvDirStore.deserialize,
9
12
  serialize: KeyvDirStore.serialize,
10
13
  });
11
14
  await kv.clear();
12
15
  await kv.set("a", 1234, -86400e3); // already expired
16
+
17
+ expect(existsSync(".cache/test1/a.json")).toEqual(true);
13
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);
14
22
  await kv.set("b", 1234); // never expired
23
+ expect(existsSync(".cache/test1/b.json")).toEqual(true);
15
24
  expect(await kv.get("b")).toEqual(1234);
25
+
16
26
  await kv.set("c", "b", 86400e3); // 1 day
17
27
  expect(await kv.get("c")).toEqual("b");
18
28
  await kv.set("d", { obj: false }, 86400e3); // obj store
@@ -20,19 +30,20 @@ it("KeyvDirStore works", async () => {
20
30
 
21
31
  // new instance with no cache Obj, to test file cache
22
32
  const kv2 = new Keyv<number | string | { obj: boolean }>({
23
- store: new KeyvDirStore(".cache/test1"),
24
- deserialize: KeyvDirStore.deserialize,
25
- serialize: KeyvDirStore.serialize,
33
+ store: new KeyvDirStore(".cache/test1", { filename: (x) => x }),
34
+ namespace: "",
35
+ ...KeyvDirStoreAsJSON,
26
36
  });
27
37
  expect(await kv2.get("a")).toEqual(undefined); // will delete file before get
28
38
  expect(await kv2.get("b")).toEqual(1234);
29
39
  expect(await kv2.get("c")).toEqual("b");
30
40
  expect(await kv2.get("d")).toEqual({ obj: false });
31
41
  });
32
- it("KeyvDirStore works with only store", async () => {
42
+ it("KeyvDirStore works without deserialize", async () => {
33
43
  // store test
34
44
  const kv = new Keyv<number | string | { obj: boolean }>({
35
- store: new KeyvDirStore(".cache/test2"),
45
+ store: new KeyvDirStore(".cache/test2", { filename: (x) => x }),
46
+ namespace: "",
36
47
  });
37
48
  await kv.clear();
38
49
  await kv.set("a", 1234, -86400e3); // already expired
@@ -46,7 +57,8 @@ it("KeyvDirStore works with only store", async () => {
46
57
 
47
58
  // new instance with no cache Obj, to test file cache
48
59
  const kv2 = new Keyv<number | string | { obj: boolean }>({
49
- store: new KeyvDirStore(".cache/test2"),
60
+ store: new KeyvDirStore(".cache/test2", { filename: (x) => x }),
61
+ namespace: "",
50
62
  });
51
63
  expect(await kv2.get("a")).toEqual(undefined); // will delete file before get
52
64
  expect(await kv2.get("b")).toEqual(1234);
package/index.ts CHANGED
@@ -3,10 +3,13 @@ import md5 from "md5";
3
3
  import { mkdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
4
4
  import path from "path";
5
5
  import sanitizeFilename from "sanitize-filename";
6
- type Value = any;
6
+
7
7
  type CacheMap<Value> = Map<string, DeserializedData<Value>>;
8
8
  /**
9
9
  * KeyvDirStore is a Keyv.Store<string> implementation that stores data in files.
10
+ *
11
+ * learn more [README](./README.md)
12
+ *
10
13
  * @example
11
14
  * const kv = new Keyv<number | string | { obj: boolean }>({
12
15
  * store: new KeyvDirStore("cache/test"),
@@ -17,14 +20,19 @@ type CacheMap<Value> = Map<string, DeserializedData<Value>>;
17
20
  * expect(await kv.get("a")).toEqual(undefined); // will delete file before get
18
21
  * await kv.set("b", 1234); // never expired
19
22
  * expect(await kv.get("b")).toEqual(1234);
23
+ *
20
24
  */
21
- export class KeyvDirStore implements Keyv.Store<string> {
25
+ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
22
26
  #dir: string;
23
27
  #cache: CacheMap<Value>;
24
28
  #ready: Promise<unknown>;
25
29
  #filename: (key: string) => string;
26
30
  ext = ".json";
27
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
+ */
28
36
  dir: string,
29
37
  {
30
38
  cache = new Map(),
@@ -36,15 +44,16 @@ export class KeyvDirStore implements Keyv.Store<string> {
36
44
  ext?: string;
37
45
  } = {}
38
46
  ) {
39
- this.#ready = mkdir(dir, { recursive: true });
47
+ this.#ready = mkdir(dir, { recursive: true }).catch(() => {});
40
48
  this.#cache = cache;
49
+
41
50
  this.#dir = dir;
42
51
  this.#filename = filename ?? this.#defaultFilename;
43
52
  this.ext = ext ?? this.ext;
44
53
  }
45
54
  #defaultFilename(key: string) {
46
55
  // use dir as hash salt to avoid collisions
47
- const readableName = sanitizeFilename(key).slice(4, 16);
56
+ const readableName = sanitizeFilename(key).slice(0, 16);
48
57
  const hashName = md5(key + "+SALT-poS1djRa4M2jXsWi").slice(0, 16);
49
58
  const name = `${readableName}-${hashName}`;
50
59
  return name;
@@ -57,17 +66,18 @@ export class KeyvDirStore implements Keyv.Store<string> {
57
66
  }
58
67
  async get(key: string) {
59
68
  // read memory
60
- const cached = this.#cache.get(key);
61
- if (cached) {
69
+ const memCached = this.#cache.get(key);
70
+ if (memCached) {
62
71
  // console.log("memory cache hit but expired", key, cached.expires, Date.now());
63
- if (cached.expires && cached.expires < Date.now()) {
72
+ if (memCached.expires && memCached.expires < Date.now()) {
64
73
  await this.delete(key);
65
74
  } else {
66
- return cached.value;
75
+ return memCached.value;
67
76
  }
68
77
  }
69
78
  // read file cache
70
- const stats = await stat(this.#path(key)).catch(() => null);
79
+ const path = this.#path(key);
80
+ const stats = await stat(path).catch(() => null);
71
81
  if (!stats) return undefined; // stat not found
72
82
  const expires = +stats.mtime;
73
83
  if (expires !== 0) {
@@ -78,8 +88,7 @@ export class KeyvDirStore implements Keyv.Store<string> {
78
88
  return undefined;
79
89
  }
80
90
  }
81
- // return this.#parse(await readFile(this.#path(key), "utf8"));
82
- return await readFile(this.#path(key), "utf8").catch(() => undefined);
91
+ return await readFile(path, "utf8").catch(() => undefined);
83
92
  }
84
93
  async set(key: string, value: Value, ttl?: number) {
85
94
  if (!value) return await this.delete(key);
@@ -90,8 +99,9 @@ export class KeyvDirStore implements Keyv.Store<string> {
90
99
  // save to file
91
100
  await this.#ready;
92
101
  // console.log({ key, value, expires });
102
+ await mkdir(this.#dir, { recursive: true }).catch(() => {});
93
103
  await writeFile(this.#path(key), value); // create a expired file
94
- await utimes(this.#path(key), new Date(), new Date(expires ?? 0)); // set a future expires time (0 as never expired)
104
+ await utimes(this.#path(key), new Date(), new Date(expires ?? 0)); // set expires time as mtime (0 as never expired)
95
105
  return true;
96
106
  }
97
107
  async delete(key: string) {
@@ -102,17 +112,18 @@ export class KeyvDirStore implements Keyv.Store<string> {
102
112
  }
103
113
  async clear() {
104
114
  await rm(this.#dir, { recursive: true }).catch(() => void 0);
105
- await mkdir(this.#dir, { recursive: true });
106
115
  }
107
116
  async has(key: string) {
108
117
  return undefined !== (await this.get(key));
109
118
  }
110
119
 
111
120
  // Save expires into mtime, and value into file
112
- static serialize({ value }: DeserializedData<Value>): string {
121
+ /** @deprecated use KeyvDirStoreJSON */
122
+ static serialize({ value }: DeserializedData<any>): string {
113
123
  return JSON.stringify(value, null, 2);
114
124
  }
115
- static deserialize(str: string): DeserializedData<Value> {
125
+ /** @deprecated use KeyvDirStoreJSON */
126
+ static deserialize(str: string): DeserializedData<any> {
116
127
  return { value: JSON.parse(str), expires: undefined };
117
128
  }
118
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keyv-dir-store",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "author": "snomiao <snomiao@gmail.com>",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,17 +17,21 @@
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",
24
25
  "md5": "^2.3.0",
25
- "sanitize-filename": "^1.6.3"
26
+ "sanitize-filename": "^1.6.3",
27
+ "yaml": "^2.4.5"
26
28
  },
27
29
  "devDependencies": {
28
30
  "@types/bun": "^1.1.3",
29
31
  "@types/jest": "^29.5.12",
30
32
  "@types/md5": "^2.3.5",
33
+ "husky": "^9.1.7",
34
+ "semantic-release": "^24.2.1",
31
35
  "typescript": "^5.4.5"
32
36
  },
33
37
  "peerDependencies": {
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)