@toapi/cache 0.3.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/README.md +5 -0
- package/dist/filesystem-cache.d.ts +18 -0
- package/dist/filesystem-cache.d.ts.map +1 -0
- package/dist/filesystem-cache.js +143 -0
- package/dist/in-memory-cache.d.ts +18 -0
- package/dist/in-memory-cache.d.ts.map +1 -0
- package/dist/in-memory-cache.js +136 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +0 -0
- package/dist/no-cache.d.ts +13 -0
- package/dist/no-cache.d.ts.map +1 -0
- package/dist/no-cache.js +21 -0
- package/dist/postgres-cache.d.ts +54 -0
- package/dist/postgres-cache.d.ts.map +1 -0
- package/dist/postgres-cache.js +193 -0
- package/dist/redis-cache.d.ts +18 -0
- package/dist/redis-cache.d.ts.map +1 -0
- package/dist/redis-cache.js +76 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { CacheEntry, Cache, Subscription, Json } from "./index";
|
|
3
|
+
export declare class FilesystemCache implements Cache {
|
|
4
|
+
db: DatabaseSync;
|
|
5
|
+
gcTimeout: number;
|
|
6
|
+
subscribers: Set<Subscription>;
|
|
7
|
+
constructor(filename: string);
|
|
8
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
9
|
+
set({ key, data, attachment, ttl, tags, }: CacheEntry & {
|
|
10
|
+
key: string;
|
|
11
|
+
ttl: number;
|
|
12
|
+
tags: string[];
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
delete(tags: string[], meta?: Json): Promise<void>;
|
|
15
|
+
gc(): void;
|
|
16
|
+
subscribe(callback: Subscription): () => void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=filesystem-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesystem-cache.d.ts","sourceRoot":"","sources":["../src/filesystem-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAOrE,qBAAa,eAAgB,YAAW,KAAK;IAC3C,EAAE,EAAE,YAAY,CAAC;IACjB,SAAS,SAAkB;IAC3B,WAAW,oBAA2B;gBAE1B,QAAQ,EAAE,MAAM;IAgCtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAqBlD,GAAG,CAAC,EACF,GAAG,EACH,IAAI,EACJ,UAAU,EACV,GAAG,EACH,IAAI,GACL,EAAE,UAAU,GAAG;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoCjB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BlD,EAAE;IAuCF,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;CAO9C"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
const MIN_GC_TIMEOUT = 5 * 1000;
|
|
4
|
+
const MAX_GC_TIMEOUT = 5 * 60 * 1000;
|
|
5
|
+
const GC_TIMEOUT_STEP = 0.1;
|
|
6
|
+
export class FilesystemCache {
|
|
7
|
+
db;
|
|
8
|
+
gcTimeout = MIN_GC_TIMEOUT;
|
|
9
|
+
subscribers = new Set();
|
|
10
|
+
constructor(filename) {
|
|
11
|
+
if (existsSync(filename)) {
|
|
12
|
+
this.db = new DatabaseSync(filename);
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
this.gc();
|
|
15
|
+
}, this.gcTimeout);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this.db = new DatabaseSync(filename);
|
|
19
|
+
this.db.exec(`
|
|
20
|
+
PRAGMA journal_mode = WAL;
|
|
21
|
+
BEGIN;
|
|
22
|
+
CREATE TABLE entries (
|
|
23
|
+
key TEXT PRIMARY KEY,
|
|
24
|
+
data TEXT,
|
|
25
|
+
attachment BLOB,
|
|
26
|
+
added_at INTEGER,
|
|
27
|
+
expires_at INTEGER
|
|
28
|
+
);
|
|
29
|
+
CREATE TABLE tags (
|
|
30
|
+
key TEXT,
|
|
31
|
+
tag TEXT,
|
|
32
|
+
UNIQUE(key, tag),
|
|
33
|
+
FOREIGN KEY (key) REFERENCES entries(key) ON DELETE CASCADE
|
|
34
|
+
);
|
|
35
|
+
COMMIT;
|
|
36
|
+
`);
|
|
37
|
+
this.gc();
|
|
38
|
+
}
|
|
39
|
+
async get(key) {
|
|
40
|
+
const result = this.db
|
|
41
|
+
.prepare(`
|
|
42
|
+
SELECT data, attachment
|
|
43
|
+
FROM entries
|
|
44
|
+
WHERE key = ? AND expires_at >= ?
|
|
45
|
+
`)
|
|
46
|
+
.get(key, Date.now());
|
|
47
|
+
return Promise.resolve(result
|
|
48
|
+
? {
|
|
49
|
+
data: result.data ? JSON.parse(result.data) : null,
|
|
50
|
+
attachment: result.attachment,
|
|
51
|
+
}
|
|
52
|
+
: null);
|
|
53
|
+
}
|
|
54
|
+
set({ key, data, attachment, ttl, tags, }) {
|
|
55
|
+
this.db.exec("BEGIN;");
|
|
56
|
+
try {
|
|
57
|
+
this.db
|
|
58
|
+
.prepare(`
|
|
59
|
+
INSERT OR REPLACE INTO entries (key, data, attachment, added_at, expires_at)
|
|
60
|
+
VALUES (?, ?, ?, ?, ?);
|
|
61
|
+
`)
|
|
62
|
+
.run(key, data ? JSON.stringify(data) : null, attachment ?? null, Date.now(), Date.now() + ttl * 1000);
|
|
63
|
+
const stmt = this.db.prepare(`
|
|
64
|
+
INSERT OR IGNORE INTO tags (key, tag) VALUES (?, ?)
|
|
65
|
+
`);
|
|
66
|
+
for (const tag of tags) {
|
|
67
|
+
stmt.run(key, tag);
|
|
68
|
+
}
|
|
69
|
+
this.db.exec("COMMIT;");
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
try {
|
|
73
|
+
this.db.exec("ROLLBACK;");
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
}
|
|
80
|
+
delete(tags, meta) {
|
|
81
|
+
this.db.exec("BEGIN;");
|
|
82
|
+
try {
|
|
83
|
+
const stmt = this.db.prepare(`
|
|
84
|
+
DELETE FROM entries WHERE key IN
|
|
85
|
+
(SELECT key FROM tags WHERE tag = ?);
|
|
86
|
+
`);
|
|
87
|
+
for (const tag of tags) {
|
|
88
|
+
stmt.run(tag);
|
|
89
|
+
}
|
|
90
|
+
this.db.exec("COMMIT;");
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
try {
|
|
94
|
+
this.db.exec("ROLLBACK;");
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
for (const callback of this.subscribers) {
|
|
100
|
+
callback(tags, meta);
|
|
101
|
+
}
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
gc() {
|
|
105
|
+
try {
|
|
106
|
+
this.db.exec("BEGIN;");
|
|
107
|
+
let changes = 0;
|
|
108
|
+
try {
|
|
109
|
+
const result = this.db
|
|
110
|
+
.prepare(`
|
|
111
|
+
DELETE FROM entries WHERE expires_at < ?;
|
|
112
|
+
`)
|
|
113
|
+
.run(Date.now());
|
|
114
|
+
changes = result.changes;
|
|
115
|
+
this.db.exec("COMMIT;");
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
try {
|
|
119
|
+
this.db.exec("ROLLBACK;");
|
|
120
|
+
}
|
|
121
|
+
catch { }
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
if (changes > 100) {
|
|
125
|
+
this.gcTimeout = Math.max(Math.floor(this.gcTimeout * (1 - GC_TIMEOUT_STEP)), MIN_GC_TIMEOUT);
|
|
126
|
+
}
|
|
127
|
+
else if (changes < 10) {
|
|
128
|
+
this.gcTimeout = Math.min(Math.floor(this.gcTimeout * (1 + GC_TIMEOUT_STEP)), MAX_GC_TIMEOUT);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
this.gc();
|
|
134
|
+
}, this.gcTimeout);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
subscribe(callback) {
|
|
138
|
+
this.subscribers.add(callback);
|
|
139
|
+
return () => {
|
|
140
|
+
this.subscribers.delete(callback);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { CacheEntry, Cache, Subscription, Json } from "./index";
|
|
3
|
+
export declare class InMemoryCache implements Cache {
|
|
4
|
+
db: DatabaseSync;
|
|
5
|
+
gcTimeout: number;
|
|
6
|
+
subscribers: Set<Subscription>;
|
|
7
|
+
constructor();
|
|
8
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
9
|
+
set({ key, data, attachment, ttl, tags, }: CacheEntry & {
|
|
10
|
+
key: string;
|
|
11
|
+
ttl: number;
|
|
12
|
+
tags: string[];
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
delete(tags: string[], meta?: Json): Promise<void>;
|
|
15
|
+
gc(): void;
|
|
16
|
+
subscribe(callback: Subscription): () => void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=in-memory-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-cache.d.ts","sourceRoot":"","sources":["../src/in-memory-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAMrE,qBAAa,aAAc,YAAW,KAAK;IACzC,EAAE,eAAgC;IAClC,SAAS,SAAkB;IAC3B,WAAW,oBAA2B;;IA2BhC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAqBlD,GAAG,CAAC,EACF,GAAG,EACH,IAAI,EACJ,UAAU,EACV,GAAG,EACH,IAAI,GACL,EAAE,UAAU,GAAG;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoCjB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BlD,EAAE;IAuCF,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;CAO9C"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
const MIN_GC_TIMEOUT = 5 * 1000;
|
|
3
|
+
const MAX_GC_TIMEOUT = 5 * 60 * 1000;
|
|
4
|
+
const GC_TIMEOUT_STEP = 0.1;
|
|
5
|
+
export class InMemoryCache {
|
|
6
|
+
db = new DatabaseSync(":memory:");
|
|
7
|
+
gcTimeout = MIN_GC_TIMEOUT;
|
|
8
|
+
subscribers = new Set();
|
|
9
|
+
constructor() {
|
|
10
|
+
this.db.exec(`
|
|
11
|
+
PRAGMA journal_mode = WAL;
|
|
12
|
+
BEGIN;
|
|
13
|
+
CREATE TABLE entries (
|
|
14
|
+
key TEXT PRIMARY KEY,
|
|
15
|
+
data TEXT,
|
|
16
|
+
attachment BLOB,
|
|
17
|
+
added_at INTEGER,
|
|
18
|
+
expires_at INTEGER
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE tags (
|
|
21
|
+
key TEXT,
|
|
22
|
+
tag TEXT,
|
|
23
|
+
UNIQUE(key, tag),
|
|
24
|
+
FOREIGN KEY (key) REFERENCES entries(key) ON DELETE CASCADE
|
|
25
|
+
);
|
|
26
|
+
COMMIT;
|
|
27
|
+
`);
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
this.gc();
|
|
30
|
+
}, this.gcTimeout);
|
|
31
|
+
}
|
|
32
|
+
async get(key) {
|
|
33
|
+
const result = this.db
|
|
34
|
+
.prepare(`
|
|
35
|
+
SELECT data, attachment
|
|
36
|
+
FROM entries
|
|
37
|
+
WHERE key = ? AND expires_at >= ?
|
|
38
|
+
`)
|
|
39
|
+
.get(key, Date.now());
|
|
40
|
+
return Promise.resolve(result
|
|
41
|
+
? {
|
|
42
|
+
data: result.data ? JSON.parse(result.data) : null,
|
|
43
|
+
attachment: result.attachment,
|
|
44
|
+
}
|
|
45
|
+
: null);
|
|
46
|
+
}
|
|
47
|
+
set({ key, data, attachment, ttl, tags, }) {
|
|
48
|
+
this.db.exec("BEGIN;");
|
|
49
|
+
try {
|
|
50
|
+
this.db
|
|
51
|
+
.prepare(`
|
|
52
|
+
INSERT OR REPLACE INTO entries (key, data, attachment, added_at, expires_at)
|
|
53
|
+
VALUES (?, ?, ?, ?, ?);
|
|
54
|
+
`)
|
|
55
|
+
.run(key, data ? JSON.stringify(data) : null, attachment ?? null, Date.now(), Date.now() + ttl * 1000);
|
|
56
|
+
const stmt = this.db.prepare(`
|
|
57
|
+
INSERT OR IGNORE INTO tags (key, tag) VALUES (?, ?)
|
|
58
|
+
`);
|
|
59
|
+
for (const tag of tags) {
|
|
60
|
+
stmt.run(key, tag);
|
|
61
|
+
}
|
|
62
|
+
this.db.exec("COMMIT;");
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
try {
|
|
66
|
+
this.db.exec("ROLLBACK;");
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
delete(tags, meta) {
|
|
74
|
+
this.db.exec("BEGIN;");
|
|
75
|
+
try {
|
|
76
|
+
const stmt = this.db.prepare(`
|
|
77
|
+
DELETE FROM entries WHERE key IN
|
|
78
|
+
(SELECT key FROM tags WHERE tag = ?);
|
|
79
|
+
`);
|
|
80
|
+
for (const tag of tags) {
|
|
81
|
+
stmt.run(tag);
|
|
82
|
+
}
|
|
83
|
+
this.db.exec("COMMIT;");
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
try {
|
|
87
|
+
this.db.exec("ROLLBACK;");
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
for (const callback of this.subscribers) {
|
|
93
|
+
callback(tags, meta);
|
|
94
|
+
}
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
gc() {
|
|
98
|
+
try {
|
|
99
|
+
this.db.exec("BEGIN;");
|
|
100
|
+
let changes = 0;
|
|
101
|
+
try {
|
|
102
|
+
const result = this.db
|
|
103
|
+
.prepare(`
|
|
104
|
+
DELETE FROM entries WHERE expires_at < ?;
|
|
105
|
+
`)
|
|
106
|
+
.run(Date.now());
|
|
107
|
+
changes = result.changes;
|
|
108
|
+
this.db.exec("COMMIT;");
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
try {
|
|
112
|
+
this.db.exec("ROLLBACK;");
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
if (changes > 100) {
|
|
118
|
+
this.gcTimeout = Math.max(Math.floor(this.gcTimeout * (1 - GC_TIMEOUT_STEP)), MIN_GC_TIMEOUT);
|
|
119
|
+
}
|
|
120
|
+
else if (changes < 10) {
|
|
121
|
+
this.gcTimeout = Math.min(Math.floor(this.gcTimeout * (1 + GC_TIMEOUT_STEP)), MAX_GC_TIMEOUT);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
this.gc();
|
|
127
|
+
}, this.gcTimeout);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
subscribe(callback) {
|
|
131
|
+
this.subscribers.add(callback);
|
|
132
|
+
return () => {
|
|
133
|
+
this.subscribers.delete(callback);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Json = string | number | boolean | null | Json[] | {
|
|
2
|
+
[key: string]: Json;
|
|
3
|
+
};
|
|
4
|
+
export interface CacheEntry {
|
|
5
|
+
data?: Json | null;
|
|
6
|
+
attachment?: Uint8Array | null;
|
|
7
|
+
}
|
|
8
|
+
export interface Subscription {
|
|
9
|
+
(tags: string[], meta?: Json): void;
|
|
10
|
+
}
|
|
11
|
+
export interface Cache {
|
|
12
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
13
|
+
set(input: CacheEntry & {
|
|
14
|
+
key: string;
|
|
15
|
+
ttl: number;
|
|
16
|
+
tags: string[];
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
delete(tags: string[], meta?: {
|
|
19
|
+
clientId?: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
subscribe(callback: Subscription): () => void;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GACZ,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,IAAI,EAAE,GACN;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC7C,GAAG,CACD,KAAK,EAAE,UAAU,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI,CAAC;CAC/C"}
|
package/dist/index.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Cache, CacheEntry, Subscription } from "./index";
|
|
2
|
+
export declare class NoCache implements Cache {
|
|
3
|
+
subscribers: Set<Subscription>;
|
|
4
|
+
get(_key: string): Promise<CacheEntry | null>;
|
|
5
|
+
set(_: CacheEntry & {
|
|
6
|
+
key: string;
|
|
7
|
+
ttl: number;
|
|
8
|
+
tags: string[];
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
delete(tags: string[]): Promise<void>;
|
|
11
|
+
subscribe(callback: Subscription): () => void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=no-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-cache.d.ts","sourceRoot":"","sources":["../src/no-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE/D,qBAAa,OAAQ,YAAW,KAAK;IACnC,WAAW,oBAA2B;IAEhC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAInD,GAAG,CACD,CAAC,EAAE,UAAU,GAAG;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GACA,OAAO,CAAC,IAAI,CAAC;IAIhB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrC,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;CAO9C"}
|
package/dist/no-cache.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class NoCache {
|
|
2
|
+
subscribers = new Set();
|
|
3
|
+
async get(_key) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
set(_) {
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
}
|
|
9
|
+
delete(tags) {
|
|
10
|
+
for (const callback of this.subscribers) {
|
|
11
|
+
callback(tags);
|
|
12
|
+
}
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
subscribe(callback) {
|
|
16
|
+
this.subscribers.add(callback);
|
|
17
|
+
return () => {
|
|
18
|
+
this.subscribers.delete(callback);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Pool, PoolClient } from "pg";
|
|
2
|
+
import type { Cache, CacheEntry, Json, Subscription } from "./index";
|
|
3
|
+
export interface PostgresCacheOptions {
|
|
4
|
+
/**
|
|
5
|
+
* When `true` (the default) the required tables and indexes are created
|
|
6
|
+
* lazily on the first operation via `CREATE TABLE IF NOT EXISTS`. Set to
|
|
7
|
+
* `false` if you manage the schema yourself (e.g. via migrations).
|
|
8
|
+
*/
|
|
9
|
+
createSchema?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The Postgres schema to hold the cache tables. Defaults to whatever the
|
|
12
|
+
* connection's `search_path` resolves to (usually `public`). When set, the
|
|
13
|
+
* tables are fully qualified as `<schema>.cache_entries` /
|
|
14
|
+
* `<schema>.cache_tags` and — with `createSchema` enabled — the schema is
|
|
15
|
+
* created if it does not already exist. Each schema also gets its own
|
|
16
|
+
* invalidation channel so caches on different schemas do not cross-notify.
|
|
17
|
+
*/
|
|
18
|
+
schema?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class PostgresCache implements Cache {
|
|
21
|
+
private pool;
|
|
22
|
+
private options;
|
|
23
|
+
subscriptions: Set<Subscription>;
|
|
24
|
+
listener?: PoolClient;
|
|
25
|
+
gcTimeout: number;
|
|
26
|
+
private ready;
|
|
27
|
+
private gcTimer?;
|
|
28
|
+
private stopped;
|
|
29
|
+
/** Fully-qualified (schema-prefixed when applicable) table identifiers. */
|
|
30
|
+
private readonly entriesTable;
|
|
31
|
+
private readonly tagsTable;
|
|
32
|
+
/** Invalidation channel, namespaced by schema so schemas stay isolated. */
|
|
33
|
+
private readonly channel;
|
|
34
|
+
constructor(pool: Pool, options?: PostgresCacheOptions);
|
|
35
|
+
private setup;
|
|
36
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
37
|
+
set({ key, data, attachment, ttl, tags, }: CacheEntry & {
|
|
38
|
+
key: string;
|
|
39
|
+
ttl: number;
|
|
40
|
+
tags: string[];
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
delete(tags: string[], meta?: Json): Promise<void>;
|
|
43
|
+
private setupListener;
|
|
44
|
+
subscribe(callback: Subscription): () => void;
|
|
45
|
+
private gc;
|
|
46
|
+
/**
|
|
47
|
+
* Stops the background garbage collector and releases the dedicated
|
|
48
|
+
* invalidation listener connection back to the pool. Call this before
|
|
49
|
+
* shutting down so no timers or connections are left dangling. The
|
|
50
|
+
* underlying pool is left untouched — the caller owns its lifecycle.
|
|
51
|
+
*/
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=postgres-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-cache.d.ts","sourceRoot":"","sources":["../src/postgres-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAgB,MAAM,IAAI,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAkBrE,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,aAAc,YAAW,KAAK;IAgBvC,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;IAhBjB,aAAa,oBAA2B;IACxC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,SAAS,SAAkB;IAE3B,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,OAAO,CAAC,CAAgC;IAChD,OAAO,CAAC,OAAO,CAAS;IAExB,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAGvB,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,oBAAyB;YAW9B,KAAK;IA8Bb,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAqB5C,GAAG,CAAC,EACR,GAAG,EACH,IAAI,EACJ,UAAU,EACV,GAAG,EACH,IAAI,GACL,EAAE,UAAU,GAAG;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkDX,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YAkB1C,aAAa;IAiB3B,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;YAgB/B,EAAE;IA6BhB;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAW7B"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const CHANNEL = "tag_based_cache_invalidate";
|
|
2
|
+
const MIN_GC_TIMEOUT = 5 * 1000;
|
|
3
|
+
const MAX_GC_TIMEOUT = 5 * 60 * 1000;
|
|
4
|
+
const GC_TIMEOUT_STEP = 0.1;
|
|
5
|
+
/** Quote a Postgres identifier so it can be safely interpolated into SQL. */
|
|
6
|
+
function quoteIdentifier(name) {
|
|
7
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
export class PostgresCache {
|
|
10
|
+
pool;
|
|
11
|
+
options;
|
|
12
|
+
subscriptions = new Set();
|
|
13
|
+
listener;
|
|
14
|
+
gcTimeout = MIN_GC_TIMEOUT;
|
|
15
|
+
ready;
|
|
16
|
+
gcTimer;
|
|
17
|
+
stopped = false;
|
|
18
|
+
/** Fully-qualified (schema-prefixed when applicable) table identifiers. */
|
|
19
|
+
entriesTable;
|
|
20
|
+
tagsTable;
|
|
21
|
+
/** Invalidation channel, namespaced by schema so schemas stay isolated. */
|
|
22
|
+
channel;
|
|
23
|
+
constructor(pool, options = {}) {
|
|
24
|
+
this.pool = pool;
|
|
25
|
+
this.options = options;
|
|
26
|
+
const { schema } = options;
|
|
27
|
+
const prefix = schema ? `${quoteIdentifier(schema)}.` : "";
|
|
28
|
+
this.entriesTable = `${prefix}cache_entries`;
|
|
29
|
+
this.tagsTable = `${prefix}cache_tags`;
|
|
30
|
+
this.channel = schema ? `${CHANNEL}:${schema}` : CHANNEL;
|
|
31
|
+
this.ready = this.setup();
|
|
32
|
+
}
|
|
33
|
+
async setup() {
|
|
34
|
+
if (this.options.createSchema !== false) {
|
|
35
|
+
if (this.options.schema) {
|
|
36
|
+
await this.pool.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(this.options.schema)}`);
|
|
37
|
+
}
|
|
38
|
+
await this.pool.query(`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS ${this.entriesTable} (
|
|
40
|
+
key TEXT PRIMARY KEY,
|
|
41
|
+
data JSONB,
|
|
42
|
+
attachment BYTEA,
|
|
43
|
+
added_at BIGINT NOT NULL,
|
|
44
|
+
expires_at BIGINT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
CREATE TABLE IF NOT EXISTS ${this.tagsTable} (
|
|
47
|
+
key TEXT NOT NULL REFERENCES ${this.entriesTable}(key) ON DELETE CASCADE,
|
|
48
|
+
tag TEXT NOT NULL,
|
|
49
|
+
PRIMARY KEY (key, tag)
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS cache_tags_tag_idx ON ${this.tagsTable} (tag);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS cache_entries_expires_at_idx
|
|
53
|
+
ON ${this.entriesTable} (expires_at);
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
this.gcTimer = setTimeout(() => this.gc(), this.gcTimeout);
|
|
57
|
+
}
|
|
58
|
+
async get(key) {
|
|
59
|
+
await this.ready;
|
|
60
|
+
const { rows } = await this.pool.query(`
|
|
61
|
+
SELECT data, attachment
|
|
62
|
+
FROM ${this.entriesTable}
|
|
63
|
+
WHERE key = $1 AND expires_at >= $2
|
|
64
|
+
`, [key, Date.now()]);
|
|
65
|
+
const row = rows[0];
|
|
66
|
+
if (!row)
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
data: row.data ?? null,
|
|
70
|
+
attachment: row.attachment ? new Uint8Array(row.attachment) : null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async set({ key, data, attachment, ttl, tags, }) {
|
|
74
|
+
await this.ready;
|
|
75
|
+
const client = await this.pool.connect();
|
|
76
|
+
try {
|
|
77
|
+
await client.query("BEGIN");
|
|
78
|
+
await client.query(`
|
|
79
|
+
INSERT INTO ${this.entriesTable} (key, data, attachment, added_at, expires_at)
|
|
80
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
81
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
82
|
+
data = EXCLUDED.data,
|
|
83
|
+
attachment = EXCLUDED.attachment,
|
|
84
|
+
added_at = EXCLUDED.added_at,
|
|
85
|
+
expires_at = EXCLUDED.expires_at
|
|
86
|
+
`, [
|
|
87
|
+
key,
|
|
88
|
+
data ? JSON.stringify(data) : null,
|
|
89
|
+
attachment ? Buffer.from(attachment) : null,
|
|
90
|
+
Date.now(),
|
|
91
|
+
Date.now() + ttl * 1000,
|
|
92
|
+
]);
|
|
93
|
+
// Replace the full tag set so tags removed since the last `set` are
|
|
94
|
+
// dropped, matching the `INSERT OR REPLACE` semantics of the SQLite
|
|
95
|
+
// implementations.
|
|
96
|
+
await client.query(`DELETE FROM ${this.tagsTable} WHERE key = $1`, [key]);
|
|
97
|
+
for (const tag of tags) {
|
|
98
|
+
await client.query(`INSERT INTO ${this.tagsTable} (key, tag) VALUES ($1, $2)
|
|
99
|
+
ON CONFLICT DO NOTHING`, [key, tag]);
|
|
100
|
+
}
|
|
101
|
+
await client.query("COMMIT");
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
try {
|
|
105
|
+
await client.query("ROLLBACK");
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
client.release();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async delete(tags, meta) {
|
|
115
|
+
await this.ready;
|
|
116
|
+
// `cache_tags` rows are removed automatically via `ON DELETE CASCADE`.
|
|
117
|
+
await this.pool.query(`
|
|
118
|
+
DELETE FROM ${this.entriesTable}
|
|
119
|
+
WHERE key IN (SELECT key FROM ${this.tagsTable} WHERE tag = ANY($1))
|
|
120
|
+
`, [tags]);
|
|
121
|
+
await this.pool.query(`SELECT pg_notify($1, $2)`, [
|
|
122
|
+
this.channel,
|
|
123
|
+
JSON.stringify({ tags, meta }),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
async setupListener() {
|
|
127
|
+
const listener = await this.pool.connect();
|
|
128
|
+
this.listener = listener;
|
|
129
|
+
listener.on("notification", (msg) => {
|
|
130
|
+
if (msg.channel !== this.channel || !msg.payload)
|
|
131
|
+
return;
|
|
132
|
+
const { tags, meta } = JSON.parse(msg.payload);
|
|
133
|
+
for (const callback of this.subscriptions) {
|
|
134
|
+
callback(tags, meta);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// LISTEN does not accept a bound parameter; the channel name is quoted so
|
|
138
|
+
// an arbitrary (schema-derived) identifier is interpolated safely.
|
|
139
|
+
await listener.query(`LISTEN ${quoteIdentifier(this.channel)}`);
|
|
140
|
+
}
|
|
141
|
+
subscribe(callback) {
|
|
142
|
+
if (this.subscriptions.size === 0) {
|
|
143
|
+
this.setupListener();
|
|
144
|
+
}
|
|
145
|
+
this.subscriptions.add(callback);
|
|
146
|
+
return () => {
|
|
147
|
+
this.subscriptions.delete(callback);
|
|
148
|
+
if (this.subscriptions.size === 0) {
|
|
149
|
+
this.listener?.removeAllListeners("notification");
|
|
150
|
+
this.listener?.release();
|
|
151
|
+
this.listener = undefined;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async gc() {
|
|
156
|
+
try {
|
|
157
|
+
const result = await this.pool.query(`DELETE FROM ${this.entriesTable} WHERE expires_at < $1`, [Date.now()]);
|
|
158
|
+
const changes = result.rowCount ?? 0;
|
|
159
|
+
if (changes > 100) {
|
|
160
|
+
this.gcTimeout = Math.max(Math.floor(this.gcTimeout * (1 - GC_TIMEOUT_STEP)), MIN_GC_TIMEOUT);
|
|
161
|
+
}
|
|
162
|
+
else if (changes < 10) {
|
|
163
|
+
this.gcTimeout = Math.min(Math.floor(this.gcTimeout * (1 + GC_TIMEOUT_STEP)), MAX_GC_TIMEOUT);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore transient errors (e.g. the pool being drained on shutdown);
|
|
168
|
+
// the next tick will retry.
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (!this.stopped) {
|
|
172
|
+
this.gcTimer = setTimeout(() => this.gc(), this.gcTimeout);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Stops the background garbage collector and releases the dedicated
|
|
178
|
+
* invalidation listener connection back to the pool. Call this before
|
|
179
|
+
* shutting down so no timers or connections are left dangling. The
|
|
180
|
+
* underlying pool is left untouched — the caller owns its lifecycle.
|
|
181
|
+
*/
|
|
182
|
+
async close() {
|
|
183
|
+
this.stopped = true;
|
|
184
|
+
if (this.gcTimer) {
|
|
185
|
+
clearTimeout(this.gcTimer);
|
|
186
|
+
this.gcTimer = undefined;
|
|
187
|
+
}
|
|
188
|
+
this.listener?.removeAllListeners("notification");
|
|
189
|
+
this.listener?.release();
|
|
190
|
+
this.listener = undefined;
|
|
191
|
+
this.subscriptions.clear();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RedisClientType } from "@redis/client";
|
|
2
|
+
import type { Cache, CacheEntry, Json, Subscription } from "./index";
|
|
3
|
+
export declare class RedisCache implements Cache {
|
|
4
|
+
private redis;
|
|
5
|
+
subscriptions: Set<Subscription>;
|
|
6
|
+
subscriber?: RedisClientType;
|
|
7
|
+
constructor(redis: RedisClientType);
|
|
8
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
9
|
+
set({ key, data, attachment, ttl, tags, }: CacheEntry & {
|
|
10
|
+
key: string;
|
|
11
|
+
ttl: number;
|
|
12
|
+
tags: string[];
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
delete(tags: string[], meta?: Json): Promise<void>;
|
|
15
|
+
setupSubscription(): Promise<void>;
|
|
16
|
+
subscribe(callback: Subscription): () => void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=redis-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-cache.d.ts","sourceRoot":"","sources":["../src/redis-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AASrE,qBAAa,UAAW,YAAW,KAAK;IAI1B,OAAO,CAAC,KAAK;IAHzB,aAAa,oBAA2B;IACxC,UAAU,CAAC,EAAE,eAAe,CAAC;gBAET,KAAK,EAAE,eAAe;IAEpC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAkB5C,GAAG,CAAC,EACR,GAAG,EACH,IAAI,EACJ,UAAU,EACV,GAAG,EACH,IAAI,GACL,EAAE,UAAU,GAAG;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBX,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlD,iBAAiB;IAWvB,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;CAc9C"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const BLOB_STRING_TYPE = 36;
|
|
2
|
+
export class RedisCache {
|
|
3
|
+
redis;
|
|
4
|
+
subscriptions = new Set();
|
|
5
|
+
subscriber;
|
|
6
|
+
constructor(redis) {
|
|
7
|
+
this.redis = redis;
|
|
8
|
+
}
|
|
9
|
+
async get(key) {
|
|
10
|
+
const data = await this.redis.get(`data:${key}`);
|
|
11
|
+
const attachment = (await this.redis
|
|
12
|
+
.withCommandOptions({
|
|
13
|
+
typeMapping: {
|
|
14
|
+
[BLOB_STRING_TYPE]: Buffer,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
.get(`attachment:${key}`));
|
|
18
|
+
return data || attachment
|
|
19
|
+
? {
|
|
20
|
+
data: data ? JSON.parse(data) : null,
|
|
21
|
+
attachment: attachment ? new Uint8Array(attachment) : null,
|
|
22
|
+
}
|
|
23
|
+
: null;
|
|
24
|
+
}
|
|
25
|
+
async set({ key, data, attachment, ttl, tags, }) {
|
|
26
|
+
let promises = [];
|
|
27
|
+
if (data) {
|
|
28
|
+
promises.push(this.redis.set(`data:${key}`, JSON.stringify(data), { EX: ttl }));
|
|
29
|
+
}
|
|
30
|
+
if (attachment) {
|
|
31
|
+
promises.push(this.redis.set(`attachment:${key}`, Buffer.from(attachment), {
|
|
32
|
+
EX: ttl,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
for (const tag of tags) {
|
|
36
|
+
promises.push(this.redis.sAdd(`tag:${tag}`, key));
|
|
37
|
+
}
|
|
38
|
+
await Promise.all(promises);
|
|
39
|
+
}
|
|
40
|
+
async delete(tags, meta) {
|
|
41
|
+
const keys = await Promise.all(tags.map((tag) => this.redis.sMembers(`tag:${tag}`)));
|
|
42
|
+
const keySet = new Set(keys.flat());
|
|
43
|
+
const keysToDelete = tags.map((tag) => `tag:${tag}`);
|
|
44
|
+
for (const key of keySet) {
|
|
45
|
+
keysToDelete.push(`data:${key}`, `attachment:${key}`);
|
|
46
|
+
}
|
|
47
|
+
// Delete all the keys if there are any
|
|
48
|
+
if (keysToDelete.length > 0) {
|
|
49
|
+
await this.redis.del(keysToDelete);
|
|
50
|
+
}
|
|
51
|
+
await this.redis.publish("invalidate", JSON.stringify({ tags, meta }));
|
|
52
|
+
}
|
|
53
|
+
async setupSubscription() {
|
|
54
|
+
this.subscriber = this.redis.duplicate();
|
|
55
|
+
await this.subscriber.connect();
|
|
56
|
+
await this.subscriber.subscribe("invalidate", (message) => {
|
|
57
|
+
const { tags, meta } = JSON.parse(message);
|
|
58
|
+
for (const callback of this.subscriptions) {
|
|
59
|
+
callback(tags, meta);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
subscribe(callback) {
|
|
64
|
+
if (this.subscriptions.size === 0) {
|
|
65
|
+
this.setupSubscription();
|
|
66
|
+
}
|
|
67
|
+
this.subscriptions.add(callback);
|
|
68
|
+
return () => {
|
|
69
|
+
this.subscriptions.delete(callback);
|
|
70
|
+
if (this.subscriptions.size === 0) {
|
|
71
|
+
this.subscriber?.destroy();
|
|
72
|
+
this.subscriber = undefined;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toapi/cache",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Michel Smola",
|
|
6
|
+
"email": "michel.smola@farbenmeer.de"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"private": false,
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"default": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./filesystem-cache": {
|
|
22
|
+
"default": "./dist/filesystem-cache.js",
|
|
23
|
+
"types": "./dist/filesystem-cache.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./redis-cache": {
|
|
26
|
+
"default": "./dist/redis-cache.js",
|
|
27
|
+
"types": "./dist/redis-cache.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./postgres-cache": {
|
|
30
|
+
"default": "./dist/postgres-cache.js",
|
|
31
|
+
"types": "./dist/postgres-cache.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./in-memory-cache": {
|
|
34
|
+
"default": "./dist/in-memory-cache.js",
|
|
35
|
+
"types": "./dist/in-memory-cache.d.ts"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.0.3",
|
|
40
|
+
"@types/pg": "^8.0.0",
|
|
41
|
+
"vitest": "^4.0.16",
|
|
42
|
+
"typescript": "^6.0.0",
|
|
43
|
+
"@redis/client": "^6.0.0",
|
|
44
|
+
"pg": "^8.0.0"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/farbenmeer/tapi.git"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@redis/client": "^5.10.0 || ^6.0.0",
|
|
52
|
+
"pg": "^8.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@redis/client": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"pg": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsc --noEmit false",
|
|
64
|
+
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
65
|
+
"test": "vitest"
|
|
66
|
+
}
|
|
67
|
+
}
|