keyv-dir-store 0.0.9 → 1.0.0

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.
Files changed (4) hide show
  1. package/README.md +199 -23
  2. package/dist/index.js +35 -11
  3. package/index.ts +49 -19
  4. package/package.json +9 -9
package/README.md CHANGED
@@ -1,41 +1,217 @@
1
1
  # Keyv Directory Store
2
2
 
3
- High performance Filesystem Keyv Store, caches each key-value pair into a $key.json. and more! *.JSON, *.YAML, *.CSV is also avaliable.
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
- ## Usages
5
+ [![npm version](https://badge.fury.io/js/keyv-dir-store.svg)](https://www.npmjs.com/package/keyv-dir-store)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ This package provides a filesystem-based storage adapter for [Keyv](https://github.com/jaredwray/keyv), storing each key-value pair in individual files with support for various formats.
9
+
10
+ ## Features
11
+
12
+ - ✅ Persistent storage using the filesystem
13
+ - ✅ Individual files per key-value pair
14
+ - ✅ Support customize serialize/deserialize, you can store your data into JSON, YAML, CSV, and TSV formats
15
+ - ✅ TTL (Time-To-Live) support using file modification times
16
+ - ✅ In-memory caching for improved performance
17
+ - ✅ Full compatibility with Keyv API
18
+ - ✅ Customizable file naming and extensions
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ # Using npm
24
+ npm install keyv keyv-dir-store
25
+
26
+ # Using yarn
27
+ yarn add keyv keyv-dir-store
28
+
29
+ # Using pnpm
30
+ pnpm add keyv keyv-dir-store
31
+
32
+ # Using bun
33
+ bun add keyv keyv-dir-store
34
+ ```
35
+
36
+ ## Usage Examples
37
+
38
+ ### Basic Usage
6
39
 
7
40
  ```ts
8
- // Default: Store each value with JSON in format "{value: ..., expires: ...}"
9
- new Keyv({
10
- store: new KeyvDirStore(".cache/kv")
41
+ import Keyv from "keyv";
42
+ import { KeyvDirStore } from "keyv-dir-store";
43
+
44
+ // Default: Store each value with JSON
45
+ const keyv = new Keyv({
46
+ store: new KeyvDirStore(".cache/kv"),
11
47
  });
12
48
 
13
- // Store each object list with CSV
14
- new Keyv({
15
- store: new KeyvDirStore(".cache/kv", { ext: ".csv" }),
49
+ // Set a value (never expires)
50
+ await keyv.set("key1", "value1");
51
+
52
+ // Set a value with TTL (expires after 1 day)
53
+ await keyv.set("key2", "value2", 86400000);
54
+
55
+ // Get a value
56
+ const value = await keyv.get("key1");
57
+
58
+ // Check if a key exists
59
+ const exists = await keyv.has("key1");
60
+
61
+ // Delete a key
62
+ await keyv.delete("key1");
63
+
64
+ // Clear all keys
65
+ await keyv.clear();
66
+ ```
67
+
68
+ ### Format-Specific Examples
69
+
70
+ #### Store with JSON (using provided helper)
71
+
72
+ ```ts
73
+ import Keyv from "keyv";
74
+ import { KeyvDirStore } from "keyv-dir-store";
75
+ import { KeyvDirStoreAsJSON } from "keyv-dir-store/KeyvDirStoreAsJSON";
76
+
77
+ const keyv = new Keyv({
78
+ store: new KeyvDirStore(".cache/kv", { suffix: ".json" }),
79
+ ...KeyvDirStoreAsJSON,
80
+ });
81
+ ```
82
+
83
+ #### Store with YAML (using provided helper)
84
+
85
+ ```ts
86
+ import Keyv from "keyv";
87
+ import { KeyvDirStore } from "keyv-dir-store";
88
+ import { KeyvDirStoreAsYaml } from "keyv-dir-store/KeyvDirStoreAsYaml";
89
+
90
+ const keyv = new Keyv({
91
+ store: new KeyvDirStore(".cache/kv", { suffix: ".yaml" }),
92
+ ...KeyvDirStoreAsYaml,
93
+ });
94
+ ```
95
+
96
+ #### Store Object Lists with CSV
97
+
98
+ ```ts
99
+ import Keyv from "keyv";
100
+ import { KeyvDirStore } from "keyv-dir-store";
101
+ import * as d3 from "d3";
102
+
103
+ const keyv = new Keyv({
104
+ store: new KeyvDirStore(".cache/kv", { suffix: ".csv" }),
16
105
  serialize: ({ value }) => d3.csvFormat(value),
17
106
  deserialize: (str) => ({ value: d3.csvParse(str), expires: undefined }),
18
107
  });
108
+ ```
19
109
 
20
- // Store each object list with TSV
21
- new Keyv({
22
- store: new KeyvDirStore(".cache/kv", { ext: ".tsv" }),
110
+ #### Store Object Lists with TSV
111
+
112
+ ```ts
113
+ import Keyv from "keyv";
114
+ import { KeyvDirStore } from "keyv-dir-store";
115
+ import * as d3 from "d3";
116
+
117
+ const keyv = new Keyv({
118
+ store: new KeyvDirStore(".cache/kv", { suffix: ".tsv" }),
23
119
  serialize: ({ value }) => d3.tsvFormat(value),
24
120
  deserialize: (str) => ({ value: d3.tsvParse(str), expires: undefined }),
25
121
  });
122
+ ```
26
123
 
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
- });
124
+ ## API Reference
125
+
126
+ ### `new KeyvDirStore(directory, options?)`
127
+
128
+ Creates a new KeyvDirStore instance.
129
+
130
+ #### Parameters
131
+
132
+ - `directory` (string): The directory path where files will be stored
133
+ - `options` (object, optional):
134
+ - `cache` (Map, optional): Custom cache Map to use
135
+ - `filename` (function, optional): Custom filename generator function
136
+ - `prefix` (string, optional): Path prefix prepended to every key (e.g. `'data/'`)
137
+ - `suffix` (string, optional): Path suffix appended to every key (default: `.json`)
33
138
 
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 }),
139
+ #### Example with Custom Options
140
+
141
+ ```ts
142
+ import Keyv from "keyv";
143
+ import { KeyvDirStore } from "keyv-dir-store";
144
+
145
+ const keyv = new Keyv({
146
+ store: new KeyvDirStore(".cache/kv", {
147
+ // Custom file suffix/extension
148
+ suffix: ".data",
149
+
150
+ // Custom filename generator
151
+ filename: (key) => `custom-prefix-${key}`,
152
+ }),
39
153
  });
154
+ ```
155
+
156
+ #### Mirror KeyvGithub paths (for use with keyv-nest)
157
+
158
+ Use the same prefix/suffix as KeyvGithub with `filename: (k) => k` for raw paths:
159
+
160
+ ```ts
161
+ import KeyvNest from "keyv-nest";
162
+ import { KeyvDirStore } from "keyv-dir-store";
163
+ import KeyvGithub from "keyv-github";
164
+
165
+ const prefix = "data/";
166
+ const suffix = ".json";
167
+
168
+ const store = KeyvNest(
169
+ new KeyvDirStore("./cache", {
170
+ prefix,
171
+ suffix,
172
+ filename: (k) => k, // use key as-is, no hashing
173
+ }),
174
+ new KeyvGithub("owner/repo", { client, prefix, suffix })
175
+ );
176
+
177
+ // key "foo" -> ./cache/data/foo.json (local) and data/foo.json (GitHub)
178
+ ```
179
+
180
+ ## How It Works
181
+
182
+ 1. Each key-value pair is stored in a separate file on disk
183
+ 2. The key is used to generate a filename (with sanitization)
184
+ 3. The value is serialized using the specified format
185
+ 4. TTL information is stored in the file's modification time
186
+ 5. An in-memory cache is used to improve performance
187
+
188
+ > **Warning**: TTL is stored using file mtime, which may be modified by other programs (backup tools, sync services, file explorers, etc.). For reliable TTL behavior, use the standard Keyv wrapper which stores expiry in the serialized value itself:
189
+ > ```ts
190
+ > // Keyv handles TTL internally - safe from mtime changes
191
+ > const keyv = new Keyv({ store: new KeyvDirStore("./cache") });
192
+ > await keyv.set("key", "value", 60000); // TTL stored in value, not mtime
193
+ > ```
194
+
195
+ ## See Also
196
+
197
+ Other Keyv storage adapters by the same author:
198
+
199
+ - [keyv-github](https://github.com/snomiao/keyv-github) — GitHub repository adapter; each key is a file, commits are writes
200
+ - [keyv-sqlite](https://github.com/snomiao/keyv-sqlite) — SQLite storage adapter
201
+ - [keyv-mongodb-store](https://github.com/snomiao/keyv-mongodb-store) — MongoDB storage adapter
202
+ - [keyv-nedb-store](https://github.com/snomiao/keyv-nedb-store) — NeDB embedded file-based adapter
203
+ - [keyv-cache-proxy](https://github.com/snomiao/keyv-cache-proxy) — transparent caching proxy that wraps any object
204
+ - [keyv-nest](https://github.com/snomiao/keyv-nest) — hierarchical multi-layer caching adapter
205
+
206
+ ## License
207
+
208
+ MIT © [snomiao](https://github.com/snomiao)
209
+
210
+ ## Contributing
211
+
212
+ Contributions, issues, and feature requests are welcome!
213
+ Feel free to check [issues page](https://github.com/snomiao/keyv-dir-store/issues).
214
+
215
+ ## Acknowledgements
40
216
 
41
- ```
217
+ This package is built on top of the excellent [Keyv](https://github.com/jaredwray/keyv) library.
package/dist/index.js CHANGED
@@ -4,15 +4,29 @@ var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
7
12
  var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
8
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
9
21
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
22
  for (let key of __getOwnPropNames(mod))
11
23
  if (!__hasOwnProp.call(to, key))
12
24
  __defProp(to, key, {
13
- get: () => mod[key],
25
+ get: __accessProp.bind(mod, key),
14
26
  enumerable: true
15
27
  });
28
+ if (canCache)
29
+ cache.set(mod, to);
16
30
  return to;
17
31
  };
18
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -330,18 +344,22 @@ class KeyvDirStore {
330
344
  #cache;
331
345
  #ready;
332
346
  #filename;
333
- ext = ".json";
347
+ opts = {};
348
+ namespace;
349
+ prefix;
350
+ suffix;
334
351
  constructor(dir, {
335
352
  cache = new Map,
336
353
  filename,
337
- ext
354
+ prefix,
355
+ suffix
338
356
  } = {}) {
339
- this.#ready = mkdir(dir, { recursive: true }).catch(() => {
340
- });
357
+ this.#ready = mkdir(dir, { recursive: true }).catch(() => {});
341
358
  this.#cache = cache;
342
359
  this.#dir = dir;
343
360
  this.#filename = filename ?? this.#defaultFilename;
344
- this.ext = ext ?? this.ext;
361
+ this.prefix = prefix ?? "";
362
+ this.suffix = suffix ?? ".json";
345
363
  }
346
364
  #defaultFilename(key) {
347
365
  const readableName = import_sanitize_filename.default(key).slice(0, 16);
@@ -350,7 +368,10 @@ class KeyvDirStore {
350
368
  return name;
351
369
  }
352
370
  #path(key) {
353
- return path.join(this.#dir, import_sanitize_filename.default(this.#filename(key) + this.ext));
371
+ const filename = this.#filename(key);
372
+ const safeFilename = import_sanitize_filename.default(filename + this.suffix);
373
+ const relativePath = this.prefix + safeFilename;
374
+ return path.join(this.#dir, relativePath);
354
375
  }
355
376
  async get(key) {
356
377
  const memCached = this.#cache.get(key);
@@ -383,10 +404,10 @@ class KeyvDirStore {
383
404
  const expires = ttl ? Date.now() + ttl : 0;
384
405
  this.#cache.set(key, { value, expires });
385
406
  await this.#ready;
386
- await mkdir(this.#dir, { recursive: true }).catch(() => {
387
- });
388
- await writeFile(this.#path(key), value);
389
- await utimes(this.#path(key), new Date, new Date(expires ?? 0));
407
+ const filePath = this.#path(key);
408
+ await mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
409
+ await writeFile(filePath, value);
410
+ await utimes(filePath, new Date, new Date(expires ?? 0));
390
411
  return true;
391
412
  }
392
413
  async delete(key) {
@@ -408,6 +429,9 @@ class KeyvDirStore {
408
429
  static deserialize(str) {
409
430
  return { value: JSON.parse(str), expires: undefined };
410
431
  }
432
+ on(_event, _listener) {
433
+ return this;
434
+ }
411
435
  }
412
436
  export {
413
437
  KeyvDirStore
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DeserializedData, default as Keyv } from "keyv";
1
+ import type { DeserializedData, KeyvStoreAdapter } from "keyv";
2
2
  import md5 from "md5";
3
3
  import { mkdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
4
4
  import path from "path";
@@ -8,9 +8,13 @@ type CacheMap<Value> = Map<string, DeserializedData<Value>>;
8
8
  /**
9
9
  * KeyvDirStore is a Keyv.Store<string> implementation that stores data in files.
10
10
  *
11
+ * **Warning**: TTL is stored in file mtime, which may be modified by other programs
12
+ * (backup tools, sync services, etc.). For reliable TTL, wrap this store with Keyv:
13
+ * `new Keyv({ store: new KeyvDirStore(...) })` - Keyv stores expiry in the value itself.
14
+ *
11
15
  * learn more [README](./README.md)
12
16
  *
13
- * @example
17
+ * @example Basic usage
14
18
  * const kv = new Keyv<number | string | { obj: boolean }>({
15
19
  * store: new KeyvDirStore("cache/test"),
16
20
  * deserialize: KeyvDirStore.deserialize,
@@ -21,13 +25,28 @@ type CacheMap<Value> = Map<string, DeserializedData<Value>>;
21
25
  * await kv.set("b", 1234); // never expired
22
26
  * expect(await kv.get("b")).toEqual(1234);
23
27
  *
28
+ * @example Mirror KeyvGithub paths (for use with keyv-nest)
29
+ * // Use same prefix/suffix as KeyvGithub, with filename: (k) => k for raw paths
30
+ * const dirStore = new KeyvDirStore("./cache", {
31
+ * prefix: "data/",
32
+ * suffix: ".json",
33
+ * filename: (k) => k, // use key as-is, no hashing
34
+ * });
35
+ * // Now dirStore uses same paths as KeyvGithub:
36
+ * // key "foo" -> ./cache/data/foo.json (local) and data/foo.json (GitHub)
37
+ *
24
38
  */
25
- export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
39
+ export class KeyvDirStore<Value extends string> implements KeyvStoreAdapter {
26
40
  #dir: string;
27
41
  #cache: CacheMap<Value>;
28
42
  #ready: Promise<unknown>;
29
43
  #filename: (key: string) => string;
30
- ext = ".json";
44
+ opts: Record<string, unknown> = {};
45
+ namespace?: string;
46
+ /** Path prefix prepended to every key (e.g. 'data/'). Defaults to ''. */
47
+ readonly prefix: string;
48
+ /** Path suffix appended to every key (e.g. '.json'). Defaults to '.json'. */
49
+ readonly suffix: string;
31
50
  constructor(
32
51
  /** dir to cache store
33
52
  * WARN: dont share this dir with other purpose
@@ -37,18 +56,23 @@ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
37
56
  {
38
57
  cache = new Map(),
39
58
  filename,
40
- ext,
59
+ prefix,
60
+ suffix,
41
61
  }: {
42
62
  cache?: CacheMap<Value>;
43
63
  filename?: (key: string) => string;
44
- ext?: string;
45
- } = {}
64
+ /** Path prefix prepended to every key (e.g. 'data/'). Use with filename: (k) => k for raw paths. */
65
+ prefix?: string;
66
+ /** Path suffix appended to every key (e.g. '.json'). Defaults to '.json'. Use with filename: (k) => k for raw paths. */
67
+ suffix?: string;
68
+ } = {},
46
69
  ) {
47
70
  this.#ready = mkdir(dir, { recursive: true }).catch(() => {});
48
71
  this.#cache = cache;
49
72
  this.#dir = dir;
50
73
  this.#filename = filename ?? this.#defaultFilename;
51
- this.ext = ext ?? this.ext;
74
+ this.prefix = prefix ?? "";
75
+ this.suffix = suffix ?? ".json";
52
76
  }
53
77
  #defaultFilename(key: string) {
54
78
  // use dir as hash salt to avoid collisions
@@ -58,12 +82,13 @@ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
58
82
  return name;
59
83
  }
60
84
  #path(key: string) {
61
- return path.join(
62
- this.#dir,
63
- sanitizeFilename(this.#filename(key) + this.ext)
64
- );
85
+ const filename = this.#filename(key);
86
+ // Sanitize filename+suffix for safety; prefix can have slashes for nested paths
87
+ const safeFilename = sanitizeFilename(filename + this.suffix);
88
+ const relativePath = this.prefix + safeFilename;
89
+ return path.join(this.#dir, relativePath);
65
90
  }
66
- async get(key: string) {
91
+ async get<T = Value>(key: string): Promise<T | undefined> {
67
92
  // read memory
68
93
  const memCached = this.#cache.get(key);
69
94
  if (memCached) {
@@ -71,7 +96,7 @@ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
71
96
  if (memCached.expires && memCached.expires < Date.now()) {
72
97
  await this.delete(key);
73
98
  } else {
74
- return memCached.value;
99
+ return memCached.value as T;
75
100
  }
76
101
  }
77
102
  // read file cache
@@ -87,7 +112,7 @@ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
87
112
  return undefined;
88
113
  }
89
114
  }
90
- return await readFile(path, "utf8").catch(() => undefined);
115
+ return await readFile(path, "utf8").catch(() => undefined) as T | undefined;
91
116
  }
92
117
  async set(key: string, value: Value, ttl?: number) {
93
118
  if (!value) return await this.delete(key);
@@ -97,10 +122,11 @@ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
97
122
  this.#cache.set(key, { value, expires });
98
123
  // save to file
99
124
  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)
125
+ const filePath = this.#path(key);
126
+ // create parent directories for nested paths (e.g. data/sub/key.json)
127
+ await mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
128
+ await writeFile(filePath, value); // create a expired file
129
+ await utimes(filePath, new Date(), new Date(expires ?? 0)); // set expires time as mtime (0 as never expired)
104
130
  return true;
105
131
  }
106
132
  async delete(key: string) {
@@ -124,4 +150,8 @@ export class KeyvDirStore<Value extends string> implements Keyv.Store<string> {
124
150
  static deserialize(str: string): DeserializedData<any> {
125
151
  return { value: JSON.parse(str), expires: undefined };
126
152
  }
153
+ // IEventEmitter implementation (required by KeyvStoreAdapter)
154
+ on(_event: string, _listener: (...args: any[]) => void): this {
155
+ return this;
156
+ }
127
157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keyv-dir-store",
3
- "version": "0.0.9",
3
+ "version": "1.0.0",
4
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
5
  "keywords": [
6
6
  "keyv",
@@ -37,20 +37,20 @@
37
37
  "test": "bun test"
38
38
  },
39
39
  "dependencies": {
40
- "@types/node": "^20.14.2",
40
+ "@types/node": "^25.3.3",
41
41
  "md5": "^2.3.0",
42
42
  "sanitize-filename": "^1.6.3",
43
- "yaml": "^2.4.5"
43
+ "yaml": "^2.8.2"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/bun": "^1.1.17",
47
- "@types/jest": "^29.5.14",
48
- "@types/md5": "^2.3.5",
46
+ "@types/bun": "^1.3.9",
47
+ "@types/jest": "^30.0.0",
48
+ "@types/md5": "^2.3.6",
49
49
  "husky": "^9.1.7",
50
- "semantic-release": "^24.2.1",
51
- "typescript": "^5.7.3"
50
+ "semantic-release": "^25.0.3",
51
+ "typescript": "^5.9.3"
52
52
  },
53
53
  "peerDependencies": {
54
- "keyv": "^4.5.4"
54
+ "keyv": "^5.6.0"
55
55
  }
56
56
  }