@c9up/echo 0.1.14 → 0.1.16
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/dist/StoreManager.d.ts +18 -0
- package/dist/StoreManager.d.ts.map +1 -1
- package/dist/StoreManager.js +18 -2
- package/dist/StoreManager.js.map +1 -1
- package/dist/drivers/MemoryDriver.d.ts +1 -1
- package/dist/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/drivers/MemoryDriver.js +41 -1
- package/dist/drivers/MemoryDriver.js.map +1 -1
- package/dist/quasar.d.ts +24 -6
- package/dist/quasar.d.ts.map +1 -1
- package/dist/quasar.js +112 -51
- package/dist/quasar.js.map +1 -1
- package/dist/vendor/quasarConnection.d.ts +35 -0
- package/dist/vendor/quasarConnection.d.ts.map +1 -0
- package/dist/vendor/quasarConnection.js +62 -0
- package/dist/vendor/quasarConnection.js.map +1 -0
- package/package.json +1 -1
- package/src/StoreManager.ts +22 -3
- package/src/drivers/MemoryDriver.ts +39 -1
- package/src/quasar.ts +135 -58
- package/src/vendor/quasarConnection.ts +129 -0
package/dist/StoreManager.d.ts
CHANGED
|
@@ -71,8 +71,14 @@ export declare class Store {
|
|
|
71
71
|
export declare function store(options?: Omit<StoreConfig, "driver">): Store;
|
|
72
72
|
/** Driver factory helpers (bento `drivers.memory` / `drivers.redis`). */
|
|
73
73
|
export declare const drivers: {
|
|
74
|
+
/**
|
|
75
|
+
* `maxItems` bounds the store by COUNT, which a TTL cannot: a cache keyed by
|
|
76
|
+
* a user id or a search term grows until the process runs out of memory,
|
|
77
|
+
* however short each entry's life. Omit it for no ceiling.
|
|
78
|
+
*/
|
|
74
79
|
memory(options?: {
|
|
75
80
|
sweepIntervalMs?: number;
|
|
81
|
+
maxItems?: number;
|
|
76
82
|
}): DriverFactory;
|
|
77
83
|
/**
|
|
78
84
|
* Either hand it a client, or name a quasar connection — the AdonisJS shape,
|
|
@@ -87,6 +93,18 @@ export declare const drivers: {
|
|
|
87
93
|
connection?: string;
|
|
88
94
|
prefix?: string;
|
|
89
95
|
}): DriverFactory;
|
|
96
|
+
/**
|
|
97
|
+
* Redis pub/sub as the bus that keeps each instance's L1 in step.
|
|
98
|
+
*
|
|
99
|
+
* Named alongside `redis` and taking the same `connection`, because it is
|
|
100
|
+
* the same deployment decision: a two-layer store without a bus is wrong as
|
|
101
|
+
* soon as a second instance exists, each process serving its own L1 copy of
|
|
102
|
+
* a key another has already deleted.
|
|
103
|
+
*/
|
|
104
|
+
redisBus(options?: {
|
|
105
|
+
connection?: string;
|
|
106
|
+
channel?: string;
|
|
107
|
+
}): CacheBus;
|
|
90
108
|
tiered(options: {
|
|
91
109
|
l1: DriverFactory;
|
|
92
110
|
l2: DriverFactory;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoreManager.d.ts","sourceRoot":"","sources":["../src/StoreManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,KAAK,QAAQ,EAAgB,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3E,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,WAAW,CAAC,EAAE,QAAQ,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAAC,CAAC;IAC5C,6CAA6C;IAC7C,OAAO,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,KAAK;;gBAML,OAAO,GAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAM;IAIrD,sCAAsC;IACtC,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAKvC,gFAAgF;IAChF,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAKvC,oEAAoE;IACpE,MAAM,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI;IAK3B,uDAAuD;IACvD,KAAK,IAAI,WAAW;CAsBpB;AAED,uDAAuD;AACvD,wBAAgB,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,KAAK,CAElE;AAOD,yEAAyE;AACzE,eAAO,MAAM,OAAO;
|
|
1
|
+
{"version":3,"file":"StoreManager.d.ts","sourceRoot":"","sources":["../src/StoreManager.ts"],"names":[],"mappings":"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"}
|
package/dist/StoreManager.js
CHANGED
|
@@ -18,7 +18,7 @@ import { CacheManager } from "./CacheManager.js";
|
|
|
18
18
|
import { MemoryDriver } from "./drivers/MemoryDriver.js";
|
|
19
19
|
import { RedisDriver } from "./drivers/RedisDriver.js";
|
|
20
20
|
import { TieredDriver } from "./drivers/TieredDriver.js";
|
|
21
|
-
import { quasarConnection } from "./quasar.js";
|
|
21
|
+
import { quasarBus, quasarConnection } from "./quasar.js";
|
|
22
22
|
/**
|
|
23
23
|
* A store, described the way a cache config describes one: a layer at a time.
|
|
24
24
|
*
|
|
@@ -85,8 +85,13 @@ function entryOf(config) {
|
|
|
85
85
|
}
|
|
86
86
|
/** Driver factory helpers (bento `drivers.memory` / `drivers.redis`). */
|
|
87
87
|
export const drivers = {
|
|
88
|
+
/**
|
|
89
|
+
* `maxItems` bounds the store by COUNT, which a TTL cannot: a cache keyed by
|
|
90
|
+
* a user id or a search term grows until the process runs out of memory,
|
|
91
|
+
* however short each entry's life. Omit it for no ceiling.
|
|
92
|
+
*/
|
|
88
93
|
memory(options) {
|
|
89
|
-
return () => new MemoryDriver(options?.sweepIntervalMs);
|
|
94
|
+
return () => new MemoryDriver(options?.sweepIntervalMs, options?.maxItems);
|
|
90
95
|
},
|
|
91
96
|
/**
|
|
92
97
|
* Either hand it a client, or name a quasar connection — the AdonisJS shape,
|
|
@@ -100,6 +105,17 @@ export const drivers = {
|
|
|
100
105
|
}
|
|
101
106
|
return () => new RedisDriver(quasarConnection(options.connection), options.prefix);
|
|
102
107
|
},
|
|
108
|
+
/**
|
|
109
|
+
* Redis pub/sub as the bus that keeps each instance's L1 in step.
|
|
110
|
+
*
|
|
111
|
+
* Named alongside `redis` and taking the same `connection`, because it is
|
|
112
|
+
* the same deployment decision: a two-layer store without a bus is wrong as
|
|
113
|
+
* soon as a second instance exists, each process serving its own L1 copy of
|
|
114
|
+
* a key another has already deleted.
|
|
115
|
+
*/
|
|
116
|
+
redisBus(options) {
|
|
117
|
+
return quasarBus(options);
|
|
118
|
+
},
|
|
103
119
|
tiered(options) {
|
|
104
120
|
return () => new TieredDriver({
|
|
105
121
|
l1: options.l1(),
|
package/dist/StoreManager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoreManager.js","sourceRoot":"","sources":["../src/StoreManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAoB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAiB,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAExE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"StoreManager.js","sourceRoot":"","sources":["../src/StoreManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { CacheEntry, DriverSetOptions, TaggableDriver } from "../types.js";
|
|
7
7
|
export declare class MemoryDriver implements TaggableDriver {
|
|
8
8
|
#private;
|
|
9
|
-
constructor(sweepIntervalMs?: number);
|
|
9
|
+
constructor(sweepIntervalMs?: number, maxItems?: number);
|
|
10
10
|
/**
|
|
11
11
|
* Drop expired entries now, without waiting for the sweep (bentocache
|
|
12
12
|
* `prune`). Useful before measuring size, or in a test that must not depend
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemoryDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/MemoryDriver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAWhF,qBAAa,YAAa,YAAW,cAAc;;
|
|
1
|
+
{"version":3,"file":"MemoryDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/MemoryDriver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAWhF,qBAAa,YAAa,YAAW,cAAc;;gBAetC,eAAe,SAAS,EAAE,QAAQ,SAAI;IA2ClD;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC,OAAO,IAAI,IAAI;IAIT,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAMhD,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAIvE,+FAA+F;IAC/F,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IA0EtD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE,QAAQ,CACb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC;IAWV,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxC,qEAAqE;IAC/D,WAAW,CAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAIhB,gEAAgE;IAC1D,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBhD,gDAAgD;IAC1C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAG9C"}
|
|
@@ -7,13 +7,48 @@ export class MemoryDriver {
|
|
|
7
7
|
#store = new Map();
|
|
8
8
|
#tagIndex = new Map();
|
|
9
9
|
#sweepInterval;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* How many entries this driver holds before it starts dropping the oldest.
|
|
12
|
+
*
|
|
13
|
+
* Eviction was by TTL alone, which bounds how long an entry lives and not
|
|
14
|
+
* how many there are: a cache keyed by anything unbounded — a user id, a
|
|
15
|
+
* search term — grew until the process ran out of memory, and only a key
|
|
16
|
+
* that was written ever left. `0` keeps the old behaviour for a caller that
|
|
17
|
+
* genuinely wants no ceiling.
|
|
18
|
+
*/
|
|
19
|
+
#maxItems;
|
|
20
|
+
constructor(sweepIntervalMs = 60_000, maxItems = 0) {
|
|
21
|
+
this.#maxItems = maxItems > 0 ? maxItems : 0;
|
|
11
22
|
this.#sweepInterval = setInterval(() => this.#sweep(), sweepIntervalMs);
|
|
12
23
|
if (typeof this.#sweepInterval === "object" &&
|
|
13
24
|
"unref" in this.#sweepInterval) {
|
|
14
25
|
this.#sweepInterval.unref();
|
|
15
26
|
}
|
|
16
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Drop the oldest entries until the store fits under its ceiling.
|
|
30
|
+
*
|
|
31
|
+
* Expired ones go first — evicting a live entry while a dead one sits in
|
|
32
|
+
* the map would throw away a value someone still wants. Only if that is not
|
|
33
|
+
* enough does it take the least recently written.
|
|
34
|
+
*/
|
|
35
|
+
#enforceCeiling() {
|
|
36
|
+
if (this.#maxItems === 0 || this.#store.size <= this.#maxItems)
|
|
37
|
+
return;
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
for (const [key, entry] of this.#store) {
|
|
40
|
+
if (this.#store.size <= this.#maxItems)
|
|
41
|
+
return;
|
|
42
|
+
if (entry.staleUntil > 0 && entry.staleUntil < now) {
|
|
43
|
+
this.#evict(key, entry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [key, entry] of this.#store) {
|
|
47
|
+
if (this.#store.size <= this.#maxItems)
|
|
48
|
+
return;
|
|
49
|
+
this.#evict(key, entry);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
17
52
|
/** Evict every entry past its grace window. */
|
|
18
53
|
#sweep() {
|
|
19
54
|
const now = Date.now();
|
|
@@ -100,7 +135,12 @@ export class MemoryDriver {
|
|
|
100
135
|
for (const t of prev.tags)
|
|
101
136
|
this.#tagIndex.get(t)?.delete(key);
|
|
102
137
|
}
|
|
138
|
+
// Re-inserting moves the key to the end of the Map's iteration order, so
|
|
139
|
+
// the first entry is always the least recently WRITTEN — which is the
|
|
140
|
+
// order this evicts in.
|
|
141
|
+
this.#store.delete(key);
|
|
103
142
|
this.#store.set(key, { value, expiresAt, staleUntil, tags });
|
|
143
|
+
this.#enforceCeiling();
|
|
104
144
|
for (const tag of tags) {
|
|
105
145
|
let set = this.#tagIndex.get(tag);
|
|
106
146
|
if (!set) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemoryDriver.js","sourceRoot":"","sources":["../../src/drivers/MemoryDriver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,MAAM,OAAO,YAAY;IACxB,MAAM,GAA6B,IAAI,GAAG,EAAE,CAAC;IAC7C,SAAS,GAA6B,IAAI,GAAG,EAAE,CAAC;IAChD,cAAc,CAAiC;
|
|
1
|
+
{"version":3,"file":"MemoryDriver.js","sourceRoot":"","sources":["../../src/drivers/MemoryDriver.ts"],"names":[],"mappings":"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"}
|
package/dist/quasar.d.ts
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
3
3
|
*
|
|
4
|
-
* Echo does not depend on quasar: it is an optional peer, and this
|
|
5
|
-
*
|
|
6
|
-
* TypeScript build stays free of it too
|
|
7
|
-
* unbuildable for anyone who caches in memory.
|
|
4
|
+
* Echo does not depend on quasar: it is an optional peer, and neither this
|
|
5
|
+
* module nor the vendored loader it calls imports it statically. The specifier
|
|
6
|
+
* is built at runtime so the TypeScript build stays free of it too.
|
|
8
7
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
8
|
+
* The loading, the shape check and the messages are the same in every package
|
|
9
|
+
* that offers a Redis-backed option, so they are vendored rather than written
|
|
10
|
+
* again: `src/vendor/quasarConnection.ts`, generated from one source. The bus
|
|
11
|
+
* below is echo's alone and stays here.
|
|
11
12
|
*/
|
|
12
13
|
import type { RedisClient } from "./drivers/RedisDriver.js";
|
|
14
|
+
import type { CacheBus } from "./drivers/TieredDriver.js";
|
|
13
15
|
/**
|
|
14
16
|
* A resolver for `drivers.redis({ connection })` — quasar is loaded on the
|
|
15
17
|
* first cache command, not at config time.
|
|
16
18
|
*/
|
|
17
19
|
export declare function quasarConnection(name?: string): () => Promise<RedisClient>;
|
|
20
|
+
/**
|
|
21
|
+
* Redis pub/sub as a cache bus, for keeping each instance's L1 in step.
|
|
22
|
+
*
|
|
23
|
+
* A two-layer store without one is wrong the moment a second instance exists:
|
|
24
|
+
* each process keeps serving its own L1 copy of a key another process has
|
|
25
|
+
* already deleted, and nothing ever tells it. That is what the bus is for, and
|
|
26
|
+
* it is why `useBus` sits next to `useL2Layer` in the generated config.
|
|
27
|
+
*
|
|
28
|
+
* Quasar opens the subscriber socket lazily and on its own connection — Redis
|
|
29
|
+
* puts a subscribed client into a mode where it accepts nothing else, so a
|
|
30
|
+
* connection that both publishes and listens needs two.
|
|
31
|
+
*/
|
|
32
|
+
export declare function quasarBus(options?: {
|
|
33
|
+
connection?: string;
|
|
34
|
+
channel?: string;
|
|
35
|
+
}): CacheBus;
|
|
18
36
|
//# sourceMappingURL=quasar.d.ts.map
|
package/dist/quasar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quasar.d.ts","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"quasar.d.ts","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAc,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAwBtE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAS1E;AAqDD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,QAAQ,CA8CX"}
|
package/dist/quasar.js
CHANGED
|
@@ -1,69 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
3
3
|
*
|
|
4
|
-
* Echo does not depend on quasar: it is an optional peer, and this
|
|
5
|
-
*
|
|
6
|
-
* TypeScript build stays free of it too
|
|
7
|
-
* unbuildable for anyone who caches in memory.
|
|
4
|
+
* Echo does not depend on quasar: it is an optional peer, and neither this
|
|
5
|
+
* module nor the vendored loader it calls imports it statically. The specifier
|
|
6
|
+
* is built at runtime so the TypeScript build stays free of it too.
|
|
8
7
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
8
|
+
* The loading, the shape check and the messages are the same in every package
|
|
9
|
+
* that offers a Redis-backed option, so they are vendored rather than written
|
|
10
|
+
* again: `src/vendor/quasarConnection.ts`, generated from one source. The bus
|
|
11
|
+
* below is echo's alone and stays here.
|
|
11
12
|
*/
|
|
13
|
+
import { quasarConnection as loadQuasarConnection } from "./vendor/quasarConnection.js";
|
|
14
|
+
/**
|
|
15
|
+
* The commands this driver actually issues.
|
|
16
|
+
*
|
|
17
|
+
* `keys` and `exists` are deliberately absent: `RedisClient` declares them, but
|
|
18
|
+
* nothing here calls them — `flush()` walks a SCAN cursor because it considers
|
|
19
|
+
* KEYS unsafe in production, so demanding `keys` would reject a client that
|
|
20
|
+
* omits it on exactly the grounds this driver agrees with. `scan` is optional
|
|
21
|
+
* on the interface, and `flush()` reports its absence with a message of its
|
|
22
|
+
* own.
|
|
23
|
+
*/
|
|
24
|
+
const REQUIRED = [
|
|
25
|
+
"get",
|
|
26
|
+
"set",
|
|
27
|
+
"del",
|
|
28
|
+
"sadd",
|
|
29
|
+
"srem",
|
|
30
|
+
"smembers",
|
|
31
|
+
"expire",
|
|
32
|
+
"ttl",
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* A resolver for `drivers.redis({ connection })` — quasar is loaded on the
|
|
36
|
+
* first cache command, not at config time.
|
|
37
|
+
*/
|
|
38
|
+
export function quasarConnection(name) {
|
|
39
|
+
return async () => loadQuasarConnection({
|
|
40
|
+
pkg: "echo",
|
|
41
|
+
name,
|
|
42
|
+
required: REQUIRED,
|
|
43
|
+
what: "the cache store",
|
|
44
|
+
raise: (_reason, message, cause) => new Error(message, { cause }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
12
47
|
function isConnectionSource(value) {
|
|
13
48
|
return (typeof value === "object" &&
|
|
14
49
|
value !== null &&
|
|
50
|
+
"connection" in value &&
|
|
15
51
|
typeof Reflect.get(value, "connection") === "function");
|
|
16
52
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
// `keys` and `exists` are deliberately NOT required: `RedisClient` declares
|
|
24
|
-
// them, but nothing here calls them — `flush()` walks a SCAN cursor because
|
|
25
|
-
// it considers KEYS unsafe in production. Demanding `keys` would reject a
|
|
26
|
-
// client that omits it on exactly the grounds this driver agrees with.
|
|
27
|
-
// `scan` is optional on the interface, and `flush()` reports its absence
|
|
28
|
-
// with a precise message of its own.
|
|
29
|
-
const required = [
|
|
30
|
-
"get",
|
|
31
|
-
"set",
|
|
32
|
-
"del",
|
|
33
|
-
"sadd",
|
|
34
|
-
"srem",
|
|
35
|
-
"smembers",
|
|
36
|
-
"expire",
|
|
37
|
-
"ttl",
|
|
38
|
-
];
|
|
39
|
-
return required.every((name) => typeof Reflect.get(value, name) === "function");
|
|
53
|
+
function isPubSubSource(value) {
|
|
54
|
+
return (typeof value === "object" &&
|
|
55
|
+
value !== null &&
|
|
56
|
+
typeof Reflect.get(value, "publish") === "function" &&
|
|
57
|
+
typeof Reflect.get(value, "subscribe") === "function");
|
|
40
58
|
}
|
|
41
59
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
60
|
+
* Resolve quasar's manager, loading it on first use.
|
|
61
|
+
*
|
|
62
|
+
* Shared by the connection resolver and the bus: echo declares quasar an
|
|
63
|
+
* optional peer and never imports it statically, so an application that caches
|
|
64
|
+
* in memory neither installs it nor pays for it.
|
|
44
65
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
loaded = await import(/* @vite-ignore */ specifier);
|
|
51
|
-
}
|
|
52
|
-
catch (cause) {
|
|
53
|
-
throw new Error(`Echo: the "${name ?? "default"}" cache store asks for a quasar connection, but @c9up/quasar is not installed.\n` +
|
|
54
|
-
" pnpm add @c9up/quasar", { cause });
|
|
55
|
-
}
|
|
56
|
-
const manager = isConnectionSource(loaded)
|
|
66
|
+
async function loadQuasar(context) {
|
|
67
|
+
const specifier = "@c9up/quasar/services/main";
|
|
68
|
+
try {
|
|
69
|
+
const loaded = await import(/* @vite-ignore */ specifier);
|
|
70
|
+
return isConnectionSource(loaded) || isPubSubSource(loaded)
|
|
57
71
|
? loaded
|
|
58
72
|
: Reflect.get(Object(loaded), "default");
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
throw new Error(`Echo: ${context}, but @c9up/quasar is not installed.\n pnpm add @c9up/quasar`, { cause });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Redis pub/sub as a cache bus, for keeping each instance's L1 in step.
|
|
80
|
+
*
|
|
81
|
+
* A two-layer store without one is wrong the moment a second instance exists:
|
|
82
|
+
* each process keeps serving its own L1 copy of a key another process has
|
|
83
|
+
* already deleted, and nothing ever tells it. That is what the bus is for, and
|
|
84
|
+
* it is why `useBus` sits next to `useL2Layer` in the generated config.
|
|
85
|
+
*
|
|
86
|
+
* Quasar opens the subscriber socket lazily and on its own connection — Redis
|
|
87
|
+
* puts a subscribed client into a mode where it accepts nothing else, so a
|
|
88
|
+
* connection that both publishes and listens needs two.
|
|
89
|
+
*/
|
|
90
|
+
export function quasarBus(options) {
|
|
91
|
+
const channel = options?.channel ?? "echo::invalidate";
|
|
92
|
+
let ready;
|
|
93
|
+
const manager = () => {
|
|
94
|
+
ready ??= loadQuasar(`the cache bus asks for the "${options?.connection ?? "default"}" quasar connection`).then((loaded) => {
|
|
95
|
+
const source = isConnectionSource(loaded)
|
|
96
|
+
? loaded.connection(options?.connection)
|
|
97
|
+
: loaded;
|
|
98
|
+
if (!isPubSubSource(source)) {
|
|
99
|
+
throw new Error("Echo: the quasar connection does not expose publish()/subscribe()");
|
|
100
|
+
}
|
|
101
|
+
return source;
|
|
102
|
+
});
|
|
103
|
+
return ready;
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
async publish(message) {
|
|
107
|
+
const source = await manager();
|
|
108
|
+
await source.publish(channel, JSON.stringify(message));
|
|
109
|
+
},
|
|
110
|
+
subscribe(handler) {
|
|
111
|
+
// Not awaited: `subscribe` is synchronous in the bus contract, and the
|
|
112
|
+
// socket opens on quasar's own schedule. A failure to reach Redis must
|
|
113
|
+
// not take down the store — a bus that is down costs staleness, and
|
|
114
|
+
// throwing here would cost every cache read.
|
|
115
|
+
void manager()
|
|
116
|
+
.then((source) => source.subscribe(channel, (raw) => {
|
|
117
|
+
try {
|
|
118
|
+
handler(JSON.parse(raw));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* a malformed frame is not a reason to stop listening */
|
|
122
|
+
}
|
|
123
|
+
}))
|
|
124
|
+
.catch(() => {
|
|
125
|
+
/* reported by the first publish, which does surface its error */
|
|
126
|
+
});
|
|
127
|
+
},
|
|
67
128
|
};
|
|
68
129
|
}
|
|
69
130
|
//# sourceMappingURL=quasar.js.map
|
package/dist/quasar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quasar.js","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"quasar.js","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface QuasarConnectionRequest {
|
|
2
|
+
/** The package asking. It prefixes every message this can raise. */
|
|
3
|
+
pkg: string;
|
|
4
|
+
/**
|
|
5
|
+
* How this package builds an error.
|
|
6
|
+
*
|
|
7
|
+
* Not optional, and not `new Error` by default: a package with its own
|
|
8
|
+
* error type carries codes (`E_<PKG>_<REASON>`) that a caller catches on,
|
|
9
|
+
* and a shared helper throwing a bare Error would silently drop them. The
|
|
10
|
+
* two reasons are distinguished so the codes can be too.
|
|
11
|
+
*/
|
|
12
|
+
raise: (reason: "quasar-missing" | "incomplete-connection", message: string, cause?: unknown) => Error;
|
|
13
|
+
/** Connection name, or `undefined` for quasar's default. */
|
|
14
|
+
name?: string;
|
|
15
|
+
/** The commands the caller will actually issue. */
|
|
16
|
+
required: readonly string[];
|
|
17
|
+
/** What the caller does with them, for the message that names a gap. */
|
|
18
|
+
what: string;
|
|
19
|
+
/**
|
|
20
|
+
* A second way out, when this package has one.
|
|
21
|
+
*
|
|
22
|
+
* Several of them accept a client object directly, so "pass a client
|
|
23
|
+
* instead of a connection name" is actionable — and it was in each
|
|
24
|
+
* package's own message before this was shared. Dropping it to unify the
|
|
25
|
+
* wording would have made the error strictly less useful.
|
|
26
|
+
*/
|
|
27
|
+
alternative?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The named connection, checked for the commands the caller needs BEFORE it is
|
|
31
|
+
* handed over — a connection missing one would otherwise fail on the first
|
|
32
|
+
* command, far from the line that asked for it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function quasarConnection<T>(request: QuasarConnectionRequest): Promise<T>;
|
|
35
|
+
//# sourceMappingURL=quasarConnection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quasarConnection.d.ts","sourceRoot":"","sources":["../../src/vendor/quasarConnection.ts"],"names":[],"mappings":"AAiDA,MAAM,WAAW,uBAAuB;IACvC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,KAAK,EAAE,CACN,MAAM,EAAE,gBAAgB,GAAG,uBAAuB,EAClD,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,KACX,KAAK,CAAC;IACX,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACvC,OAAO,EAAE,uBAAuB,GAC9B,OAAO,CAAC,CAAC,CAAC,CAuCZ"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Generated from scripts/vendor/quasarConnection.ts — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// This package is published and built from its own repository, so the file
|
|
4
|
+
// has to exist here rather than be imported. `pnpm vendor:sync` rewrites it,
|
|
5
|
+
// and `pnpm vendor:check` fails if this copy has drifted from the original.
|
|
6
|
+
/**
|
|
7
|
+
* Read a member without assuming how the object answers.
|
|
8
|
+
*
|
|
9
|
+
* Two shapes have to work, and they disagree about `in`. Quasar's accessor is
|
|
10
|
+
* a Proxy over an empty null-prototype object with only a `get` trap, so
|
|
11
|
+
* `"connection" in manager` is FALSE while reading it returns the function —
|
|
12
|
+
* gating on `in` rejected the real manager. And a module namespace under a
|
|
13
|
+
* test double RAISES for an export it does not have, rather than answering
|
|
14
|
+
* undefined, so a bare read fails on the mock. Reading through a catch is what
|
|
15
|
+
* satisfies both.
|
|
16
|
+
*/
|
|
17
|
+
function readMember(value, name) {
|
|
18
|
+
if (typeof value !== "object" || value === null)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return Reflect.get(value, name);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isConnectionSource(value) {
|
|
28
|
+
return typeof readMember(value, "connection") === "function";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The named connection, checked for the commands the caller needs BEFORE it is
|
|
32
|
+
* handed over — a connection missing one would otherwise fail on the first
|
|
33
|
+
* command, far from the line that asked for it.
|
|
34
|
+
*/
|
|
35
|
+
export async function quasarConnection(request) {
|
|
36
|
+
const { pkg, name, required, what, raise, alternative } = request;
|
|
37
|
+
const specifier = "@c9up/quasar/services/main";
|
|
38
|
+
let loaded;
|
|
39
|
+
try {
|
|
40
|
+
loaded = await import(/* @vite-ignore */ specifier);
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
throw raise("quasar-missing", `[${pkg}] ${what} asks for the quasar connection "${name ?? "default"}", but @c9up/quasar is not installed.\n` +
|
|
44
|
+
` pnpm add @c9up/quasar${alternative ? `, ${alternative}` : ""}`, cause);
|
|
45
|
+
}
|
|
46
|
+
const manager = isConnectionSource(loaded)
|
|
47
|
+
? loaded
|
|
48
|
+
: Reflect.get(Object(loaded), "default");
|
|
49
|
+
if (!isConnectionSource(manager)) {
|
|
50
|
+
throw raise("quasar-missing", `[${pkg}] @c9up/quasar/services/main did not expose a connection() manager`);
|
|
51
|
+
}
|
|
52
|
+
const connection = manager.connection(name);
|
|
53
|
+
const missing = required.filter((command) => typeof readMember(connection, command) !== "function");
|
|
54
|
+
if (missing.length > 0) {
|
|
55
|
+
throw raise("incomplete-connection", `[${pkg}] the quasar connection${name ? ` '${name}'` : ""} is missing ${missing.join(", ")}, which ${what} issues`);
|
|
56
|
+
}
|
|
57
|
+
// Load-bearing: every member of `required` has just been checked to be a
|
|
58
|
+
// function on this object. The caller names the type it needs because only
|
|
59
|
+
// the caller knows which commands it asked for.
|
|
60
|
+
return connection;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=quasarConnection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quasarConnection.js","sourceRoot":"","sources":["../../src/vendor/quasarConnection.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,4EAA4E;AAqB5E;;;;;;;;;;GAUG;AACH,SAAS,UAAU,CAAC,KAAc,EAAE,IAAY;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,IAAI,CAAC;QACJ,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACzC,OAAO,OAAO,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,UAAU,CAAC;AAC9D,CAAC;AAmCD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,OAAgC;IAEhC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAClE,MAAM,SAAS,GAAG,4BAA4B,CAAC;IAC/C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,KAAK,CACV,gBAAgB,EAChB,IAAI,GAAG,KAAK,IAAI,oCAAoC,IAAI,IAAI,SAAS,yCAAyC;YAC7G,0BAA0B,WAAW,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAClE,KAAK,CACL,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC;QACzC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC;IAC1C,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,MAAM,KAAK,CACV,gBAAgB,EAChB,IAAI,GAAG,oEAAoE,CAC3E,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAC9B,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,UAAU,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,UAAU,CAClE,CAAC;IACF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,KAAK,CACV,uBAAuB,EACvB,IAAI,GAAG,0BAA0B,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,eAAe,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,SAAS,CAClH,CAAC;IACH,CAAC;IACD,yEAAyE;IACzE,2EAA2E;IAC3E,gDAAgD;IAChD,OAAO,UAAe,CAAC;AACxB,CAAC"}
|
package/package.json
CHANGED
package/src/StoreManager.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { MemoryDriver } from "./drivers/MemoryDriver.js";
|
|
|
20
20
|
import { type RedisClient, RedisDriver } from "./drivers/RedisDriver.js";
|
|
21
21
|
import { type CacheBus, TieredDriver } from "./drivers/TieredDriver.js";
|
|
22
22
|
import type { Duration } from "./duration.js";
|
|
23
|
-
import { quasarConnection } from "./quasar.js";
|
|
23
|
+
import { quasarBus, quasarConnection } from "./quasar.js";
|
|
24
24
|
import type { CacheDriver, CacheEmitter, CacheEventMap } from "./types.js";
|
|
25
25
|
|
|
26
26
|
/** A lazily-instantiated driver (built once per store, on first `use`). */
|
|
@@ -127,8 +127,16 @@ function entryOf(config: StoreConfig | Store): StoreConfig {
|
|
|
127
127
|
|
|
128
128
|
/** Driver factory helpers (bento `drivers.memory` / `drivers.redis`). */
|
|
129
129
|
export const drivers = {
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
/**
|
|
131
|
+
* `maxItems` bounds the store by COUNT, which a TTL cannot: a cache keyed by
|
|
132
|
+
* a user id or a search term grows until the process runs out of memory,
|
|
133
|
+
* however short each entry's life. Omit it for no ceiling.
|
|
134
|
+
*/
|
|
135
|
+
memory(options?: {
|
|
136
|
+
sweepIntervalMs?: number;
|
|
137
|
+
maxItems?: number;
|
|
138
|
+
}): DriverFactory {
|
|
139
|
+
return () => new MemoryDriver(options?.sweepIntervalMs, options?.maxItems);
|
|
132
140
|
},
|
|
133
141
|
/**
|
|
134
142
|
* Either hand it a client, or name a quasar connection — the AdonisJS shape,
|
|
@@ -147,6 +155,17 @@ export const drivers = {
|
|
|
147
155
|
return () =>
|
|
148
156
|
new RedisDriver(quasarConnection(options.connection), options.prefix);
|
|
149
157
|
},
|
|
158
|
+
/**
|
|
159
|
+
* Redis pub/sub as the bus that keeps each instance's L1 in step.
|
|
160
|
+
*
|
|
161
|
+
* Named alongside `redis` and taking the same `connection`, because it is
|
|
162
|
+
* the same deployment decision: a two-layer store without a bus is wrong as
|
|
163
|
+
* soon as a second instance exists, each process serving its own L1 copy of
|
|
164
|
+
* a key another has already deleted.
|
|
165
|
+
*/
|
|
166
|
+
redisBus(options?: { connection?: string; channel?: string }): CacheBus {
|
|
167
|
+
return quasarBus(options);
|
|
168
|
+
},
|
|
150
169
|
tiered(options: {
|
|
151
170
|
l1: DriverFactory;
|
|
152
171
|
l2: DriverFactory;
|
|
@@ -19,8 +19,19 @@ export class MemoryDriver implements TaggableDriver {
|
|
|
19
19
|
#store: Map<string, StoredEntry> = new Map();
|
|
20
20
|
#tagIndex: Map<string, Set<string>> = new Map();
|
|
21
21
|
#sweepInterval: ReturnType<typeof setInterval>;
|
|
22
|
+
/**
|
|
23
|
+
* How many entries this driver holds before it starts dropping the oldest.
|
|
24
|
+
*
|
|
25
|
+
* Eviction was by TTL alone, which bounds how long an entry lives and not
|
|
26
|
+
* how many there are: a cache keyed by anything unbounded — a user id, a
|
|
27
|
+
* search term — grew until the process ran out of memory, and only a key
|
|
28
|
+
* that was written ever left. `0` keeps the old behaviour for a caller that
|
|
29
|
+
* genuinely wants no ceiling.
|
|
30
|
+
*/
|
|
31
|
+
readonly #maxItems: number;
|
|
22
32
|
|
|
23
|
-
constructor(sweepIntervalMs = 60_000) {
|
|
33
|
+
constructor(sweepIntervalMs = 60_000, maxItems = 0) {
|
|
34
|
+
this.#maxItems = maxItems > 0 ? maxItems : 0;
|
|
24
35
|
this.#sweepInterval = setInterval(() => this.#sweep(), sweepIntervalMs);
|
|
25
36
|
if (
|
|
26
37
|
typeof this.#sweepInterval === "object" &&
|
|
@@ -30,6 +41,28 @@ export class MemoryDriver implements TaggableDriver {
|
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Drop the oldest entries until the store fits under its ceiling.
|
|
46
|
+
*
|
|
47
|
+
* Expired ones go first — evicting a live entry while a dead one sits in
|
|
48
|
+
* the map would throw away a value someone still wants. Only if that is not
|
|
49
|
+
* enough does it take the least recently written.
|
|
50
|
+
*/
|
|
51
|
+
#enforceCeiling(): void {
|
|
52
|
+
if (this.#maxItems === 0 || this.#store.size <= this.#maxItems) return;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const [key, entry] of this.#store) {
|
|
55
|
+
if (this.#store.size <= this.#maxItems) return;
|
|
56
|
+
if (entry.staleUntil > 0 && entry.staleUntil < now) {
|
|
57
|
+
this.#evict(key, entry);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const [key, entry] of this.#store) {
|
|
61
|
+
if (this.#store.size <= this.#maxItems) return;
|
|
62
|
+
this.#evict(key, entry);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
/** Evict every entry past its grace window. */
|
|
34
67
|
#sweep(): void {
|
|
35
68
|
const now = Date.now();
|
|
@@ -130,7 +163,12 @@ export class MemoryDriver implements TaggableDriver {
|
|
|
130
163
|
if (prev !== undefined) {
|
|
131
164
|
for (const t of prev.tags) this.#tagIndex.get(t)?.delete(key);
|
|
132
165
|
}
|
|
166
|
+
// Re-inserting moves the key to the end of the Map's iteration order, so
|
|
167
|
+
// the first entry is always the least recently WRITTEN — which is the
|
|
168
|
+
// order this evicts in.
|
|
169
|
+
this.#store.delete(key);
|
|
133
170
|
this.#store.set(key, { value, expiresAt, staleUntil, tags });
|
|
171
|
+
this.#enforceCeiling();
|
|
134
172
|
for (const tag of tags) {
|
|
135
173
|
let set = this.#tagIndex.get(tag);
|
|
136
174
|
if (!set) {
|
package/src/quasar.ts
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
3
3
|
*
|
|
4
|
-
* Echo does not depend on quasar: it is an optional peer, and this
|
|
5
|
-
*
|
|
6
|
-
* TypeScript build stays free of it too
|
|
7
|
-
* unbuildable for anyone who caches in memory.
|
|
4
|
+
* Echo does not depend on quasar: it is an optional peer, and neither this
|
|
5
|
+
* module nor the vendored loader it calls imports it statically. The specifier
|
|
6
|
+
* is built at runtime so the TypeScript build stays free of it too.
|
|
8
7
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
8
|
+
* The loading, the shape check and the messages are the same in every package
|
|
9
|
+
* that offers a Redis-backed option, so they are vendored rather than written
|
|
10
|
+
* again: `src/vendor/quasarConnection.ts`, generated from one source. The bus
|
|
11
|
+
* below is echo's alone and stays here.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type { RedisClient } from "./drivers/RedisDriver.js";
|
|
15
|
+
import type { BusMessage, CacheBus } from "./drivers/TieredDriver.js";
|
|
16
|
+
import { quasarConnection as loadQuasarConnection } from "./vendor/quasarConnection.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The commands this driver actually issues.
|
|
20
|
+
*
|
|
21
|
+
* `keys` and `exists` are deliberately absent: `RedisClient` declares them, but
|
|
22
|
+
* nothing here calls them — `flush()` walks a SCAN cursor because it considers
|
|
23
|
+
* KEYS unsafe in production, so demanding `keys` would reject a client that
|
|
24
|
+
* omits it on exactly the grounds this driver agrees with. `scan` is optional
|
|
25
|
+
* on the interface, and `flush()` reports its absence with a message of its
|
|
26
|
+
* own.
|
|
27
|
+
*/
|
|
28
|
+
const REQUIRED = [
|
|
29
|
+
"get",
|
|
30
|
+
"set",
|
|
31
|
+
"del",
|
|
32
|
+
"sadd",
|
|
33
|
+
"srem",
|
|
34
|
+
"smembers",
|
|
35
|
+
"expire",
|
|
36
|
+
"ttl",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A resolver for `drivers.redis({ connection })` — quasar is loaded on the
|
|
41
|
+
* first cache command, not at config time.
|
|
42
|
+
*/
|
|
43
|
+
export function quasarConnection(name?: string): () => Promise<RedisClient> {
|
|
44
|
+
return async () =>
|
|
45
|
+
loadQuasarConnection<RedisClient>({
|
|
46
|
+
pkg: "echo",
|
|
47
|
+
name,
|
|
48
|
+
required: REQUIRED,
|
|
49
|
+
what: "the cache store",
|
|
50
|
+
raise: (_reason, message, cause) => new Error(message, { cause }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
14
53
|
|
|
15
54
|
/** The slice of quasar's manager this needs: a connection, by name. */
|
|
16
55
|
interface ConnectionSource {
|
|
@@ -21,69 +60,107 @@ function isConnectionSource(value: unknown): value is ConnectionSource {
|
|
|
21
60
|
return (
|
|
22
61
|
typeof value === "object" &&
|
|
23
62
|
value !== null &&
|
|
63
|
+
"connection" in value &&
|
|
24
64
|
typeof Reflect.get(value, "connection") === "function"
|
|
25
65
|
);
|
|
26
66
|
}
|
|
27
67
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"get",
|
|
41
|
-
"set",
|
|
42
|
-
"del",
|
|
43
|
-
"sadd",
|
|
44
|
-
"srem",
|
|
45
|
-
"smembers",
|
|
46
|
-
"expire",
|
|
47
|
-
"ttl",
|
|
48
|
-
];
|
|
49
|
-
return required.every(
|
|
50
|
-
(name) => typeof Reflect.get(value, name) === "function",
|
|
68
|
+
/** The slice of quasar's manager a pub/sub bus needs. */
|
|
69
|
+
interface PubSubSource {
|
|
70
|
+
publish(channel: string, message: string): unknown;
|
|
71
|
+
subscribe(channel: string, handler: (message: string) => void): unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isPubSubSource(value: unknown): value is PubSubSource {
|
|
75
|
+
return (
|
|
76
|
+
typeof value === "object" &&
|
|
77
|
+
value !== null &&
|
|
78
|
+
typeof Reflect.get(value, "publish") === "function" &&
|
|
79
|
+
typeof Reflect.get(value, "subscribe") === "function"
|
|
51
80
|
);
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
84
|
+
* Resolve quasar's manager, loading it on first use.
|
|
85
|
+
*
|
|
86
|
+
* Shared by the connection resolver and the bus: echo declares quasar an
|
|
87
|
+
* optional peer and never imports it statically, so an application that caches
|
|
88
|
+
* in memory neither installs it nor pays for it.
|
|
57
89
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
loaded = await import(/* @vite-ignore */ specifier);
|
|
64
|
-
} catch (cause) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
`Echo: the "${name ?? "default"}" cache store asks for a quasar connection, but @c9up/quasar is not installed.\n` +
|
|
67
|
-
" pnpm add @c9up/quasar",
|
|
68
|
-
{ cause },
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const manager = isConnectionSource(loaded)
|
|
90
|
+
async function loadQuasar(context: string): Promise<unknown> {
|
|
91
|
+
const specifier = "@c9up/quasar/services/main";
|
|
92
|
+
try {
|
|
93
|
+
const loaded = await import(/* @vite-ignore */ specifier);
|
|
94
|
+
return isConnectionSource(loaded) || isPubSubSource(loaded)
|
|
73
95
|
? loaded
|
|
74
96
|
: Reflect.get(Object(loaded), "default");
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Echo: ${context}, but @c9up/quasar is not installed.\n pnpm add @c9up/quasar`,
|
|
100
|
+
{ cause },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Redis pub/sub as a cache bus, for keeping each instance's L1 in step.
|
|
107
|
+
*
|
|
108
|
+
* A two-layer store without one is wrong the moment a second instance exists:
|
|
109
|
+
* each process keeps serving its own L1 copy of a key another process has
|
|
110
|
+
* already deleted, and nothing ever tells it. That is what the bus is for, and
|
|
111
|
+
* it is why `useBus` sits next to `useL2Layer` in the generated config.
|
|
112
|
+
*
|
|
113
|
+
* Quasar opens the subscriber socket lazily and on its own connection — Redis
|
|
114
|
+
* puts a subscribed client into a mode where it accepts nothing else, so a
|
|
115
|
+
* connection that both publishes and listens needs two.
|
|
116
|
+
*/
|
|
117
|
+
export function quasarBus(options?: {
|
|
118
|
+
connection?: string;
|
|
119
|
+
channel?: string;
|
|
120
|
+
}): CacheBus {
|
|
121
|
+
const channel = options?.channel ?? "echo::invalidate";
|
|
122
|
+
let ready: Promise<PubSubSource> | undefined;
|
|
123
|
+
|
|
124
|
+
const manager = (): Promise<PubSubSource> => {
|
|
125
|
+
ready ??= loadQuasar(
|
|
126
|
+
`the cache bus asks for the "${options?.connection ?? "default"}" quasar connection`,
|
|
127
|
+
).then((loaded) => {
|
|
128
|
+
const source = isConnectionSource(loaded)
|
|
129
|
+
? loaded.connection(options?.connection)
|
|
130
|
+
: loaded;
|
|
131
|
+
if (!isPubSubSource(source)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Echo: the quasar connection does not expose publish()/subscribe()",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return source;
|
|
137
|
+
});
|
|
138
|
+
return ready;
|
|
139
|
+
};
|
|
80
140
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
141
|
+
return {
|
|
142
|
+
async publish(message: BusMessage): Promise<void> {
|
|
143
|
+
const source = await manager();
|
|
144
|
+
await source.publish(channel, JSON.stringify(message));
|
|
145
|
+
},
|
|
146
|
+
subscribe(handler: (message: BusMessage) => void): void {
|
|
147
|
+
// Not awaited: `subscribe` is synchronous in the bus contract, and the
|
|
148
|
+
// socket opens on quasar's own schedule. A failure to reach Redis must
|
|
149
|
+
// not take down the store — a bus that is down costs staleness, and
|
|
150
|
+
// throwing here would cost every cache read.
|
|
151
|
+
void manager()
|
|
152
|
+
.then((source) =>
|
|
153
|
+
source.subscribe(channel, (raw: string) => {
|
|
154
|
+
try {
|
|
155
|
+
handler(JSON.parse(raw) as BusMessage);
|
|
156
|
+
} catch {
|
|
157
|
+
/* a malformed frame is not a reason to stop listening */
|
|
158
|
+
}
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
.catch(() => {
|
|
162
|
+
/* reported by the first publish, which does surface its error */
|
|
163
|
+
});
|
|
164
|
+
},
|
|
88
165
|
};
|
|
89
166
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Generated from scripts/vendor/quasarConnection.ts — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// This package is published and built from its own repository, so the file
|
|
4
|
+
// has to exist here rather than be imported. `pnpm vendor:sync` rewrites it,
|
|
5
|
+
// and `pnpm vendor:check` fails if this copy has drifted from the original.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
9
|
+
*
|
|
10
|
+
* No package here depends on quasar: it is an optional peer, and this module
|
|
11
|
+
* never imports it statically — the specifier is built at runtime so the
|
|
12
|
+
* TypeScript build stays free of it too. A host that never names a connection
|
|
13
|
+
* neither installs it nor pays for it.
|
|
14
|
+
*
|
|
15
|
+
* What varies between callers is passed in rather than copied: which commands
|
|
16
|
+
* they will issue, and what they are doing with them. Both end up in the
|
|
17
|
+
* message, because "the connection is missing lpop" is only useful next to
|
|
18
|
+
* "which the queue's atomic pop issues".
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The slice of quasar's manager this needs: a connection, by name. */
|
|
22
|
+
interface ConnectionSource {
|
|
23
|
+
connection(name?: string): unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a member without assuming how the object answers.
|
|
28
|
+
*
|
|
29
|
+
* Two shapes have to work, and they disagree about `in`. Quasar's accessor is
|
|
30
|
+
* a Proxy over an empty null-prototype object with only a `get` trap, so
|
|
31
|
+
* `"connection" in manager` is FALSE while reading it returns the function —
|
|
32
|
+
* gating on `in` rejected the real manager. And a module namespace under a
|
|
33
|
+
* test double RAISES for an export it does not have, rather than answering
|
|
34
|
+
* undefined, so a bare read fails on the mock. Reading through a catch is what
|
|
35
|
+
* satisfies both.
|
|
36
|
+
*/
|
|
37
|
+
function readMember(value: unknown, name: string): unknown {
|
|
38
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
return Reflect.get(value, name);
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isConnectionSource(value: unknown): value is ConnectionSource {
|
|
47
|
+
return typeof readMember(value, "connection") === "function";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface QuasarConnectionRequest {
|
|
51
|
+
/** The package asking. It prefixes every message this can raise. */
|
|
52
|
+
pkg: string;
|
|
53
|
+
/**
|
|
54
|
+
* How this package builds an error.
|
|
55
|
+
*
|
|
56
|
+
* Not optional, and not `new Error` by default: a package with its own
|
|
57
|
+
* error type carries codes (`E_<PKG>_<REASON>`) that a caller catches on,
|
|
58
|
+
* and a shared helper throwing a bare Error would silently drop them. The
|
|
59
|
+
* two reasons are distinguished so the codes can be too.
|
|
60
|
+
*/
|
|
61
|
+
raise: (
|
|
62
|
+
reason: "quasar-missing" | "incomplete-connection",
|
|
63
|
+
message: string,
|
|
64
|
+
cause?: unknown,
|
|
65
|
+
) => Error;
|
|
66
|
+
/** Connection name, or `undefined` for quasar's default. */
|
|
67
|
+
name?: string;
|
|
68
|
+
/** The commands the caller will actually issue. */
|
|
69
|
+
required: readonly string[];
|
|
70
|
+
/** What the caller does with them, for the message that names a gap. */
|
|
71
|
+
what: string;
|
|
72
|
+
/**
|
|
73
|
+
* A second way out, when this package has one.
|
|
74
|
+
*
|
|
75
|
+
* Several of them accept a client object directly, so "pass a client
|
|
76
|
+
* instead of a connection name" is actionable — and it was in each
|
|
77
|
+
* package's own message before this was shared. Dropping it to unify the
|
|
78
|
+
* wording would have made the error strictly less useful.
|
|
79
|
+
*/
|
|
80
|
+
alternative?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The named connection, checked for the commands the caller needs BEFORE it is
|
|
85
|
+
* handed over — a connection missing one would otherwise fail on the first
|
|
86
|
+
* command, far from the line that asked for it.
|
|
87
|
+
*/
|
|
88
|
+
export async function quasarConnection<T>(
|
|
89
|
+
request: QuasarConnectionRequest,
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
const { pkg, name, required, what, raise, alternative } = request;
|
|
92
|
+
const specifier = "@c9up/quasar/services/main";
|
|
93
|
+
let loaded: unknown;
|
|
94
|
+
try {
|
|
95
|
+
loaded = await import(/* @vite-ignore */ specifier);
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
throw raise(
|
|
98
|
+
"quasar-missing",
|
|
99
|
+
`[${pkg}] ${what} asks for the quasar connection "${name ?? "default"}", but @c9up/quasar is not installed.\n` +
|
|
100
|
+
` pnpm add @c9up/quasar${alternative ? `, ${alternative}` : ""}`,
|
|
101
|
+
cause,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const manager = isConnectionSource(loaded)
|
|
106
|
+
? loaded
|
|
107
|
+
: Reflect.get(Object(loaded), "default");
|
|
108
|
+
if (!isConnectionSource(manager)) {
|
|
109
|
+
throw raise(
|
|
110
|
+
"quasar-missing",
|
|
111
|
+
`[${pkg}] @c9up/quasar/services/main did not expose a connection() manager`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const connection = manager.connection(name);
|
|
116
|
+
const missing = required.filter(
|
|
117
|
+
(command) => typeof readMember(connection, command) !== "function",
|
|
118
|
+
);
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
throw raise(
|
|
121
|
+
"incomplete-connection",
|
|
122
|
+
`[${pkg}] the quasar connection${name ? ` '${name}'` : ""} is missing ${missing.join(", ")}, which ${what} issues`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
// Load-bearing: every member of `required` has just been checked to be a
|
|
126
|
+
// function on this object. The caller names the type it needs because only
|
|
127
|
+
// the caller knows which commands it asked for.
|
|
128
|
+
return connection as T;
|
|
129
|
+
}
|