@abtnode/db-cache 1.17.3-beta-20251118-061144-335cd35d → 1.17.3-beta-20251119-102907-28b69b76

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 (3) hide show
  1. package/dist/index.cjs +211 -82
  2. package/dist/index.mjs +206 -81
  3. package/package.json +4 -2
package/dist/index.cjs CHANGED
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const path = require('path');
3
+ const path$1 = require('path');
4
4
  const redis = require('redis');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const node_util = require('node:util');
5
9
  const sqlite3 = require('sqlite3');
6
- const fs = require('fs');
7
- const util = require('util');
10
+ const crypto = require('node:crypto');
11
+ const lockfile = require('proper-lockfile');
8
12
 
9
13
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
10
14
 
@@ -20,25 +24,44 @@ function _interopNamespaceCompat(e) {
20
24
  return n;
21
25
  }
22
26
 
27
+ const path__default = /*#__PURE__*/_interopDefaultCompat(path$1);
28
+ const fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
29
+ const os__namespace = /*#__PURE__*/_interopNamespaceCompat(os);
23
30
  const path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
24
- const path__default = /*#__PURE__*/_interopDefaultCompat(path);
25
31
  const sqlite3__default = /*#__PURE__*/_interopDefaultCompat(sqlite3);
26
- const fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
32
+ const crypto__namespace = /*#__PURE__*/_interopNamespaceCompat(crypto);
33
+ const lockfile__default = /*#__PURE__*/_interopDefaultCompat(lockfile);
27
34
 
28
- var __defProp$4 = Object.defineProperty;
29
- var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
30
- var __publicField$4 = (obj, key, value) => {
31
- __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
35
+ function ulid() {
36
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
37
+ let t = Date.now();
38
+ let timeStr = "";
39
+ for (let i = 0; i < 10; i++) {
40
+ timeStr = alphabet[t % 32] + timeStr;
41
+ t = Math.floor(t / 32);
42
+ }
43
+ let rand = Math.random().toString(32).substring(2);
44
+ while (rand.length < 16) {
45
+ rand += Math.random().toString(32).substring(2);
46
+ }
47
+ rand = rand.substring(0, 16).toUpperCase();
48
+ return timeStr + rand;
49
+ }
50
+
51
+ var __defProp$5 = Object.defineProperty;
52
+ var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
53
+ var __publicField$5 = (obj, key, value) => {
54
+ __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
32
55
  return value;
33
56
  };
34
57
  const _RedisAdapter = class _RedisAdapter {
35
58
  constructor() {
36
- __publicField$4(this, "opts", null);
37
- __publicField$4(this, "defaultTtl", 1e3 * 60 * 60);
38
- __publicField$4(this, "url", "");
39
- __publicField$4(this, "prefix", "");
40
- __publicField$4(this, "prefixKey", (key) => `${this.prefix}:${key}`);
41
- __publicField$4(this, "prefixKeyGroup", (key) => `${this.prefix}:group:${key}`);
59
+ __publicField$5(this, "opts", null);
60
+ __publicField$5(this, "defaultTtl", 1e3 * 60 * 60);
61
+ __publicField$5(this, "url", "");
62
+ __publicField$5(this, "prefix", "");
63
+ __publicField$5(this, "prefixKey", (key) => `${this.prefix}:${key}`);
64
+ __publicField$5(this, "prefixKeyGroup", (key) => `${this.prefix}:group:${key}`);
42
65
  }
43
66
  clearAll() {
44
67
  throw new Error("Method not implemented.");
@@ -56,10 +79,15 @@ const _RedisAdapter = class _RedisAdapter {
56
79
  _RedisAdapter.initPromises.set(
57
80
  this.url,
58
81
  (async () => {
59
- const cli = redis.createClient({ url: this.url });
82
+ const { url } = this;
83
+ if (!url) {
84
+ throw new Error("Redis URL is not set");
85
+ }
86
+ const cli = redis.createClient({ url });
60
87
  cli.on("error", console.error);
61
88
  await cli.connect();
62
- _RedisAdapter.clients.set(this.url, cli);
89
+ await cli.ping();
90
+ _RedisAdapter.clients.set(url, cli);
63
91
  })()
64
92
  );
65
93
  }
@@ -70,7 +98,11 @@ const _RedisAdapter = class _RedisAdapter {
70
98
  if (!this.url || !_RedisAdapter.clients.has(this.url)) {
71
99
  throw new Error("Redis not initialized");
72
100
  }
73
- return _RedisAdapter.clients.get(this.url);
101
+ const client = _RedisAdapter.clients.get(this.url);
102
+ if (!client) {
103
+ throw new Error("Redis client not found");
104
+ }
105
+ return client;
74
106
  }
75
107
  serialize(value) {
76
108
  return JSON.stringify({ v: value });
@@ -88,12 +120,21 @@ const _RedisAdapter = class _RedisAdapter {
88
120
  async set(key, value, { ttl, nx }) {
89
121
  const client = this.getClient();
90
122
  const storageKey = this.prefixKey(key);
91
- if (ttl && ttl > 0) {
92
- await client.set(storageKey, this.serialize(value), { PX: ttl, NX: nx });
123
+ const effectiveTtl = ttl !== void 0 ? ttl : this.defaultTtl;
124
+ let result;
125
+ if (effectiveTtl && effectiveTtl > 0) {
126
+ result = await client.set(storageKey, this.serialize(value), { PX: effectiveTtl, NX: nx });
93
127
  } else {
94
- await client.set(storageKey, this.serialize(value), { NX: nx });
128
+ result = await client.set(storageKey, this.serialize(value), { NX: nx });
95
129
  }
96
- return true;
130
+ if (result !== "OK") {
131
+ const storedValue = await client.get(storageKey);
132
+ if (storedValue === this.serialize(value)) {
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+ return result === "OK";
97
138
  }
98
139
  async get(key) {
99
140
  const client = this.getClient();
@@ -152,9 +193,11 @@ const _RedisAdapter = class _RedisAdapter {
152
193
  async close() {
153
194
  if (this.url && _RedisAdapter.clients.has(this.url)) {
154
195
  const client = _RedisAdapter.clients.get(this.url);
155
- await client.quit();
156
- _RedisAdapter.clients.delete(this.url);
157
- _RedisAdapter.initPromises.delete(this.url);
196
+ if (client) {
197
+ await client.quit();
198
+ _RedisAdapter.clients.delete(this.url);
199
+ _RedisAdapter.initPromises.delete(this.url);
200
+ }
158
201
  }
159
202
  }
160
203
  async flushAll() {
@@ -163,8 +206,8 @@ const _RedisAdapter = class _RedisAdapter {
163
206
  }
164
207
  };
165
208
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
- __publicField$4(_RedisAdapter, "clients", /* @__PURE__ */ new Map());
167
- __publicField$4(_RedisAdapter, "initPromises", /* @__PURE__ */ new Map());
209
+ __publicField$5(_RedisAdapter, "clients", /* @__PURE__ */ new Map());
210
+ __publicField$5(_RedisAdapter, "initPromises", /* @__PURE__ */ new Map());
168
211
  let RedisAdapter = _RedisAdapter;
169
212
 
170
213
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -193,6 +236,67 @@ async function withRetry(fn, {
193
236
  }
194
237
  }
195
238
 
239
+ var __defProp$4 = Object.defineProperty;
240
+ var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
241
+ var __publicField$4 = (obj, key, value) => {
242
+ __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
243
+ return value;
244
+ };
245
+ class FileLock {
246
+ // 默认 10 分钟
247
+ constructor(baseDir, defaultTtl) {
248
+ __publicField$4(this, "baseDir");
249
+ __publicField$4(this, "defaultTtl", 10 * 60 * 1e3);
250
+ this.baseDir = baseDir;
251
+ if (defaultTtl !== void 0) {
252
+ this.defaultTtl = defaultTtl;
253
+ }
254
+ }
255
+ getLockFilePath(storageKey) {
256
+ const lockDir = path__namespace.join(this.baseDir, "locks");
257
+ if (!fs__namespace.existsSync(lockDir)) {
258
+ fs__namespace.mkdirSync(lockDir, { recursive: true });
259
+ }
260
+ const safeName = crypto__namespace.createHash("sha256").update(storageKey).digest("hex");
261
+ return path__namespace.join(lockDir, `${safeName}.lock`);
262
+ }
263
+ /**
264
+ * acquire 返回 true 代表成功加锁
265
+ * 返回 false 代表有其他进程/线程持有锁
266
+ */
267
+ async acquire(storageKey, ttl) {
268
+ const lockPath = this.getLockFilePath(storageKey);
269
+ const effectiveTtl = ttl !== void 0 ? ttl : this.defaultTtl;
270
+ try {
271
+ try {
272
+ fs__namespace.writeFileSync(lockPath, "", { flag: "wx" });
273
+ } catch (_createErr) {
274
+ }
275
+ await lockfile__default.lock(lockPath, {
276
+ realpath: true,
277
+ stale: effectiveTtl,
278
+ // 超时自动释放
279
+ update: Math.floor(effectiveTtl / 3),
280
+ // 定期续租,避免长任务过期
281
+ retries: 0
282
+ });
283
+ return true;
284
+ } catch (_err) {
285
+ return false;
286
+ }
287
+ }
288
+ /**
289
+ * 释放锁
290
+ */
291
+ async release(storageKey) {
292
+ const lockPath = this.getLockFilePath(storageKey);
293
+ try {
294
+ await lockfile__default.unlock(lockPath);
295
+ } catch (_err) {
296
+ }
297
+ }
298
+ }
299
+
196
300
  var __defProp$3 = Object.defineProperty;
197
301
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
198
302
  var __publicField$3 = (obj, key, value) => {
@@ -229,11 +333,11 @@ async function initSqliteWithRetry(db) {
229
333
  await dbExec(
230
334
  db,
231
335
  `
232
- PRAGMA journal_mode = WAL;
233
- PRAGMA synchronous = normal;
234
- PRAGMA wal_autocheckpoint = 5000;
235
- PRAGMA busy_timeout = 5000;
236
- `
336
+ PRAGMA journal_mode = WAL;
337
+ PRAGMA synchronous = normal;
338
+ PRAGMA wal_autocheckpoint = 5000;
339
+ PRAGMA busy_timeout = 5000;
340
+ `
237
341
  );
238
342
  await dbRun(
239
343
  db,
@@ -245,7 +349,7 @@ async function initSqliteWithRetry(db) {
245
349
  expiresAt INTEGER,
246
350
  PRIMARY KEY (key, subKey)
247
351
  )
248
- `
352
+ `
249
353
  );
250
354
  } catch (err) {
251
355
  throw new Error(`SQLite init failed: ${err}`);
@@ -258,28 +362,36 @@ const _SqliteAdapter = class _SqliteAdapter {
258
362
  __publicField$3(this, "defaultTtl", 1e3 * 60 * 60);
259
363
  __publicField$3(this, "cleanupInterval", 5 * 60 * 1e3);
260
364
  __publicField$3(this, "dbPath", "");
365
+ __publicField$3(this, "fileLock", null);
261
366
  }
367
+ /* ------------------------------- init ------------------------------- */
262
368
  async ensure() {
263
369
  if (!this.dbPath) {
264
370
  this.dbPath = this.opts.sqlitePath === ":memory:" ? ":memory:" : path__namespace.resolve(this.opts.sqlitePath);
265
371
  this.prefix = this.opts.prefix;
266
372
  this.defaultTtl = this.opts.ttl;
267
373
  this.cleanupInterval = this.opts.cleanupInterval ?? 30 * 60 * 1e3;
374
+ if (this.dbPath !== ":memory:") {
375
+ const baseDir = path__namespace.dirname(this.dbPath);
376
+ this.fileLock = new FileLock(baseDir);
377
+ } else {
378
+ const randomId = Math.random().toString(36).substring(2, 15);
379
+ const baseDir = path__namespace.join(os__namespace.tmpdir(), `db-cache-locks-${randomId}`);
380
+ this.fileLock = new FileLock(baseDir);
381
+ }
268
382
  }
269
383
  if (_SqliteAdapter.clients.has(this.dbPath))
270
384
  return;
271
385
  if (!_SqliteAdapter.initPromises.has(this.dbPath)) {
272
- const dir = path__namespace.dirname(this.dbPath);
273
- if (!fs__namespace.existsSync(dir)) {
274
- fs__namespace.mkdirSync(dir, { recursive: true });
386
+ if (this.dbPath !== ":memory:") {
387
+ const dir = path__namespace.dirname(this.dbPath);
388
+ if (!fs__namespace.existsSync(dir)) {
389
+ fs__namespace.mkdirSync(dir, { recursive: true });
390
+ }
275
391
  }
276
392
  _SqliteAdapter.initPromises.set(
277
393
  this.dbPath,
278
394
  (async () => {
279
- const dbDir = path__namespace.dirname(this.dbPath);
280
- if (!fs__namespace.existsSync(dbDir)) {
281
- fs__namespace.mkdirSync(dbDir, { recursive: true });
282
- }
283
395
  const db = new sqlite3__default.Database(this.dbPath);
284
396
  await initSqliteWithRetry(db);
285
397
  _SqliteAdapter.clients.set(this.dbPath, db);
@@ -303,16 +415,12 @@ const _SqliteAdapter = class _SqliteAdapter {
303
415
  }
304
416
  return _SqliteAdapter.clients.get(this.dbPath);
305
417
  }
418
+ /* ------------------------------- cleanup ------------------------------- */
306
419
  async cleanup() {
307
420
  const client = this.getClient();
308
- await new Promise((res, rej) => {
309
- client.run(
310
- "DELETE FROM kvcache WHERE expiresAt IS NOT NULL AND expiresAt <= ?",
311
- [Date.now()],
312
- (err) => err ? rej(err) : res()
313
- );
314
- });
421
+ await dbRun(client, "DELETE FROM kvcache WHERE expiresAt IS NOT NULL AND expiresAt <= ?", [Date.now()]);
315
422
  }
423
+ /* ------------------------------- core ------------------------------- */
316
424
  prefixKey(key) {
317
425
  return `${this.prefix}:${key}`;
318
426
  }
@@ -323,8 +431,7 @@ const _SqliteAdapter = class _SqliteAdapter {
323
431
  if (raw === null || raw === void 0)
324
432
  return null;
325
433
  try {
326
- const obj = JSON.parse(raw);
327
- return obj.v;
434
+ return JSON.parse(raw).v;
328
435
  } catch {
329
436
  return null;
330
437
  }
@@ -334,33 +441,52 @@ const _SqliteAdapter = class _SqliteAdapter {
334
441
  const storageKey = this.prefixKey(key);
335
442
  const effectiveTtl = opts.ttl !== void 0 ? opts.ttl : this.defaultTtl;
336
443
  const expiresAt = effectiveTtl > 0 ? Date.now() + effectiveTtl : null;
337
- const subKey = "";
338
- if (opts.nx) {
339
- const exists = await this.has(key);
340
- if (exists)
444
+ let fileLocked = false;
445
+ if (opts.nx && this.fileLock) {
446
+ fileLocked = await this.fileLock.acquire(storageKey, effectiveTtl);
447
+ if (!fileLocked)
341
448
  return false;
342
449
  }
343
- await dbRun(
344
- client,
345
- `
346
- INSERT INTO kvcache (key, subKey, value, expiresAt)
347
- VALUES (?, ?, ?, ?)
348
- ON CONFLICT(key, subKey) DO UPDATE SET
349
- value = excluded.value,
350
- expiresAt = excluded.expiresAt
351
- `,
352
- [storageKey, subKey, this.serialize(value), expiresAt]
353
- );
354
- return true;
450
+ try {
451
+ if (opts.nx) {
452
+ const existing = await dbGet(
453
+ client,
454
+ "SELECT value FROM kvcache WHERE key = ? AND subKey = ?",
455
+ [storageKey, ""]
456
+ );
457
+ if (existing) {
458
+ return false;
459
+ }
460
+ }
461
+ await dbRun(
462
+ client,
463
+ `
464
+ INSERT INTO kvcache (key, subKey, value, expiresAt)
465
+ VALUES (?, ?, ?, ?)
466
+ ON CONFLICT(key, subKey) DO UPDATE SET
467
+ value = excluded.value,
468
+ expiresAt = excluded.expiresAt
469
+ `,
470
+ [storageKey, "", this.serialize(value), expiresAt]
471
+ );
472
+ if (fileLocked && this.fileLock) {
473
+ await this.fileLock.release(storageKey);
474
+ }
475
+ return true;
476
+ } catch (err) {
477
+ if (fileLocked && this.fileLock) {
478
+ await this.fileLock.release(storageKey);
479
+ }
480
+ throw err;
481
+ }
355
482
  }
356
483
  async get(key) {
357
484
  const client = this.getClient();
358
485
  const storageKey = this.prefixKey(key);
359
- const subKey = "";
360
486
  const row = await dbGet(
361
487
  client,
362
488
  'SELECT value, "expiresAt" FROM kvcache WHERE key = ? AND subKey = ?',
363
- [storageKey, subKey]
489
+ [storageKey, ""]
364
490
  );
365
491
  if (!row)
366
492
  return null;
@@ -374,15 +500,17 @@ const _SqliteAdapter = class _SqliteAdapter {
374
500
  const client = this.getClient();
375
501
  const storageKey = this.prefixKey(key);
376
502
  await dbRun(client, "DELETE FROM kvcache WHERE key = ?", [storageKey]);
503
+ if (this.fileLock) {
504
+ await this.fileLock.release(storageKey);
505
+ }
377
506
  }
378
507
  async has(key) {
379
508
  const client = this.getClient();
380
509
  const storageKey = this.prefixKey(key);
381
- const subKey = "";
382
510
  const row = await dbGet(
383
511
  client,
384
512
  'SELECT "expiresAt" FROM kvcache WHERE key = ? AND subKey = ?',
385
- [storageKey, subKey]
513
+ [storageKey, ""]
386
514
  );
387
515
  if (!row)
388
516
  return false;
@@ -392,6 +520,7 @@ const _SqliteAdapter = class _SqliteAdapter {
392
520
  }
393
521
  return true;
394
522
  }
523
+ /* ------------------------------- group APIs ------------------------------- */
395
524
  async groupSet(key, subKey, value, opts) {
396
525
  const client = this.getClient();
397
526
  const storageKey = this.prefixKey(key);
@@ -405,11 +534,11 @@ const _SqliteAdapter = class _SqliteAdapter {
405
534
  await dbRun(
406
535
  client,
407
536
  `
408
- INSERT INTO kvcache (key, subKey, value, expiresAt)
409
- VALUES (?, ?, ?, ?)
410
- ON CONFLICT(key, subKey) DO UPDATE SET
411
- value = excluded.value,
412
- expiresAt = excluded.expiresAt
537
+ INSERT INTO kvcache (key, subKey, value, expiresAt)
538
+ VALUES (?, ?, ?, ?)
539
+ ON CONFLICT(key, subKey) DO UPDATE SET
540
+ value = excluded.value,
541
+ expiresAt = excluded.expiresAt
413
542
  `,
414
543
  [storageKey, subKey, this.serialize(value), expiresAt]
415
544
  );
@@ -449,8 +578,12 @@ const _SqliteAdapter = class _SqliteAdapter {
449
578
  async groupDel(key, subKey) {
450
579
  const client = this.getClient();
451
580
  const storageKey = this.prefixKey(key);
452
- return dbRun(client, "DELETE FROM kvcache WHERE key = ? AND subKey = ?", [storageKey, subKey]);
581
+ await dbRun(client, "DELETE FROM kvcache WHERE key = ? AND subKey = ?", [storageKey, subKey]);
582
+ if (this.fileLock) {
583
+ await this.fileLock.release(storageKey);
584
+ }
453
585
  }
586
+ /* ------------------------------- misc ------------------------------- */
454
587
  async close() {
455
588
  if (this.dbPath) {
456
589
  const timer = _SqliteAdapter.cleanupTimers.get(this.dbPath);
@@ -468,7 +601,7 @@ const _SqliteAdapter = class _SqliteAdapter {
468
601
  }
469
602
  async flushAll() {
470
603
  const client = this.getClient();
471
- const run = util.promisify(client.run.bind(client));
604
+ const run = node_util.promisify(client.run.bind(client));
472
605
  await run("DELETE FROM kvcache");
473
606
  }
474
607
  };
@@ -730,12 +863,8 @@ class LockDBCache extends SingleFlightDBCache {
730
863
  }
731
864
  this._inFlight.set(key, Date.now());
732
865
  try {
733
- const exists = await this.has(key);
734
- if (!exists) {
735
- const result = await this.set(key, Date.now(), { ttl: this.defaultTtl, nx: true });
736
- return result;
737
- }
738
- return false;
866
+ const result = await this.set(key, ulid(), { ttl: this.defaultTtl, nx: true });
867
+ return result;
739
868
  } finally {
740
869
  this._inFlight.delete(key);
741
870
  for (const [key2, value] of this._inFlight.entries()) {
package/dist/index.mjs CHANGED
@@ -1,24 +1,43 @@
1
- import * as path from 'path';
2
- import path__default from 'path';
1
+ import path$1 from 'path';
3
2
  import { createClient } from 'redis';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { promisify } from 'node:util';
4
7
  import sqlite3 from 'sqlite3';
5
- import * as fs from 'fs';
6
- import { promisify } from 'util';
8
+ import * as crypto from 'node:crypto';
9
+ import lockfile from 'proper-lockfile';
7
10
 
8
- var __defProp$4 = Object.defineProperty;
9
- var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
- var __publicField$4 = (obj, key, value) => {
11
- __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
11
+ function ulid() {
12
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
13
+ let t = Date.now();
14
+ let timeStr = "";
15
+ for (let i = 0; i < 10; i++) {
16
+ timeStr = alphabet[t % 32] + timeStr;
17
+ t = Math.floor(t / 32);
18
+ }
19
+ let rand = Math.random().toString(32).substring(2);
20
+ while (rand.length < 16) {
21
+ rand += Math.random().toString(32).substring(2);
22
+ }
23
+ rand = rand.substring(0, 16).toUpperCase();
24
+ return timeStr + rand;
25
+ }
26
+
27
+ var __defProp$5 = Object.defineProperty;
28
+ var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
29
+ var __publicField$5 = (obj, key, value) => {
30
+ __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
12
31
  return value;
13
32
  };
14
33
  const _RedisAdapter = class _RedisAdapter {
15
34
  constructor() {
16
- __publicField$4(this, "opts", null);
17
- __publicField$4(this, "defaultTtl", 1e3 * 60 * 60);
18
- __publicField$4(this, "url", "");
19
- __publicField$4(this, "prefix", "");
20
- __publicField$4(this, "prefixKey", (key) => `${this.prefix}:${key}`);
21
- __publicField$4(this, "prefixKeyGroup", (key) => `${this.prefix}:group:${key}`);
35
+ __publicField$5(this, "opts", null);
36
+ __publicField$5(this, "defaultTtl", 1e3 * 60 * 60);
37
+ __publicField$5(this, "url", "");
38
+ __publicField$5(this, "prefix", "");
39
+ __publicField$5(this, "prefixKey", (key) => `${this.prefix}:${key}`);
40
+ __publicField$5(this, "prefixKeyGroup", (key) => `${this.prefix}:group:${key}`);
22
41
  }
23
42
  clearAll() {
24
43
  throw new Error("Method not implemented.");
@@ -36,10 +55,15 @@ const _RedisAdapter = class _RedisAdapter {
36
55
  _RedisAdapter.initPromises.set(
37
56
  this.url,
38
57
  (async () => {
39
- const cli = createClient({ url: this.url });
58
+ const { url } = this;
59
+ if (!url) {
60
+ throw new Error("Redis URL is not set");
61
+ }
62
+ const cli = createClient({ url });
40
63
  cli.on("error", console.error);
41
64
  await cli.connect();
42
- _RedisAdapter.clients.set(this.url, cli);
65
+ await cli.ping();
66
+ _RedisAdapter.clients.set(url, cli);
43
67
  })()
44
68
  );
45
69
  }
@@ -50,7 +74,11 @@ const _RedisAdapter = class _RedisAdapter {
50
74
  if (!this.url || !_RedisAdapter.clients.has(this.url)) {
51
75
  throw new Error("Redis not initialized");
52
76
  }
53
- return _RedisAdapter.clients.get(this.url);
77
+ const client = _RedisAdapter.clients.get(this.url);
78
+ if (!client) {
79
+ throw new Error("Redis client not found");
80
+ }
81
+ return client;
54
82
  }
55
83
  serialize(value) {
56
84
  return JSON.stringify({ v: value });
@@ -68,12 +96,21 @@ const _RedisAdapter = class _RedisAdapter {
68
96
  async set(key, value, { ttl, nx }) {
69
97
  const client = this.getClient();
70
98
  const storageKey = this.prefixKey(key);
71
- if (ttl && ttl > 0) {
72
- await client.set(storageKey, this.serialize(value), { PX: ttl, NX: nx });
99
+ const effectiveTtl = ttl !== void 0 ? ttl : this.defaultTtl;
100
+ let result;
101
+ if (effectiveTtl && effectiveTtl > 0) {
102
+ result = await client.set(storageKey, this.serialize(value), { PX: effectiveTtl, NX: nx });
73
103
  } else {
74
- await client.set(storageKey, this.serialize(value), { NX: nx });
104
+ result = await client.set(storageKey, this.serialize(value), { NX: nx });
75
105
  }
76
- return true;
106
+ if (result !== "OK") {
107
+ const storedValue = await client.get(storageKey);
108
+ if (storedValue === this.serialize(value)) {
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ return result === "OK";
77
114
  }
78
115
  async get(key) {
79
116
  const client = this.getClient();
@@ -132,9 +169,11 @@ const _RedisAdapter = class _RedisAdapter {
132
169
  async close() {
133
170
  if (this.url && _RedisAdapter.clients.has(this.url)) {
134
171
  const client = _RedisAdapter.clients.get(this.url);
135
- await client.quit();
136
- _RedisAdapter.clients.delete(this.url);
137
- _RedisAdapter.initPromises.delete(this.url);
172
+ if (client) {
173
+ await client.quit();
174
+ _RedisAdapter.clients.delete(this.url);
175
+ _RedisAdapter.initPromises.delete(this.url);
176
+ }
138
177
  }
139
178
  }
140
179
  async flushAll() {
@@ -143,8 +182,8 @@ const _RedisAdapter = class _RedisAdapter {
143
182
  }
144
183
  };
145
184
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
- __publicField$4(_RedisAdapter, "clients", /* @__PURE__ */ new Map());
147
- __publicField$4(_RedisAdapter, "initPromises", /* @__PURE__ */ new Map());
185
+ __publicField$5(_RedisAdapter, "clients", /* @__PURE__ */ new Map());
186
+ __publicField$5(_RedisAdapter, "initPromises", /* @__PURE__ */ new Map());
148
187
  let RedisAdapter = _RedisAdapter;
149
188
 
150
189
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -173,6 +212,67 @@ async function withRetry(fn, {
173
212
  }
174
213
  }
175
214
 
215
+ var __defProp$4 = Object.defineProperty;
216
+ var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
217
+ var __publicField$4 = (obj, key, value) => {
218
+ __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
219
+ return value;
220
+ };
221
+ class FileLock {
222
+ // 默认 10 分钟
223
+ constructor(baseDir, defaultTtl) {
224
+ __publicField$4(this, "baseDir");
225
+ __publicField$4(this, "defaultTtl", 10 * 60 * 1e3);
226
+ this.baseDir = baseDir;
227
+ if (defaultTtl !== void 0) {
228
+ this.defaultTtl = defaultTtl;
229
+ }
230
+ }
231
+ getLockFilePath(storageKey) {
232
+ const lockDir = path.join(this.baseDir, "locks");
233
+ if (!fs.existsSync(lockDir)) {
234
+ fs.mkdirSync(lockDir, { recursive: true });
235
+ }
236
+ const safeName = crypto.createHash("sha256").update(storageKey).digest("hex");
237
+ return path.join(lockDir, `${safeName}.lock`);
238
+ }
239
+ /**
240
+ * acquire 返回 true 代表成功加锁
241
+ * 返回 false 代表有其他进程/线程持有锁
242
+ */
243
+ async acquire(storageKey, ttl) {
244
+ const lockPath = this.getLockFilePath(storageKey);
245
+ const effectiveTtl = ttl !== void 0 ? ttl : this.defaultTtl;
246
+ try {
247
+ try {
248
+ fs.writeFileSync(lockPath, "", { flag: "wx" });
249
+ } catch (_createErr) {
250
+ }
251
+ await lockfile.lock(lockPath, {
252
+ realpath: true,
253
+ stale: effectiveTtl,
254
+ // 超时自动释放
255
+ update: Math.floor(effectiveTtl / 3),
256
+ // 定期续租,避免长任务过期
257
+ retries: 0
258
+ });
259
+ return true;
260
+ } catch (_err) {
261
+ return false;
262
+ }
263
+ }
264
+ /**
265
+ * 释放锁
266
+ */
267
+ async release(storageKey) {
268
+ const lockPath = this.getLockFilePath(storageKey);
269
+ try {
270
+ await lockfile.unlock(lockPath);
271
+ } catch (_err) {
272
+ }
273
+ }
274
+ }
275
+
176
276
  var __defProp$3 = Object.defineProperty;
177
277
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
178
278
  var __publicField$3 = (obj, key, value) => {
@@ -209,11 +309,11 @@ async function initSqliteWithRetry(db) {
209
309
  await dbExec(
210
310
  db,
211
311
  `
212
- PRAGMA journal_mode = WAL;
213
- PRAGMA synchronous = normal;
214
- PRAGMA wal_autocheckpoint = 5000;
215
- PRAGMA busy_timeout = 5000;
216
- `
312
+ PRAGMA journal_mode = WAL;
313
+ PRAGMA synchronous = normal;
314
+ PRAGMA wal_autocheckpoint = 5000;
315
+ PRAGMA busy_timeout = 5000;
316
+ `
217
317
  );
218
318
  await dbRun(
219
319
  db,
@@ -225,7 +325,7 @@ async function initSqliteWithRetry(db) {
225
325
  expiresAt INTEGER,
226
326
  PRIMARY KEY (key, subKey)
227
327
  )
228
- `
328
+ `
229
329
  );
230
330
  } catch (err) {
231
331
  throw new Error(`SQLite init failed: ${err}`);
@@ -238,28 +338,36 @@ const _SqliteAdapter = class _SqliteAdapter {
238
338
  __publicField$3(this, "defaultTtl", 1e3 * 60 * 60);
239
339
  __publicField$3(this, "cleanupInterval", 5 * 60 * 1e3);
240
340
  __publicField$3(this, "dbPath", "");
341
+ __publicField$3(this, "fileLock", null);
241
342
  }
343
+ /* ------------------------------- init ------------------------------- */
242
344
  async ensure() {
243
345
  if (!this.dbPath) {
244
346
  this.dbPath = this.opts.sqlitePath === ":memory:" ? ":memory:" : path.resolve(this.opts.sqlitePath);
245
347
  this.prefix = this.opts.prefix;
246
348
  this.defaultTtl = this.opts.ttl;
247
349
  this.cleanupInterval = this.opts.cleanupInterval ?? 30 * 60 * 1e3;
350
+ if (this.dbPath !== ":memory:") {
351
+ const baseDir = path.dirname(this.dbPath);
352
+ this.fileLock = new FileLock(baseDir);
353
+ } else {
354
+ const randomId = Math.random().toString(36).substring(2, 15);
355
+ const baseDir = path.join(os.tmpdir(), `db-cache-locks-${randomId}`);
356
+ this.fileLock = new FileLock(baseDir);
357
+ }
248
358
  }
249
359
  if (_SqliteAdapter.clients.has(this.dbPath))
250
360
  return;
251
361
  if (!_SqliteAdapter.initPromises.has(this.dbPath)) {
252
- const dir = path.dirname(this.dbPath);
253
- if (!fs.existsSync(dir)) {
254
- fs.mkdirSync(dir, { recursive: true });
362
+ if (this.dbPath !== ":memory:") {
363
+ const dir = path.dirname(this.dbPath);
364
+ if (!fs.existsSync(dir)) {
365
+ fs.mkdirSync(dir, { recursive: true });
366
+ }
255
367
  }
256
368
  _SqliteAdapter.initPromises.set(
257
369
  this.dbPath,
258
370
  (async () => {
259
- const dbDir = path.dirname(this.dbPath);
260
- if (!fs.existsSync(dbDir)) {
261
- fs.mkdirSync(dbDir, { recursive: true });
262
- }
263
371
  const db = new sqlite3.Database(this.dbPath);
264
372
  await initSqliteWithRetry(db);
265
373
  _SqliteAdapter.clients.set(this.dbPath, db);
@@ -283,16 +391,12 @@ const _SqliteAdapter = class _SqliteAdapter {
283
391
  }
284
392
  return _SqliteAdapter.clients.get(this.dbPath);
285
393
  }
394
+ /* ------------------------------- cleanup ------------------------------- */
286
395
  async cleanup() {
287
396
  const client = this.getClient();
288
- await new Promise((res, rej) => {
289
- client.run(
290
- "DELETE FROM kvcache WHERE expiresAt IS NOT NULL AND expiresAt <= ?",
291
- [Date.now()],
292
- (err) => err ? rej(err) : res()
293
- );
294
- });
397
+ await dbRun(client, "DELETE FROM kvcache WHERE expiresAt IS NOT NULL AND expiresAt <= ?", [Date.now()]);
295
398
  }
399
+ /* ------------------------------- core ------------------------------- */
296
400
  prefixKey(key) {
297
401
  return `${this.prefix}:${key}`;
298
402
  }
@@ -303,8 +407,7 @@ const _SqliteAdapter = class _SqliteAdapter {
303
407
  if (raw === null || raw === void 0)
304
408
  return null;
305
409
  try {
306
- const obj = JSON.parse(raw);
307
- return obj.v;
410
+ return JSON.parse(raw).v;
308
411
  } catch {
309
412
  return null;
310
413
  }
@@ -314,33 +417,52 @@ const _SqliteAdapter = class _SqliteAdapter {
314
417
  const storageKey = this.prefixKey(key);
315
418
  const effectiveTtl = opts.ttl !== void 0 ? opts.ttl : this.defaultTtl;
316
419
  const expiresAt = effectiveTtl > 0 ? Date.now() + effectiveTtl : null;
317
- const subKey = "";
318
- if (opts.nx) {
319
- const exists = await this.has(key);
320
- if (exists)
420
+ let fileLocked = false;
421
+ if (opts.nx && this.fileLock) {
422
+ fileLocked = await this.fileLock.acquire(storageKey, effectiveTtl);
423
+ if (!fileLocked)
321
424
  return false;
322
425
  }
323
- await dbRun(
324
- client,
325
- `
326
- INSERT INTO kvcache (key, subKey, value, expiresAt)
327
- VALUES (?, ?, ?, ?)
328
- ON CONFLICT(key, subKey) DO UPDATE SET
329
- value = excluded.value,
330
- expiresAt = excluded.expiresAt
331
- `,
332
- [storageKey, subKey, this.serialize(value), expiresAt]
333
- );
334
- return true;
426
+ try {
427
+ if (opts.nx) {
428
+ const existing = await dbGet(
429
+ client,
430
+ "SELECT value FROM kvcache WHERE key = ? AND subKey = ?",
431
+ [storageKey, ""]
432
+ );
433
+ if (existing) {
434
+ return false;
435
+ }
436
+ }
437
+ await dbRun(
438
+ client,
439
+ `
440
+ INSERT INTO kvcache (key, subKey, value, expiresAt)
441
+ VALUES (?, ?, ?, ?)
442
+ ON CONFLICT(key, subKey) DO UPDATE SET
443
+ value = excluded.value,
444
+ expiresAt = excluded.expiresAt
445
+ `,
446
+ [storageKey, "", this.serialize(value), expiresAt]
447
+ );
448
+ if (fileLocked && this.fileLock) {
449
+ await this.fileLock.release(storageKey);
450
+ }
451
+ return true;
452
+ } catch (err) {
453
+ if (fileLocked && this.fileLock) {
454
+ await this.fileLock.release(storageKey);
455
+ }
456
+ throw err;
457
+ }
335
458
  }
336
459
  async get(key) {
337
460
  const client = this.getClient();
338
461
  const storageKey = this.prefixKey(key);
339
- const subKey = "";
340
462
  const row = await dbGet(
341
463
  client,
342
464
  'SELECT value, "expiresAt" FROM kvcache WHERE key = ? AND subKey = ?',
343
- [storageKey, subKey]
465
+ [storageKey, ""]
344
466
  );
345
467
  if (!row)
346
468
  return null;
@@ -354,15 +476,17 @@ const _SqliteAdapter = class _SqliteAdapter {
354
476
  const client = this.getClient();
355
477
  const storageKey = this.prefixKey(key);
356
478
  await dbRun(client, "DELETE FROM kvcache WHERE key = ?", [storageKey]);
479
+ if (this.fileLock) {
480
+ await this.fileLock.release(storageKey);
481
+ }
357
482
  }
358
483
  async has(key) {
359
484
  const client = this.getClient();
360
485
  const storageKey = this.prefixKey(key);
361
- const subKey = "";
362
486
  const row = await dbGet(
363
487
  client,
364
488
  'SELECT "expiresAt" FROM kvcache WHERE key = ? AND subKey = ?',
365
- [storageKey, subKey]
489
+ [storageKey, ""]
366
490
  );
367
491
  if (!row)
368
492
  return false;
@@ -372,6 +496,7 @@ const _SqliteAdapter = class _SqliteAdapter {
372
496
  }
373
497
  return true;
374
498
  }
499
+ /* ------------------------------- group APIs ------------------------------- */
375
500
  async groupSet(key, subKey, value, opts) {
376
501
  const client = this.getClient();
377
502
  const storageKey = this.prefixKey(key);
@@ -385,11 +510,11 @@ const _SqliteAdapter = class _SqliteAdapter {
385
510
  await dbRun(
386
511
  client,
387
512
  `
388
- INSERT INTO kvcache (key, subKey, value, expiresAt)
389
- VALUES (?, ?, ?, ?)
390
- ON CONFLICT(key, subKey) DO UPDATE SET
391
- value = excluded.value,
392
- expiresAt = excluded.expiresAt
513
+ INSERT INTO kvcache (key, subKey, value, expiresAt)
514
+ VALUES (?, ?, ?, ?)
515
+ ON CONFLICT(key, subKey) DO UPDATE SET
516
+ value = excluded.value,
517
+ expiresAt = excluded.expiresAt
393
518
  `,
394
519
  [storageKey, subKey, this.serialize(value), expiresAt]
395
520
  );
@@ -429,8 +554,12 @@ const _SqliteAdapter = class _SqliteAdapter {
429
554
  async groupDel(key, subKey) {
430
555
  const client = this.getClient();
431
556
  const storageKey = this.prefixKey(key);
432
- return dbRun(client, "DELETE FROM kvcache WHERE key = ? AND subKey = ?", [storageKey, subKey]);
557
+ await dbRun(client, "DELETE FROM kvcache WHERE key = ? AND subKey = ?", [storageKey, subKey]);
558
+ if (this.fileLock) {
559
+ await this.fileLock.release(storageKey);
560
+ }
433
561
  }
562
+ /* ------------------------------- misc ------------------------------- */
434
563
  async close() {
435
564
  if (this.dbPath) {
436
565
  const timer = _SqliteAdapter.cleanupTimers.get(this.dbPath);
@@ -710,12 +839,8 @@ class LockDBCache extends SingleFlightDBCache {
710
839
  }
711
840
  this._inFlight.set(key, Date.now());
712
841
  try {
713
- const exists = await this.has(key);
714
- if (!exists) {
715
- const result = await this.set(key, Date.now(), { ttl: this.defaultTtl, nx: true });
716
- return result;
717
- }
718
- return false;
842
+ const result = await this.set(key, ulid(), { ttl: this.defaultTtl, nx: true });
843
+ return result;
719
844
  } finally {
720
845
  this._inFlight.delete(key);
721
846
  for (const [key2, value] of this._inFlight.entries()) {
@@ -809,7 +934,7 @@ const isJestTest = () => {
809
934
  return process.env.NODE_ENV === "test" || process.env.BABEL_ENV === "test";
810
935
  };
811
936
  const getAbtNodeRedisAndSQLiteUrl = () => {
812
- const blockletCacheDir = process.env.BLOCKLET_DATA_DIR ? path__default.join(process.env.BLOCKLET_DATA_DIR, "__default-cache-store.db") : void 0;
937
+ const blockletCacheDir = process.env.BLOCKLET_DATA_DIR ? path$1.join(process.env.BLOCKLET_DATA_DIR, "__default-cache-store.db") : void 0;
813
938
  const params = {
814
939
  redisUrl: process.env.ABT_NODE_CACHE_REDIS_URL,
815
940
  sqlitePath: isJestTest() ? ":memory:" : blockletCacheDir || process.env.ABT_NODE_CACHE_SQLITE_PATH
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abtnode/db-cache",
3
- "version": "1.17.3-beta-20251118-061144-335cd35d",
3
+ "version": "1.17.3-beta-20251119-102907-28b69b76",
4
4
  "description": "Db cache use redis or sqlite as backend",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,6 +27,8 @@
27
27
  "author": "",
28
28
  "license": "ISC",
29
29
  "dependencies": {
30
+ "@types/proper-lockfile": "^4.1.4",
31
+ "proper-lockfile": "^4.1.2",
30
32
  "redis": "^5.1.1",
31
33
  "sqlite3": "^5.1.7"
32
34
  },
@@ -40,5 +42,5 @@
40
42
  "typescript": "^5.6.3",
41
43
  "unbuild": "^2.0.0"
42
44
  },
43
- "gitHead": "8e2245b336c9b4cdfd8e53012ab44a11b68c5665"
45
+ "gitHead": "6ae74784386f183ac410a226b6b9334c0f722fcc"
44
46
  }