@holo-js/cache-db 0.3.28 → 0.4.1
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 +21 -0
- package/dist/.holo-build-fingerprint +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +52 -50
- package/package.json +22 -14
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
|
+
191cb6e93b5151d623ad08ce145879dc5218c717208bb746de85c124a51331f6
|
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
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
|
192
|
+
return void 0;
|
|
190
193
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
204
|
-
return
|
|
205
|
-
const
|
|
206
|
-
const existing = await readLock(tx, tableName, name
|
|
207
|
-
if (existing.
|
|
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
|
-
|
|
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:
|
|
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 () =>
|
|
238
|
-
|
|
239
|
-
|
|
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 () =>
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
+
"version": "0.4.1",
|
|
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.
|
|
27
|
-
"@holo-js/db": "^0.
|
|
28
|
-
"@holo-js/kernel": "^0.
|
|
32
|
+
"@holo-js/cache": "^0.4.1",
|
|
33
|
+
"@holo-js/db": "^0.4.1",
|
|
34
|
+
"@holo-js/kernel": "^0.4.1"
|
|
29
35
|
},
|
|
30
36
|
"dependencies": {
|
|
31
37
|
"tslib": "^2.8.1"
|
|
32
38
|
},
|
|
33
39
|
"devDependencies": {
|
|
34
|
-
"@holo-js/cache": "^0.
|
|
35
|
-
"@holo-js/db": "^0.
|
|
36
|
-
"@holo-js/db-
|
|
37
|
-
"@holo-js/
|
|
38
|
-
"@
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"@holo-js/cache": "^0.4.1",
|
|
41
|
+
"@holo-js/db": "^0.4.1",
|
|
42
|
+
"@holo-js/db-mysql": "^0.4.1",
|
|
43
|
+
"@holo-js/db-postgres": "^0.4.1",
|
|
44
|
+
"@holo-js/db-sqlite": "^0.4.1",
|
|
45
|
+
"@holo-js/kernel": "^0.4.1",
|
|
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.
|
|
52
|
+
"node": "^22.18.0 || ^24.0.0 || ^26.0.0"
|
|
45
53
|
}
|
|
46
54
|
}
|