@anteros/core 0.0.37 → 0.0.39
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/database/db.cache.ts +1 -1
- package/database/mongodbadapter.ts +2 -2
- package/database/tenant.ts +3 -2
- package/package.json +1 -1
- package/probe-cache.ts +3 -3
- package/tests/cache.test.ts +40 -22
- package/utils/cache.ts +28 -54
- package/bench-cache.ts +0 -70
package/database/db.cache.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { FindOptions, FindCallOptions } from "../types/mongo";
|
|
4
|
-
import { Cache, useFilesystemCache, useMemoryCache, useRedisCache } from "../utils/cache";
|
|
4
|
+
import { type Cache, useFilesystemCache, useMemoryCache, useRedisCache } from "../utils/cache";
|
|
5
5
|
import { cfg } from "../server/config";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1144,8 +1144,8 @@ class MongoRest {
|
|
|
1144
1144
|
missing: (key) => ns().missing({ key }),
|
|
1145
1145
|
delete: (key) => ns().delete({ key }),
|
|
1146
1146
|
deleteMany: (keys) => ns().deleteMany({ keys }),
|
|
1147
|
-
pull: (key) => ns().pull(key)
|
|
1148
|
-
expire: (key
|
|
1147
|
+
pull: (key) => ns().pull(key) as Promise<any>, // bentocache types are stricter than the TenantCache contract
|
|
1148
|
+
expire: (key) => ns().expire({ key }), // bentocache soft-expire takes { key } only (grace is a getOrSet option)
|
|
1149
1149
|
clear: () => ns().clear(),
|
|
1150
1150
|
namespace: (name) => cache.namespace(`app:${tenantId}:${name}`),
|
|
1151
1151
|
};
|
package/database/tenant.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { useRest } from "./rest";
|
|
|
4
4
|
async function syncTenants() {
|
|
5
5
|
try {
|
|
6
6
|
for await (let tenant of cfg.tenants ?? []) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
let rest = new useRest({
|
|
8
|
+
tenant_id: tenant.id,
|
|
9
|
+
database: {
|
|
9
10
|
uri: tenant.database.uri,
|
|
10
11
|
options: {
|
|
11
12
|
timeoutMS: 2000,
|
package/package.json
CHANGED
package/probe-cache.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemoryCache } from "./utils/cache";
|
|
2
2
|
|
|
3
3
|
async function probe() {
|
|
4
|
-
const c =
|
|
4
|
+
const c = useMemoryCache();
|
|
5
5
|
const v = await c.get({ key: "k" });
|
|
6
|
-
const p = await c.pull(
|
|
6
|
+
const p = await c.pull("p");
|
|
7
7
|
const g = await c.getOrSet({ key: "g", factory: () => 42 });
|
|
8
8
|
const s = await c.set({ key: "a", value: 1 });
|
|
9
9
|
const h = await c.has({ key: "a" });
|
package/tests/cache.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
-
import { Cache, useMemoryCache, useFilesystemCache } from "../utils/cache";
|
|
2
|
+
import { type Cache, useMemoryCache, useFilesystemCache } from "../utils/cache";
|
|
3
3
|
|
|
4
4
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
5
5
|
|
|
@@ -12,16 +12,19 @@ describe("Cache (memory)", () => {
|
|
|
12
12
|
|
|
13
13
|
it("set + get round-trip", async () => {
|
|
14
14
|
await c.set({ key: "k", value: { id: 1 }, ttl: "5m" });
|
|
15
|
-
|
|
15
|
+
const v = await c.get({ key: "k" });
|
|
16
|
+
expect(v).toEqual({ id: 1 });
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
it("setForever never expires", async () => {
|
|
19
20
|
await c.setForever({ key: "cfg", value: { theme: "dark" } });
|
|
20
|
-
|
|
21
|
+
const v = await c.get({ key: "cfg" });
|
|
22
|
+
expect(v).toEqual({ theme: "dark" });
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
it("get returns
|
|
24
|
-
|
|
25
|
+
it("get returns undefined on miss", async () => {
|
|
26
|
+
const v = await c.get({ key: "nope" });
|
|
27
|
+
expect(v).toBeUndefined();
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
it("has / missing", async () => {
|
|
@@ -33,9 +36,11 @@ describe("Cache (memory)", () => {
|
|
|
33
36
|
|
|
34
37
|
it("ttl expiry (ms)", async () => {
|
|
35
38
|
await c.set({ key: "x", value: 1, ttl: 50 });
|
|
36
|
-
|
|
39
|
+
const v1 = await c.get({ key: "x" });
|
|
40
|
+
expect(v1).toBe(1);
|
|
37
41
|
await sleep(80);
|
|
38
|
-
|
|
42
|
+
const v2 = await c.get({ key: "x" });
|
|
43
|
+
expect(v2).toBeUndefined();
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
it("getOrSet uses factory and caches", async () => {
|
|
@@ -44,21 +49,25 @@ describe("Cache (memory)", () => {
|
|
|
44
49
|
calls++;
|
|
45
50
|
return { n: calls };
|
|
46
51
|
};
|
|
47
|
-
|
|
48
|
-
expect(
|
|
52
|
+
const a = await c.getOrSet({ key: "g", factory });
|
|
53
|
+
expect(a).toEqual({ n: 1 });
|
|
54
|
+
const b = await c.getOrSet({ key: "g", factory });
|
|
55
|
+
expect(b).toEqual({ n: 1 });
|
|
49
56
|
expect(calls).toBe(1);
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
it("getOrSetForever", async () => {
|
|
53
60
|
const value = await c.getOrSetForever({ key: "f", factory: () => 42 });
|
|
54
61
|
expect(value).toBe(42);
|
|
55
|
-
|
|
62
|
+
const v = await c.get({ key: "f" });
|
|
63
|
+
expect(v).toBe(42);
|
|
56
64
|
});
|
|
57
65
|
|
|
58
66
|
it("delete removes entry", async () => {
|
|
59
67
|
await c.set({ key: "d", value: 1 });
|
|
60
68
|
await c.delete({ key: "d" });
|
|
61
|
-
|
|
69
|
+
const v = await c.get({ key: "d" });
|
|
70
|
+
expect(v).toBeUndefined();
|
|
62
71
|
});
|
|
63
72
|
|
|
64
73
|
it("deleteMany removes several keys", async () => {
|
|
@@ -71,16 +80,20 @@ describe("Cache (memory)", () => {
|
|
|
71
80
|
|
|
72
81
|
it("pull returns and removes", async () => {
|
|
73
82
|
await c.set({ key: "p", value: 7 });
|
|
74
|
-
|
|
75
|
-
expect(
|
|
83
|
+
const p = await c.pull("p");
|
|
84
|
+
expect(p).toBe(7);
|
|
85
|
+
const v = await c.get({ key: "p" });
|
|
86
|
+
expect(v).toBeUndefined();
|
|
76
87
|
});
|
|
77
88
|
|
|
78
89
|
it("clear empties everything", async () => {
|
|
79
90
|
await c.set({ key: "a", value: 1 });
|
|
80
91
|
await c.set({ key: "b", value: 2 });
|
|
81
92
|
await c.clear();
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
const va = await c.get({ key: "a" });
|
|
94
|
+
const vb = await c.get({ key: "b" });
|
|
95
|
+
expect(va).toBeUndefined();
|
|
96
|
+
expect(vb).toBeUndefined();
|
|
84
97
|
});
|
|
85
98
|
|
|
86
99
|
it("getOrSet recovers after ttl expiry (grace accepted)", async () => {
|
|
@@ -89,19 +102,22 @@ describe("Cache (memory)", () => {
|
|
|
89
102
|
expect(v1).toBe("fresh");
|
|
90
103
|
|
|
91
104
|
await sleep(60); // past ttl
|
|
92
|
-
|
|
105
|
+
const v2 = await c.get({ key: "e" });
|
|
106
|
+
expect(v2).toBeUndefined(); // expired
|
|
93
107
|
|
|
94
108
|
// getOrSet re-runs the factory after expiry
|
|
95
|
-
const
|
|
96
|
-
expect(
|
|
109
|
+
const v3 = await c.getOrSet({ key: "e", factory: () => "fresh2" });
|
|
110
|
+
expect(v3).toBe("fresh2");
|
|
97
111
|
});
|
|
98
112
|
|
|
99
113
|
it("namespace groups keys", async () => {
|
|
100
114
|
const users = c.namespace("users");
|
|
101
115
|
await users.set({ key: "1", value: { name: "John" }, ttl: "5m" });
|
|
102
116
|
await users.set({ key: "2", value: { name: "Jane" } });
|
|
103
|
-
|
|
104
|
-
expect(
|
|
117
|
+
const u1 = await users.get({ key: "1" });
|
|
118
|
+
expect(u1).toEqual({ name: "John" });
|
|
119
|
+
const root = await c.get({ key: "1" });
|
|
120
|
+
expect(root).toBeUndefined(); // not in root
|
|
105
121
|
await users.clear();
|
|
106
122
|
expect(await users.missing({ key: "1" })).toBe(true);
|
|
107
123
|
expect(await users.missing({ key: "2" })).toBe(true);
|
|
@@ -116,9 +132,11 @@ describe("Cache (filesystem)", () => {
|
|
|
116
132
|
await a.disconnect();
|
|
117
133
|
|
|
118
134
|
const b = useFilesystemCache({ directory: dir });
|
|
119
|
-
|
|
135
|
+
const v = await b.get({ key: "persist" });
|
|
136
|
+
expect(v).toEqual({ ok: true });
|
|
120
137
|
await b.delete({ key: "persist" });
|
|
121
|
-
|
|
138
|
+
const gone = await b.get({ key: "persist" });
|
|
139
|
+
expect(gone).toBeUndefined();
|
|
122
140
|
await b.disconnect();
|
|
123
141
|
});
|
|
124
142
|
});
|
package/utils/cache.ts
CHANGED
|
@@ -3,11 +3,9 @@ import { memoryDriver } from "bentocache/drivers/memory";
|
|
|
3
3
|
import { redisDriver } from "bentocache/drivers/redis";
|
|
4
4
|
import { fileDriver } from "bentocache/drivers/file";
|
|
5
5
|
import path from "path";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
fileDriver,
|
|
10
|
-
}
|
|
6
|
+
|
|
7
|
+
/** The underlying BentoCache instance — full provider API, with autocompletion */
|
|
8
|
+
export type Cache = InstanceType<typeof BentoCache>;
|
|
11
9
|
|
|
12
10
|
type Driver = 'memory' | 'redis' | 'filesystem';
|
|
13
11
|
|
|
@@ -25,50 +23,27 @@ type CacheOptions = {
|
|
|
25
23
|
pruneInterval?: string;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
constructor(driver: Driver, options?: CacheOptions) {
|
|
31
|
-
let store = bentostore().useL1Layer(memoryDriver({
|
|
32
|
-
}))
|
|
33
|
-
|
|
34
|
-
if (driver == 'filesystem') {
|
|
35
|
-
store = store.useL2Layer(fileDriver({
|
|
36
|
-
directory: options?.filesystem?.directory ?? path.join(process.cwd(), '.cache'),
|
|
37
|
-
pruneInterval: options?.pruneInterval ?? '1h'
|
|
38
|
-
}))
|
|
39
|
-
}
|
|
26
|
+
function createCache(driver: Driver, options?: CacheOptions): Cache {
|
|
27
|
+
let store = bentostore().useL1Layer(memoryDriver({}))
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
this.#cache = new BentoCache({
|
|
47
|
-
default: driver,
|
|
48
|
-
stores: {
|
|
49
|
-
[driver]: store,
|
|
50
|
-
}
|
|
51
|
-
})
|
|
29
|
+
if (driver == 'filesystem') {
|
|
30
|
+
store = store.useL2Layer(fileDriver({
|
|
31
|
+
directory: options?.filesystem?.directory ?? path.join(process.cwd(), '.cache'),
|
|
32
|
+
pruneInterval: options?.pruneInterval ?? '1h'
|
|
33
|
+
}))
|
|
52
34
|
}
|
|
53
35
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
deleteMany(options: any): Promise<boolean> { return this.#cache.deleteMany(options); }
|
|
66
|
-
deleteByTag(options: any): Promise<boolean> { return this.#cache.deleteByTag(options); }
|
|
67
|
-
expire(options: any): Promise<boolean> { return this.#cache.expire(options); }
|
|
68
|
-
clear(options?: any): Promise<void> { return this.#cache.clear(options); }
|
|
69
|
-
prune(): Promise<void> { return this.#cache.prune(); }
|
|
70
|
-
namespace(namespace: string): any { return this.#cache.namespace(namespace); }
|
|
71
|
-
disconnect(): Promise<void> { return this.#cache.disconnect(); }
|
|
36
|
+
if (driver == 'redis' && options?.redis?.connection) {
|
|
37
|
+
store = store.useL2Layer(redisDriver({
|
|
38
|
+
connection: options.redis.connection
|
|
39
|
+
}))
|
|
40
|
+
}
|
|
41
|
+
return new BentoCache({
|
|
42
|
+
default: driver,
|
|
43
|
+
stores: {
|
|
44
|
+
[driver]: store,
|
|
45
|
+
}
|
|
46
|
+
})
|
|
72
47
|
}
|
|
73
48
|
|
|
74
49
|
|
|
@@ -79,8 +54,8 @@ function useRedisCache(options: {
|
|
|
79
54
|
password: string;
|
|
80
55
|
};
|
|
81
56
|
pruneInterval?: string;
|
|
82
|
-
}) {
|
|
83
|
-
return
|
|
57
|
+
}): Cache {
|
|
58
|
+
return createCache('redis', {
|
|
84
59
|
redis: {
|
|
85
60
|
connection: options.connection,
|
|
86
61
|
},
|
|
@@ -88,15 +63,15 @@ function useRedisCache(options: {
|
|
|
88
63
|
});
|
|
89
64
|
}
|
|
90
65
|
|
|
91
|
-
function useMemoryCache() {
|
|
92
|
-
return
|
|
66
|
+
function useMemoryCache(): Cache {
|
|
67
|
+
return createCache('memory');
|
|
93
68
|
}
|
|
94
69
|
|
|
95
70
|
function useFilesystemCache(options: {
|
|
96
71
|
directory: string;
|
|
97
72
|
pruneInterval?: string;
|
|
98
|
-
}) {
|
|
99
|
-
return
|
|
73
|
+
}): Cache {
|
|
74
|
+
return createCache('filesystem', {
|
|
100
75
|
filesystem: {
|
|
101
76
|
directory: options.directory,
|
|
102
77
|
},
|
|
@@ -106,9 +81,8 @@ function useFilesystemCache(options: {
|
|
|
106
81
|
|
|
107
82
|
|
|
108
83
|
|
|
109
|
-
export default Cache;
|
|
110
84
|
export {
|
|
111
|
-
|
|
85
|
+
|
|
112
86
|
useFilesystemCache,
|
|
113
87
|
useMemoryCache,
|
|
114
88
|
useRedisCache
|
package/bench-cache.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// Scratch benchmark — `bun run bench-cache.ts` (depuis packages/core)
|
|
2
|
-
// Vérifie que useCache fonctionne vraiment et mesure les ms (miss vs hit vs sans cache).
|
|
3
|
-
import { useRest } from "./database/rest";
|
|
4
|
-
import { formatConfig, cfg } from "./server/config";
|
|
5
|
-
import { syncTenants } from "./database/tenant";
|
|
6
|
-
import { syncCollections } from "./database/collection";
|
|
7
|
-
|
|
8
|
-
const TENANT = "bench";
|
|
9
|
-
const DB = "mongodb://localhost:27017/_DB_BENCH";
|
|
10
|
-
const N = 10000; // docs seedés
|
|
11
|
-
|
|
12
|
-
async function main() {
|
|
13
|
-
formatConfig({
|
|
14
|
-
server: { port: 4000, cache: { enabled: true, driver: "memory" } },
|
|
15
|
-
tenants: [{ id: TENANT, dir: "bench-tenant", database: { uri: DB } }],
|
|
16
|
-
});
|
|
17
|
-
await syncTenants();
|
|
18
|
-
await syncCollections();
|
|
19
|
-
|
|
20
|
-
(cfg as any).collections = [
|
|
21
|
-
{ _tenant_: TENANT, slug: "items", fields: [{ name: "title", type: "string" }, { name: "count", type: "number" }], api: { access: { "*": true } } },
|
|
22
|
-
];
|
|
23
|
-
const rest = new useRest({ internal: false, tenant_id: TENANT });
|
|
24
|
-
const col = rest.db.collection("items");
|
|
25
|
-
|
|
26
|
-
// Seed
|
|
27
|
-
console.log(`Seeding ${N} docs…`);
|
|
28
|
-
const batch = [];
|
|
29
|
-
for (let i = 0; i < N; i++) batch.push({ title: `item-${i % 100}`, count: i });
|
|
30
|
-
for (let i = 0; i < batch.length; i += 1000) await col.insertMany(batch.slice(i, i + 1000));
|
|
31
|
-
console.log("Seeded ✓");
|
|
32
|
-
|
|
33
|
-
const params = { $match: { count: { $gte: 1000, $lte: 9000 } }, $sort: { count: -1 }, $limit: 50 };
|
|
34
|
-
|
|
35
|
-
// 1. Sans cache — moyenne sur 100 requêtes
|
|
36
|
-
const t0 = performance.now();
|
|
37
|
-
const ITERS = 100;
|
|
38
|
-
for (let i = 0; i < ITERS; i++) await rest.find("items", params);
|
|
39
|
-
const noCacheAvg = (performance.now() - t0) / ITERS;
|
|
40
|
-
|
|
41
|
-
// 2. Avec cache — 1er appel (MISS → requête DB) puis hits
|
|
42
|
-
const tMiss = performance.now();
|
|
43
|
-
const r1 = await rest.find("items", params, { useCache: true });
|
|
44
|
-
const missMs = performance.now() - tMiss;
|
|
45
|
-
console.log(`MISS: ${r1.length} docs retournés (requête DB réelle)`);
|
|
46
|
-
|
|
47
|
-
const tHits = performance.now();
|
|
48
|
-
for (let i = 0; i < ITERS; i++) await rest.find("items", params, { useCache: true });
|
|
49
|
-
const hitAvg = (performance.now() - tHits) / ITERS;
|
|
50
|
-
|
|
51
|
-
// 3. Preuve que le hit sert BIEN depuis le cache (pas la DB) :
|
|
52
|
-
// on mute DIRECTEMENT en DB (bypass framework) un doc présent dans le résultat.
|
|
53
|
-
await col.updateMany({ count: 9000 }, { $set: { title: "MUTATED-DIRECT-DB" } });
|
|
54
|
-
const cached = await rest.find("items", params, { useCache: true });
|
|
55
|
-
const dbFresh = await rest.find("items", params);
|
|
56
|
-
const stale = cached.some((d: any) => d.title === "MUTATED-DIRECT-DB");
|
|
57
|
-
const fresh = dbFresh.some((d: any) => d.title === "MUTATED-DIRECT-DB");
|
|
58
|
-
|
|
59
|
-
console.log("\n─── RÉSULTATS (driver memory, Mongo local) ───");
|
|
60
|
-
console.log(`Sans cache : ${noCacheAvg.toFixed(3)} ms/requête (${ITERS} requêtes)`);
|
|
61
|
-
console.log(`MISS (cache): ${missMs.toFixed(3)} ms (1er appel → requête DB, résultat mis en cache)`);
|
|
62
|
-
console.log(`HIT (cache) : ${hitAvg.toFixed(3)} ms/requête (${ITERS} requêtes)`);
|
|
63
|
-
console.log(`Gain : ${(noCacheAvg / hitAvg).toFixed(0)}× plus rapide`);
|
|
64
|
-
console.log(`Preuve cache: cache sert ${stale ? "la VALEUR MUTÉE (bug! cache pas servi)" : "l'ANCIENNE valeur (cache servi ✓)"}, requête fraîche = ${fresh ? "nouvelle valeur (DB bien mutée ✓)" : "?"}`);
|
|
65
|
-
|
|
66
|
-
await col.drop();
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
main().catch((e) => { console.error(e); process.exit(1); });
|