@holo-js/cache-db 0.3.27 → 0.4.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holo-JS contributors
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.
@@ -0,0 +1 @@
1
+ f2c783277b1c8c25f714f27b171f9c8f09c9da2c409f0fe1652f845a51177963
package/dist/index.d.ts CHANGED
@@ -57,7 +57,6 @@ type DatabaseCacheDriverOptions = {
57
57
  readonly connectionName: string;
58
58
  readonly table: string;
59
59
  readonly lockTable: string;
60
- readonly prefix?: string;
61
60
  readonly connection: HoloProjectConnectionConfig | string;
62
61
  readonly now?: () => number;
63
62
  readonly sleep?: (milliseconds: number) => Promise<void>;
@@ -88,7 +87,7 @@ declare function isDatabaseCacheTableMissingError(error: unknown, tableName: str
88
87
  declare function normalizeDatabaseCacheTableError(error: unknown, tableName: string, lockTableName: string): never;
89
88
  declare function withDatabaseCacheTableGuard<TValue>(tableName: string, lockTableName: string, callback: () => Promise<TValue>): Promise<TValue>;
90
89
  declare function readEntry(connection: DatabaseContext, tableName: string, key: string, now: number): Promise<DatabaseReadResult<DatabaseCacheEntryRow>>;
91
- declare function readLock(connection: DatabaseContext, tableName: string, name: string, now: number): Promise<DatabaseReadResult<DatabaseCacheLockRow>>;
90
+ declare function readLock(connection: DatabaseContext, tableName: string, name: string): Promise<DatabaseCacheLockRow | undefined>;
92
91
  declare function createDatabaseCacheDriver(options: DatabaseCacheDriverOptions): CacheDriverContract;
93
92
  declare const databaseCacheDriverFactory: Readonly<{
94
93
  driver: "database";
@@ -108,4 +107,5 @@ declare const cacheDbInternals: {
108
107
  withDatabaseCacheTableGuard: typeof withDatabaseCacheTableGuard;
109
108
  };
110
109
 
111
- export { CACHE_DATABASE_TABLE_DEFINITIONS, type CacheDatabaseTableColumnDefinition, type CacheDatabaseTableColumnKind, type CacheDatabaseTableDefinition, DEFAULT_CACHE_DATABASE_LOCK_TABLE, DEFAULT_CACHE_DATABASE_TABLE, type DatabaseCacheDriverOptions, cacheDbInternals, createDatabaseCacheDriver, databaseCacheDriverFactory };
110
+ export { CACHE_DATABASE_TABLE_DEFINITIONS, DEFAULT_CACHE_DATABASE_LOCK_TABLE, DEFAULT_CACHE_DATABASE_TABLE, cacheDbInternals, createDatabaseCacheDriver, databaseCacheDriverFactory };
111
+ export type { CacheDatabaseTableColumnDefinition, CacheDatabaseTableColumnKind, CacheDatabaseTableDefinition, DatabaseCacheDriverOptions };
package/dist/index.mjs CHANGED
@@ -12,10 +12,14 @@ import {
12
12
  createConnectionManager,
13
13
  createSchemaService,
14
14
  createRuntimeConnectionOptions,
15
+ holoRuntimeInternals,
15
16
  TableQueryBuilder
16
17
  } from "@holo-js/db";
18
+ var { driverErrorCodes, isDatabaseDeadlockError } = holoRuntimeInternals;
17
19
  var DEFAULT_CACHE_DATABASE_TABLE = "cache";
18
20
  var DEFAULT_CACHE_DATABASE_LOCK_TABLE = "cache_locks";
21
+ var DATABASE_CACHE_LOCK_LOTTERY = { chances: 2, outOf: 100 };
22
+ var MISSING_TABLE_DRIVER_CODES = /* @__PURE__ */ new Set(["SQLITE_ERROR", "ER_NO_SUCH_TABLE", "42P01"]);
19
23
  var CACHE_DATABASE_TABLE_DEFINITIONS = [
20
24
  {
21
25
  role: "entries",
@@ -138,9 +142,8 @@ function isDatabaseCacheTableMissingError(error, tableName, lockTableName) {
138
142
  return false;
139
143
  }
140
144
  const message = error.message.toLowerCase();
141
- const errorCode = typeof Reflect.get(error, "code") === "string" ? String(Reflect.get(error, "code")).toUpperCase() : void 0;
142
145
  const mentionsCacheTable = message.includes(tableName.toLowerCase()) || message.includes(lockTableName.toLowerCase());
143
- if (errorCode === "SQLITE_ERROR" || errorCode === "ER_NO_SUCH_TABLE" || errorCode === "42P01") {
146
+ if (driverErrorCodes(error).some((code) => MISSING_TABLE_DRIVER_CODES.has(code.toUpperCase()))) {
144
147
  return mentionsCacheTable || message.includes("no such table") || message.includes("does not exist");
145
148
  }
146
149
  return (message.includes("no such table") || message.includes("does not exist") || message.includes("doesn't exist")) && mentionsCacheTable;
@@ -183,37 +186,59 @@ async function readEntry(connection, tableName, key, now) {
183
186
  value: { ...row, expires_at: expiresAt }
184
187
  };
185
188
  }
186
- async function readLock(connection, tableName, name, now) {
189
+ async function readLock(connection, tableName, name) {
187
190
  const row = await new TableQueryBuilder(tableName, connection).where("name", name).first();
188
191
  if (!row) {
189
- return { state: "missing" };
192
+ return void 0;
190
193
  }
191
- const expiresAt = normalizeDatabaseTimestamp(row.expires_at);
192
- if (expiresAt <= now) {
193
- await new TableQueryBuilder(tableName, connection).where("name", name).delete();
194
- return { state: "missing" };
194
+ return { ...row, expires_at: normalizeDatabaseTimestamp(row.expires_at) };
195
+ }
196
+ async function retryOnDeadlock(operation) {
197
+ try {
198
+ return await operation();
199
+ } catch (error) {
200
+ if (!isDatabaseDeadlockError(error)) {
201
+ throw error;
202
+ }
203
+ return operation();
204
+ }
205
+ }
206
+ async function pruneExpiredLocks(connection, tableName, nowMs) {
207
+ try {
208
+ await new TableQueryBuilder(tableName, connection).where("expires_at", "<=", nowMs).delete();
209
+ } catch (error) {
210
+ if (!isDatabaseDeadlockError(error)) {
211
+ throw error;
212
+ }
195
213
  }
196
- return {
197
- state: "hit",
198
- value: { ...row, expires_at: expiresAt }
199
- };
200
214
  }
201
215
  function createDatabaseLock(connection, tableName, entryTableName, name, seconds, now, sleep, ownerFactory) {
202
216
  const owner = ownerFactory();
203
- async function tryAcquire() {
204
- return withDatabaseCacheTableGuard(entryTableName, tableName, async () => connection.transaction(async (tx) => {
205
- const locks = new TableQueryBuilder(tableName, tx);
206
- const existing = await readLock(tx, tableName, name, now());
207
- if (existing.state === "hit") {
217
+ async function acquireInTransaction() {
218
+ return connection.transaction(async (tx) => {
219
+ const nowMs = now();
220
+ const existing = await readLock(tx, tableName, name);
221
+ if (existing && existing.expires_at > nowMs) {
208
222
  return false;
209
223
  }
210
- const inserted = await locks.insertOrIgnore({
224
+ if (existing) {
225
+ await new TableQueryBuilder(tableName, tx).where("name", name).where("expires_at", "<=", nowMs).delete();
226
+ }
227
+ const inserted = await new TableQueryBuilder(tableName, tx).insertOrIgnore({
211
228
  name,
212
229
  owner,
213
- expires_at: now() + seconds * 1e3
230
+ expires_at: nowMs + seconds * 1e3
214
231
  });
215
232
  return resolveExecutionResultAffectedRows(inserted) > 0;
216
- }));
233
+ });
234
+ }
235
+ async function tryAcquire() {
236
+ return withDatabaseCacheTableGuard(entryTableName, tableName, async () => {
237
+ if (Math.random() * DATABASE_CACHE_LOCK_LOTTERY.outOf < DATABASE_CACHE_LOCK_LOTTERY.chances) {
238
+ await pruneExpiredLocks(connection, tableName, now());
239
+ }
240
+ return retryOnDeadlock(acquireInTransaction);
241
+ });
217
242
  }
218
243
  async function withCallback(callback) {
219
244
  if (!callback) {
@@ -234,15 +259,9 @@ function createDatabaseLock(connection, tableName, entryTableName, name, seconds
234
259
  return withCallback(callback);
235
260
  },
236
261
  async release() {
237
- const deleted = await withDatabaseCacheTableGuard(entryTableName, tableName, async () => connection.transaction(async (tx) => {
238
- const current = await readLock(tx, tableName, name, now());
239
- if (current.state === "missing" || current.value.owner !== owner) {
240
- return 0;
241
- }
242
- return resolveExecutionResultAffectedRows(
243
- await new TableQueryBuilder(tableName, tx).where("name", name).delete()
244
- );
245
- }));
262
+ const deleted = await withDatabaseCacheTableGuard(entryTableName, tableName, async () => resolveExecutionResultAffectedRows(
263
+ await new TableQueryBuilder(tableName, connection).where("name", name).where("owner", owner).delete()
264
+ ));
246
265
  return deleted > 0;
247
266
  },
248
267
  async block(waitSeconds, callback) {
@@ -267,7 +286,6 @@ function createDatabaseCacheDriver(options) {
267
286
  const connection = createDatabaseConnection(options.connectionName, options.connection);
268
287
  const entryTableName = options.table;
269
288
  const lockTableName = options.lockTable;
270
- const prefix = options.prefix ?? "";
271
289
  async function writeEntry(tx, input) {
272
290
  await new TableQueryBuilder(entryTableName, tx).upsert({
273
291
  key: input.key,
@@ -353,23 +371,9 @@ function createDatabaseCacheDriver(options) {
353
371
  });
354
372
  },
355
373
  async flush() {
356
- await withDatabaseCacheTableGuard(entryTableName, lockTableName, async () => connection.transaction(async (tx) => {
357
- if (!prefix) {
358
- await new TableQueryBuilder(lockTableName, tx).delete();
359
- await new TableQueryBuilder(entryTableName, tx).delete();
360
- return;
361
- }
362
- const hasSqlWildcard = /[%_[]/.test(prefix);
363
- if (hasSqlWildcard) {
364
- const upperBound = `${prefix}\uFFFF`;
365
- await new TableQueryBuilder(lockTableName, tx).where("name", ">=", prefix).where("name", "<", upperBound).delete();
366
- await new TableQueryBuilder(entryTableName, tx).where("key", ">=", prefix).where("key", "<", upperBound).delete();
367
- return;
368
- }
369
- const likePattern = `${prefix}%`;
370
- await new TableQueryBuilder(lockTableName, tx).whereLike("name", likePattern).delete();
371
- await new TableQueryBuilder(entryTableName, tx).whereLike("key", likePattern).delete();
372
- }));
374
+ await withDatabaseCacheTableGuard(entryTableName, lockTableName, async () => {
375
+ await new TableQueryBuilder(entryTableName, connection).delete();
376
+ });
373
377
  },
374
378
  async increment(key, amount) {
375
379
  return mutateNumericValue(key, amount);
@@ -383,9 +387,7 @@ function createDatabaseCacheDriver(options) {
383
387
  };
384
388
  Object.defineProperty(driver, /* @__PURE__ */ Symbol.for("holo.cache.driver.dispose"), {
385
389
  value() {
386
- void connection.disconnect().catch((error) => {
387
- console.error(`[@holo-js/cache-db] Failed to disconnect cache driver "${options.name}".`, error);
388
- });
390
+ return connection.disconnect();
389
391
  }
390
392
  });
391
393
  return driver;
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@holo-js/cache-db",
3
- "version": "0.3.27",
3
+ "version": "0.4.0",
4
4
  "description": "Holo-JS Framework - DB-backed cache driver",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cobraprojects/holo-js.git",
10
+ "directory": "packages/cache-db"
11
+ },
7
12
  "exports": {
8
13
  ".": {
9
14
  "types": "./dist/index.d.ts",
@@ -14,7 +19,8 @@
14
19
  "main": "./dist/index.mjs",
15
20
  "types": "./dist/index.d.ts",
16
21
  "files": [
17
- "dist"
22
+ "dist",
23
+ "LICENSE"
18
24
  ],
19
25
  "scripts": {
20
26
  "build": "tsup",
@@ -23,24 +29,26 @@
23
29
  "test": "vitest --run"
24
30
  },
25
31
  "peerDependencies": {
26
- "@holo-js/cache": "^0.3.27",
27
- "@holo-js/db": "^0.3.27",
28
- "@holo-js/kernel": "^0.3.27"
32
+ "@holo-js/cache": "^0.4.0",
33
+ "@holo-js/db": "^0.4.0",
34
+ "@holo-js/kernel": "^0.4.0"
29
35
  },
30
36
  "dependencies": {
31
37
  "tslib": "^2.8.1"
32
38
  },
33
39
  "devDependencies": {
34
- "@holo-js/cache": "^0.3.27",
35
- "@holo-js/db": "^0.3.27",
36
- "@holo-js/db-sqlite": "^0.3.27",
37
- "@holo-js/kernel": "^0.3.27",
38
- "@types/node": "^22.10.2",
39
- "tsup": "^8.3.5",
40
- "typescript": "^5.7.2",
41
- "vitest": "^4.1.5"
40
+ "@holo-js/cache": "^0.4.0",
41
+ "@holo-js/db": "^0.4.0",
42
+ "@holo-js/db-mysql": "^0.4.0",
43
+ "@holo-js/db-postgres": "^0.4.0",
44
+ "@holo-js/db-sqlite": "^0.4.0",
45
+ "@holo-js/kernel": "^0.4.0",
46
+ "@types/node": "^22.20.5",
47
+ "tsup": "^9.0.0",
48
+ "typescript": "^6.0.3",
49
+ "vitest": "^5.0.5"
42
50
  },
43
51
  "engines": {
44
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
52
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
45
53
  }
46
54
  }