@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brust/cache-redis",
3
- "version": "2.0.0-alpha.34",
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.34"
32
+ "@brust/core": "2.0.0-alpha.35"
33
33
  },
34
34
  "devDependencies": {
35
- "@brust/core": "2.0.0-alpha.34",
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
- const idx = [...tags.map((t) => K.tag(t)), K.path(method, meta.path)]
59
- const marks = [...tags.map((t) => K.markTag(t)), K.markPath(method, meta.path)]
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
- local idx = {}; for i = 1, n do idx[i] = KEYS[1 + i] end
38
- for _, m in ipairs(redis.call('SUNION', unpack(idx))) do out[#out + 1] = m end
39
- redis.call('DEL', unpack(idx))
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
- const r = (await evalsha(this.c(), INVAL_SCRIPT, [K.seq, ...idx, ...marks], [mode, idx.length, this.markerTtlMs, this.channel(), JSON.stringify(origin), JSON.stringify(args)])) as (number | string)[]
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
  }