@brust/cache-redis 2.0.0-alpha.34 → 2.0.0-alpha.35
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/package.json +3 -3
- package/src/adapter.ts +4 -2
- package/src/keys.ts +7 -1
- package/src/lua.ts +19 -5
- package/src/pairing.ts +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brust/cache-redis",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.35",
|
|
4
4
|
"description": "brust cache adapter for Redis and Dragonfly (Bun RedisClient, no npm dependency)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://brust.dev",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@brust/core": "2.0.0-alpha.
|
|
32
|
+
"@brust/core": "2.0.0-alpha.35"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@brust/core": "2.0.0-alpha.
|
|
35
|
+
"@brust/core": "2.0.0-alpha.35",
|
|
36
36
|
"@types/bun": "^1.4.4",
|
|
37
37
|
"typescript": "^7.0.2"
|
|
38
38
|
},
|
package/src/adapter.ts
CHANGED
|
@@ -55,8 +55,10 @@ export function createAdapter(name: 'redis' | 'dragonfly', raw: Record<string, u
|
|
|
55
55
|
const method = meta.method.toUpperCase()
|
|
56
56
|
const tags = [...new Set(meta.tags)]
|
|
57
57
|
const stage = K.stage(crypto.randomUUID())
|
|
58
|
-
|
|
59
|
-
const
|
|
58
|
+
// F493: every page joins its prefix's set (one more SADD, one more marker GET per store).
|
|
59
|
+
const prefix = meta.prefix ?? ''
|
|
60
|
+
const idx = [...tags.map((t) => K.tag(t)), K.path(method, meta.path), K.pfx(prefix)]
|
|
61
|
+
const marks = [...tags.map((t) => K.markTag(t)), K.markPath(method, meta.path), K.markPfx(prefix)]
|
|
60
62
|
const frame = encodeFrame(page, { entryStamp: meta.stamp, storedAt: Date.now(), tags, method, path: meta.path })
|
|
61
63
|
// Both commands go out on the one auto-pipelined connection: one round trip.
|
|
62
64
|
const staged = c.set(stage, frame, 'PX', STAGE_TTL_MS)
|
package/src/keys.ts
CHANGED
|
@@ -7,8 +7,11 @@ export interface Keys {
|
|
|
7
7
|
page(l2Key: string): string
|
|
8
8
|
tag(tag: string): string
|
|
9
9
|
path(method: string | undefined, path: string): string
|
|
10
|
+
/** F493: every page of one route-cache key prefix (the prefix is JSON-quoted: `:` in a host is safe). */
|
|
11
|
+
pfx(prefix: string): string
|
|
10
12
|
markTag(tag: string): string
|
|
11
13
|
markPath(method: string | undefined, path: string): string
|
|
14
|
+
markPfx(prefix: string): string
|
|
12
15
|
seq: string
|
|
13
16
|
stage(id: string): string
|
|
14
17
|
channel: string
|
|
@@ -24,8 +27,10 @@ export function keys(ns: string): Keys {
|
|
|
24
27
|
page: (k) => `${p}:page:${k}`,
|
|
25
28
|
tag: (t) => `${p}:tag:${t}`,
|
|
26
29
|
path: (m, path) => `${p}:path:${pathName(m, path)}`,
|
|
30
|
+
pfx: (x) => `${p}:pfx:${JSON.stringify(x)}`,
|
|
27
31
|
markTag: (t) => `${p}:inval:tag:${t}`,
|
|
28
32
|
markPath: (m, path) => `${p}:inval:path:${pathName(m, path)}`,
|
|
33
|
+
markPfx: (x) => `${p}:inval:pfx:${JSON.stringify(x)}`,
|
|
29
34
|
seq: `${p}:seq`,
|
|
30
35
|
stage: (id) => `${p}:stage:${id}`,
|
|
31
36
|
channel: `brust:${ns}:inval`,
|
|
@@ -38,10 +43,11 @@ export function canonical(a: InvalidateArgs): string {
|
|
|
38
43
|
return JSON.stringify({
|
|
39
44
|
tags: [...new Set(a.tags ?? [])].sort(),
|
|
40
45
|
...(a.path !== undefined ? { path: a.path, method: (a.method ?? 'GET').toUpperCase() } : {}),
|
|
46
|
+
...(a.prefix !== undefined ? { prefix: a.prefix, ...(a.path === undefined && a.method !== undefined ? { method: a.method.toUpperCase() } : {}) } : {}),
|
|
41
47
|
})
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/** Does the invalidation touch pages? (A key-only one is tier 1's business: the store holds none by key.) */
|
|
45
51
|
export function hasStoreWork(a: InvalidateArgs): boolean {
|
|
46
|
-
return (a.tags?.length ?? 0) > 0 || a.path !== undefined
|
|
52
|
+
return (a.tags?.length ?? 0) > 0 || a.path !== undefined || a.prefix !== undefined
|
|
47
53
|
}
|
package/src/lua.ts
CHANGED
|
@@ -20,8 +20,10 @@ for i = 1, n do
|
|
|
20
20
|
end
|
|
21
21
|
return 1`
|
|
22
22
|
|
|
23
|
-
// INVAL_LUA KEYS = [seq, idx_1..idx_n, mark_1..mark_n]
|
|
24
|
-
// ARGV = [mode ('both'|'delete'|'publish'), n, marker_ttl_ms, channel, origin_json, args_json]
|
|
23
|
+
// INVAL_LUA KEYS = [seq, idx_1..idx_n, mark_1..mark_n, filter?]
|
|
24
|
+
// ARGV = [mode ('both'|'delete'|'publish'), n, marker_ttl_ms, channel, origin_json, args_json, has_filter ('1'|'')]
|
|
25
|
+
// F493: with a filter (one prefix's member set) only the members of each idx set that are also in the filter are
|
|
26
|
+
// returned and SREM'd from both (the other prefixes' members stay indexed); without one, SUNION + DEL as before.
|
|
25
27
|
// The `publish` and `both` modes PUBLISH {seq, origin, args} from INSIDE the script (N1): scripts run one at a time, so
|
|
26
28
|
// the order of events on the channel is the order of seq, however many processes invalidate at once.
|
|
27
29
|
export const INVAL_LUA = `local unpack = table.unpack or unpack -- Lua 5.1 (Redis) and 5.4 (Dragonfly)
|
|
@@ -34,9 +36,21 @@ for i = 1, n do
|
|
|
34
36
|
end
|
|
35
37
|
local out = { mode == 'delete' and v - 1 or v }
|
|
36
38
|
if mode ~= 'publish' and n > 0 then
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
if ARGV[7] == '1' then
|
|
40
|
+
local f = KEYS[2 + 2 * n]; local seen = {}
|
|
41
|
+
for i = 1, n do
|
|
42
|
+
local ms = redis.call('SINTER', KEYS[1 + i], f)
|
|
43
|
+
for j = 1, #ms, 1000 do
|
|
44
|
+
redis.call('SREM', KEYS[1 + i], unpack(ms, j, math.min(j + 999, #ms)))
|
|
45
|
+
redis.call('SREM', f, unpack(ms, j, math.min(j + 999, #ms)))
|
|
46
|
+
end
|
|
47
|
+
for _, m in ipairs(ms) do if not seen[m] then seen[m] = true; out[#out + 1] = m end end
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
local idx = {}; for i = 1, n do idx[i] = KEYS[1 + i] end
|
|
51
|
+
for _, m in ipairs(redis.call('SUNION', unpack(idx))) do out[#out + 1] = m end
|
|
52
|
+
redis.call('DEL', unpack(idx))
|
|
53
|
+
end
|
|
40
54
|
end
|
|
41
55
|
if mode ~= 'delete' then
|
|
42
56
|
redis.call('PUBLISH', ARGV[4], '{"seq":' .. string.format('%d', v) .. ',"origin":' .. ARGV[5] .. ',"args":' .. ARGV[6] .. '}')
|
package/src/pairing.ts
CHANGED
|
@@ -74,7 +74,18 @@ export class Pairing {
|
|
|
74
74
|
idx.push(K.path(method, args.path))
|
|
75
75
|
marks.push(K.markPath(method, args.path))
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// F493: `prefix` + tags/path deletes only that prefix's members (the prefix set filters), and marks the unscoped
|
|
78
|
+
// tag/path markers (conservative: a racing set of another prefix is refused, a later miss). `prefix` alone deletes the
|
|
79
|
+
// prefix's set and marks its marker. The store holds GET pages only: `{prefix, method: <not GET>}` deletes nothing.
|
|
80
|
+
const filter: string[] = []
|
|
81
|
+
if (args.prefix !== undefined) {
|
|
82
|
+
if (idx.length) filter.push(K.pfx(args.prefix))
|
|
83
|
+
else if (method === undefined || method.toUpperCase() === 'GET') {
|
|
84
|
+
idx.push(K.pfx(args.prefix))
|
|
85
|
+
marks.push(K.markPfx(args.prefix))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const r = (await evalsha(this.c(), INVAL_SCRIPT, [K.seq, ...idx, ...marks, ...filter], [mode, idx.length, this.markerTtlMs, this.channel(), JSON.stringify(origin), JSON.stringify(args), filter.length ? '1' : ''])) as (number | string)[]
|
|
78
89
|
return { seq: Number(r[0]), members: r.slice(1).map(String) }
|
|
79
90
|
}
|
|
80
91
|
}
|