@voltro/kv 0.85.0 → 0.87.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +5987 -0
  2. package/README.md +110 -0
  3. package/THIRD-PARTY-NOTICES.md +865 -2
  4. package/dist/Kv.js +19 -0
  5. package/dist/KvStore.js +5 -0
  6. package/dist/analytics.d.ts +337 -0
  7. package/dist/analytics.js +16 -0
  8. package/dist/backendMemory.js +36 -0
  9. package/dist/backendRedis.js +48 -0
  10. package/dist/cacheAtomicProtocol.js +247 -0
  11. package/dist/cacheAtomicScript.js +309 -0
  12. package/dist/cacheAtomicState.js +141 -0
  13. package/dist/connection.d.ts +990 -9
  14. package/dist/connection.js +361 -2
  15. package/dist/connectionAnalyticsConfig.js +153 -0
  16. package/dist/connectionConfigProvider.js +111 -0
  17. package/dist/connectionDefinitions.js +285 -0
  18. package/dist/connectionMemory.js +207 -0
  19. package/dist/connectionMetrics.js +79 -0
  20. package/dist/connectionNamespace.js +19 -0
  21. package/dist/connectionNats.js +148 -0
  22. package/dist/connectionNatsConfig.js +87 -0
  23. package/dist/connectionNatsSubjects.js +16 -0
  24. package/dist/connectionOwner.js +44 -0
  25. package/dist/connectionPool.js +738 -0
  26. package/dist/connectionPrefix.js +37 -0
  27. package/dist/connectionScripts.js +4 -0
  28. package/dist/connectionSqlConfig.js +119 -0
  29. package/dist/connectionStorageConfig.js +190 -0
  30. package/dist/connectionStorageFacade.js +78 -0
  31. package/dist/connectionSubscriber.js +130 -0
  32. package/dist/errors.js +13 -0
  33. package/dist/expirySweep.js +26 -0
  34. package/dist/hot-contract.d.ts +297 -0
  35. package/dist/hot-contract.js +18 -0
  36. package/dist/hot-testing.d.ts +229 -0
  37. package/dist/hot-testing.js +1 -0
  38. package/dist/hot.d.ts +936 -0
  39. package/dist/hot.js +7 -0
  40. package/dist/hotMemory.js +46 -0
  41. package/dist/hotOperations.js +223 -0
  42. package/dist/hotRateLimit.js +103 -0
  43. package/dist/hotReadAvailability.js +19 -0
  44. package/dist/hotRedis.js +139 -0
  45. package/dist/hotRedisScript.js +4 -0
  46. package/dist/hotRouting.js +78 -0
  47. package/dist/hotState.js +182 -0
  48. package/dist/hotStoreConformance.js +367 -0
  49. package/dist/index.d.ts +113 -9
  50. package/dist/index.js +7 -115
  51. package/dist/nativeRedisReads.js +78 -0
  52. package/dist/nativeRedisTopology.js +221 -0
  53. package/dist/redisDiagnostics.js +27 -0
  54. package/dist/redisReadPolicy.js +39 -0
  55. package/dist/sql.d.ts +77 -0
  56. package/dist/sql.js +0 -0
  57. package/dist/storage.d.ts +166 -0
  58. package/dist/storage.js +48 -0
  59. package/dist/storageAddress.js +20 -0
  60. package/package.json +43 -3
  61. package/dist/connection-Dsn5fgcH.js +0 -84
@@ -0,0 +1,247 @@
1
+ //#region src/cacheAtomicProtocol.ts
2
+ var e = class extends Error {
3
+ constructor() {
4
+ super("Invalid atomic cache command or state."), this.name = "CacheAtomicProtocolError";
5
+ }
6
+ }, t = 9e5, n = () => {
7
+ throw new e();
8
+ }, r = (e, t) => {
9
+ if (typeof e != "object" || !e || Array.isArray(e) || Object.getPrototypeOf(e) !== Object.prototype && Object.getPrototypeOf(e) !== null) return n();
10
+ let r = e;
11
+ return Object.keys(r).length !== t.length || t.some((e) => !Object.hasOwn(r, e)) ? n() : r;
12
+ }, i = (e) => typeof e == "string" ? e : n(), a = (e) => {
13
+ let t = i(e);
14
+ return /^(?:[a-f0-9]{2})+$/.test(t) ? t : n();
15
+ }, o = (e) => {
16
+ let t = i(e);
17
+ return /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(t) ? t : n();
18
+ }, s = (e, t, r) => {
19
+ if (!Array.isArray(e)) return n();
20
+ let i = e.map(t);
21
+ return i.some((e, t) => t > 0 && r(i[t - 1]) >= r(e)) ? n() : i;
22
+ }, c = (e) => s(e, a, (e) => e), l = (e) => Array.isArray(e) ? e : typeof e == "object" && e && Object.keys(e).length === 0 && (Object.getPrototypeOf(e) === Object.prototype || Object.getPrototypeOf(e) === null) ? [] : n(), u = (e) => {
23
+ if (e === null) return null;
24
+ let t = r(e, ["version", "tags"]);
25
+ return {
26
+ version: o(t.version),
27
+ tags: c(l(t.tags))
28
+ };
29
+ }, d = (e) => {
30
+ let t = i(e);
31
+ return t[36] === ":" ? {
32
+ version: o(t.slice(0, 36)),
33
+ key: a(t.slice(37))
34
+ } : n();
35
+ }, f = (e) => typeof e == "string" ? Array.from(new TextEncoder().encode(JSON.stringify(e)), (e) => e.toString(16).padStart(2, "0")).join("") : n(), p = (e) => `{voltro:cache:atomic:${f(e)}}:r`, m = (e) => {
36
+ if (typeof e != "object" || !e || Array.isArray(e)) return n();
37
+ let t = e.op;
38
+ switch (t) {
39
+ case "revision": return r(e, ["op"]), { op: t };
40
+ case "fence": return r(e, ["op"]), { op: t };
41
+ case "inspect": return {
42
+ op: t,
43
+ key: a(r(e, ["op", "key"]).key)
44
+ };
45
+ case "inspect-values": return {
46
+ op: t,
47
+ keys: c(r(e, ["op", "keys"]).keys)
48
+ };
49
+ case "read": {
50
+ let n = r(e, [
51
+ "op",
52
+ "key",
53
+ "snapshot"
54
+ ]), i = u(n.snapshot);
55
+ return {
56
+ op: t,
57
+ key: a(n.key),
58
+ snapshot: i
59
+ };
60
+ }
61
+ case "tag": return {
62
+ op: t,
63
+ tag: a(r(e, ["op", "tag"]).tag)
64
+ };
65
+ case "scan": {
66
+ let a = i(r(e, ["op", "cursor"]).cursor);
67
+ return !/^(0|[1-9][0-9]{0,19})$/.test(a) || BigInt(a) > 18446744073709551615n ? n() : {
68
+ op: t,
69
+ cursor: a
70
+ };
71
+ }
72
+ case "write": {
73
+ let s = r(e, [
74
+ "op",
75
+ "key",
76
+ "snapshot",
77
+ "version",
78
+ "revision",
79
+ "tags",
80
+ "value",
81
+ "ttlMs",
82
+ "fence"
83
+ ]), l = u(s.snapshot), d = o(s.version);
84
+ if (d === l?.version) return n();
85
+ let f = s.ttlMs;
86
+ if (f !== null && (typeof f != "number" || !Number.isSafeInteger(f) || f < 0)) return n();
87
+ let p = s.fence;
88
+ return p !== null && (typeof p != "number" || !Number.isSafeInteger(p) || p < 0) ? n() : {
89
+ op: t,
90
+ key: a(s.key),
91
+ snapshot: l,
92
+ version: d,
93
+ revision: o(s.revision),
94
+ tags: c(s.tags),
95
+ value: i(s.value),
96
+ ttlMs: f,
97
+ fence: p
98
+ };
99
+ }
100
+ case "remove": {
101
+ let n = r(e, [
102
+ "op",
103
+ "key",
104
+ "snapshot",
105
+ "revision"
106
+ ]);
107
+ return {
108
+ op: t,
109
+ key: a(n.key),
110
+ snapshot: u(n.snapshot),
111
+ revision: o(n.revision)
112
+ };
113
+ }
114
+ case "invalidate": {
115
+ let c = r(e, [
116
+ "op",
117
+ "tag",
118
+ "members",
119
+ "snapshots",
120
+ "revision"
121
+ ]), l = s(c.members, (e) => (d(e), i(e)), (e) => e), f = s(c.snapshots, (e) => {
122
+ let t = r(e, ["key", "snapshot"]);
123
+ return {
124
+ key: a(t.key),
125
+ snapshot: u(t.snapshot)
126
+ };
127
+ }, (e) => e.key), p = [...new Set(l.map((e) => d(e).key))].sort();
128
+ return p.length !== f.length || p.some((e, t) => f[t].key !== e) ? n() : {
129
+ op: t,
130
+ tag: a(c.tag),
131
+ members: l,
132
+ snapshots: f,
133
+ revision: o(c.revision)
134
+ };
135
+ }
136
+ case "clear": {
137
+ let n = r(e, [
138
+ "op",
139
+ "keys",
140
+ "revision"
141
+ ]);
142
+ return {
143
+ op: t,
144
+ keys: s(n.keys, i, (e) => e),
145
+ revision: n.revision === null ? null : o(n.revision)
146
+ };
147
+ }
148
+ default: return n();
149
+ }
150
+ }, h = (e, t) => {
151
+ if (typeof e != "string" || !/\{voltro:cache:atomic:(?:[a-f0-9]{2})+\}:r$/.test(e)) return n();
152
+ let r = e.slice(0, -2), i = /* @__PURE__ */ new Set(), o = (e) => {
153
+ i.add(`${r}:v:${a(e)}`);
154
+ }, s = (e) => {
155
+ i.add(`${r}:t:${a(e)}`);
156
+ }, c = () => {
157
+ i.add(`${r}:s`);
158
+ }, l = (e) => {
159
+ i.add(`${r}:e:${a(e)}`);
160
+ };
161
+ switch (t.op) {
162
+ case "inspect":
163
+ o(t.key);
164
+ break;
165
+ case "inspect-values":
166
+ t.keys.forEach(o);
167
+ break;
168
+ case "read":
169
+ o(t.key), t.snapshot?.tags.forEach(s);
170
+ break;
171
+ case "fence":
172
+ c();
173
+ break;
174
+ case "write":
175
+ o(t.key), t.tags.forEach(s), t.snapshot?.tags.forEach(s), t.fence !== null && t.tags.forEach(l);
176
+ break;
177
+ case "remove":
178
+ o(t.key), t.snapshot?.tags.forEach(s);
179
+ break;
180
+ case "tag":
181
+ s(t.tag);
182
+ break;
183
+ case "invalidate":
184
+ s(t.tag), c(), l(t.tag), t.snapshots.forEach((e) => {
185
+ o(e.key), e.snapshot?.tags.forEach(s);
186
+ });
187
+ break;
188
+ case "clear": for (let o of t.keys) if (o !== e) {
189
+ if (!o.startsWith(`${r}:v:`) && !o.startsWith(`${r}:t:`)) return n();
190
+ a(o.slice(r.length + 3)), i.add(o);
191
+ }
192
+ }
193
+ return [e, ...[...i].sort()];
194
+ }, g = (e, t) => `${o(t)}:${a(e)}`, _ = (e) => e === void 0 ? null : !Number.isFinite(e) || !Number.isSafeInteger(Math.ceil(e)) ? n() : Math.max(0, Math.ceil(e)), v = (e) => {
195
+ if (typeof e != "object" || !e || Array.isArray(e) || Object.getPrototypeOf(e) !== Object.prototype && Object.getPrototypeOf(e) !== null) return n();
196
+ let t = e;
197
+ if (Object.keys(t).some((e) => e !== "maxTagMembers" && e !== "maxReplyBytes")) return n();
198
+ for (let [e, r] of Object.entries(t)) if (typeof r != "number" || !Number.isSafeInteger(r) || r < (e === "maxReplyBytes" ? 128 : 1)) return n();
199
+ return {
200
+ ...Object.hasOwn(t, "maxTagMembers") ? { maxTagMembers: t.maxTagMembers } : {},
201
+ ...Object.hasOwn(t, "maxReplyBytes") ? { maxReplyBytes: t.maxReplyBytes } : {}
202
+ };
203
+ }, y = (e) => {
204
+ if (typeof e != "string") return n();
205
+ let t;
206
+ try {
207
+ t = JSON.parse(e);
208
+ } catch {
209
+ return n();
210
+ }
211
+ let i = r(t, ["status", "value"]), a = i.status;
212
+ return a !== "ok" && a !== "conflict" && a !== "limit" || a !== "ok" && i.value !== null ? n() : {
213
+ status: a,
214
+ value: i.value
215
+ };
216
+ }, b = (e) => s(l(e), (e) => {
217
+ let t = r(e, ["key", "snapshot"]);
218
+ return {
219
+ key: a(t.key),
220
+ snapshot: u(t.snapshot)
221
+ };
222
+ }, (e) => e.key), x = (e, t) => {
223
+ let n = r(t, ["cursor", "keys"]), a = i(n.cursor);
224
+ m({
225
+ op: "scan",
226
+ cursor: a
227
+ });
228
+ let o = e.slice(0, -2), s = l(n.keys).map((e) => `${o}${i(e)}`).filter((e) => e !== `${o}:s` && !e.startsWith(`${o}:e:`));
229
+ return h(e, {
230
+ op: "clear",
231
+ keys: [...new Set(s)].sort(),
232
+ revision: null
233
+ }), {
234
+ cursor: a,
235
+ keys: s
236
+ };
237
+ }, S = (e) => [
238
+ "revision",
239
+ "fence",
240
+ "inspect",
241
+ "inspect-values",
242
+ "read",
243
+ "tag",
244
+ "scan"
245
+ ].includes(e.op);
246
+ //#endregion
247
+ export { t as CACHE_FENCE_RETENTION_MS, e as CacheAtomicProtocolError, l as cacheAtomicArray, f as cacheAtomicComponent, h as cacheAtomicKeys, d as cacheAtomicMember, g as cacheAtomicMembership, p as cacheAtomicRoot, x as cacheAtomicScanPage, u as cacheAtomicSnapshot, _ as cacheAtomicTtl, b as cacheAtomicValueSnapshots, m as decodeCacheAtomicCommand, v as decodeCacheAtomicPolicy, y as decodeCacheAtomicReply, S as isCacheAtomicRead };
@@ -0,0 +1,309 @@
1
+ import { CACHE_FENCE_RETENTION_MS as e } from "./cacheAtomicProtocol.js";
2
+ //#region src/cacheAtomicScript.ts
3
+ var t = String.raw`
4
+ local c = cjson.decode(ARGV[1])
5
+ local policy = ARGV[2] and cjson.decode(ARGV[2]) or {}
6
+ local function invalid() error('Invalid atomic cache command or state.') end
7
+ local function null(x) return x == nil or x == cjson.null end
8
+ local function reply(status, value)
9
+ local encoded = cjson.encode({ status = status, value = value == nil and cjson.null or value })
10
+ if policy.maxReplyBytes and string.len(encoded) > policy.maxReplyBytes then
11
+ return cjson.encode({ status = 'limit', value = cjson.null })
12
+ end
13
+ return encoded
14
+ end
15
+ local function id(s)
16
+ if type(s) ~= 'string' or #s == 0 or #s % 2 ~= 0 or not string.match(s, '^[a-f0-9]+$') then invalid() end
17
+ return s
18
+ end
19
+ local function version(s)
20
+ if type(s) ~= 'string' or #s ~= 36 or string.sub(s, 9, 9) ~= '-' or string.sub(s, 14, 14) ~= '-'
21
+ or string.sub(s, 19, 19) ~= '-' or string.sub(s, 24, 24) ~= '-' then invalid() end
22
+ local hex = string.gsub(s, '-', '')
23
+ if #hex ~= 32 or not string.match(hex, '^[a-f0-9]+$') then invalid() end
24
+ return s
25
+ end
26
+ local function array(a, check)
27
+ if type(a) ~= 'table' then invalid() end
28
+ for k, v in pairs(a) do
29
+ if type(k) ~= 'number' or k % 1 ~= 0 or k < 1 or k > #a then invalid() end
30
+ check(v)
31
+ end
32
+ return a
33
+ end
34
+ local function ids(a)
35
+ array(a, id)
36
+ for i = 2, #a do if a[i - 1] >= a[i] then invalid() end end
37
+ return a
38
+ end
39
+ local function snapshot(s)
40
+ if null(s) then return nil end
41
+ if type(s) ~= 'table' then invalid() end
42
+ version(s.version); ids(s.tags)
43
+ return s
44
+ end
45
+ local function member(s)
46
+ if type(s) ~= 'string' or string.sub(s, 37, 37) ~= ':' then invalid() end
47
+ return version(string.sub(s, 1, 36)), id(string.sub(s, 38))
48
+ end
49
+ local function equalTags(a, b)
50
+ if #a ~= #b then return false end
51
+ for i = 1, #a do if a[i] ~= b[i] then return false end end
52
+ return true
53
+ end
54
+ local function same(a, b)
55
+ if null(a) or null(b) then return null(a) and null(b) end
56
+ return a.version == b.version and equalTags(a.tags, b.tags)
57
+ end
58
+ local function contains(a, wanted)
59
+ for _, v in ipairs(a) do if v == wanted then return true end end
60
+ return false
61
+ end
62
+ if #KEYS < 1 or type(c) ~= 'table' or type(c.op) ~= 'string' then invalid() end
63
+ local function fields(value, names)
64
+ if type(value) ~= 'table' then invalid() end
65
+ local allowed = {}
66
+ for _, name in ipairs(names) do allowed[name] = true; if value[name] == nil then invalid() end end
67
+ for name, _ in pairs(value) do if not allowed[name] then invalid() end end
68
+ end
69
+ local layouts = {
70
+ revision = { 'op' }, fence = { 'op' }, inspect = { 'op', 'key' }, ['inspect-values'] = { 'op', 'keys' },
71
+ read = { 'op', 'key', 'snapshot' }, tag = { 'op', 'tag' }, scan = { 'op', 'cursor' },
72
+ write = { 'op', 'key', 'snapshot', 'version', 'revision', 'tags', 'value', 'ttlMs', 'fence' },
73
+ remove = { 'op', 'key', 'snapshot', 'revision' },
74
+ invalidate = { 'op', 'tag', 'members', 'snapshots', 'revision' },
75
+ clear = { 'op', 'keys', 'revision' },
76
+ }
77
+ if not layouts[c.op] then invalid() end
78
+ fields(c, layouts[c.op])
79
+ if type(policy) ~= 'table' then invalid() end
80
+ for name, value in pairs(policy) do
81
+ if name ~= 'maxReplyBytes' and name ~= 'maxTagMembers' then invalid() end
82
+ local minimum = name == 'maxReplyBytes' and 128 or 1
83
+ if type(value) ~= 'number' or value < minimum or value % 1 ~= 0 or value > 9007199254740991 then invalid() end
84
+ end
85
+ local root = KEYS[1]
86
+ if string.sub(root, -2) ~= ':r' then invalid() end
87
+ local space = string.sub(root, 1, #root - 2)
88
+ local declared = {}
89
+ for _, key in ipairs(KEYS) do if declared[key] then invalid() end; declared[key] = key end
90
+ local needed = { [root] = true }
91
+ local function needValue(k) needed[space .. ':v:' .. id(k)] = true end
92
+ local function needTag(t) needed[space .. ':t:' .. id(t)] = true end
93
+ local function needEpoch(t) needed[space .. ':e:' .. id(t)] = true end
94
+ local function counter(n)
95
+ if type(n) ~= 'number' or n < 0 or n % 1 ~= 0 or n > 9007199254740991 then invalid() end
96
+ return n
97
+ end
98
+ local function needSnapshot(s)
99
+ if not null(s) then fields(s, { 'version', 'tags' }) end
100
+ local value = snapshot(s)
101
+ if value then for _, tag in ipairs(value.tags) do needTag(tag) end end
102
+ end
103
+ if c.op == 'fence' then needed[space .. ':s'] = true
104
+ elseif c.op == 'inspect' then needValue(c.key)
105
+ elseif c.op == 'inspect-values' then ids(c.keys); for _, key in ipairs(c.keys) do needValue(key) end
106
+ elseif c.op == 'read' then needValue(c.key); needSnapshot(c.snapshot)
107
+ elseif c.op == 'write' then
108
+ needValue(c.key); needSnapshot(c.snapshot); ids(c.tags)
109
+ for _, tag in ipairs(c.tags) do needTag(tag) end
110
+ version(c.version); version(c.revision)
111
+ if not null(c.fence) then counter(c.fence); for _, tag in ipairs(c.tags) do needEpoch(tag) end end
112
+ if not null(c.snapshot) and c.snapshot.version == c.version then invalid() end
113
+ if type(c.value) ~= 'string' or not null(c.ttlMs) and (type(c.ttlMs) ~= 'number' or c.ttlMs < 0 or c.ttlMs % 1 ~= 0 or c.ttlMs > 9007199254740991) then invalid() end
114
+ elseif c.op == 'remove' then needValue(c.key); needSnapshot(c.snapshot); version(c.revision)
115
+ elseif c.op == 'tag' then needTag(c.tag)
116
+ elseif c.op == 'invalidate' then
117
+ needTag(c.tag); version(c.revision); needed[space .. ':s'] = true; needEpoch(c.tag)
118
+ local memberKeys = {}
119
+ array(c.members, function(m) local _, key = member(m); memberKeys[key] = true end)
120
+ for i = 2, #c.members do if c.members[i - 1] >= c.members[i] then invalid() end end
121
+ array(c.snapshots, function(s)
122
+ fields(s, { 'key', 'snapshot' }); needValue(s.key); needSnapshot(s.snapshot)
123
+ if not memberKeys[s.key] then invalid() end; memberKeys[s.key] = nil
124
+ end)
125
+ for _ in pairs(memberKeys) do invalid() end
126
+ for i = 2, #c.snapshots do if c.snapshots[i - 1].key >= c.snapshots[i].key then invalid() end end
127
+ elseif c.op == 'clear' then
128
+ if not null(c.revision) then version(c.revision) end
129
+ array(c.keys, function(key)
130
+ if type(key) ~= 'string' then invalid() end
131
+ if key ~= root then
132
+ local prefix = string.sub(key, 1, #space + 3)
133
+ if prefix ~= space .. ':v:' and prefix ~= space .. ':t:' then invalid() end
134
+ id(string.sub(key, #space + 4)); needed[key] = true
135
+ end
136
+ end)
137
+ for i = 2, #c.keys do if c.keys[i - 1] >= c.keys[i] then invalid() end end
138
+ elseif c.op == 'scan' then
139
+ if type(c.cursor) ~= 'string' or not string.match(c.cursor, '^%d+$') or #c.cursor > 20
140
+ or #c.cursor > 1 and string.sub(c.cursor, 1, 1) == '0'
141
+ or #c.cursor == 20 and c.cursor > '18446744073709551615' then invalid() end
142
+ elseif c.op ~= 'revision' then invalid() end
143
+ for key, _ in pairs(needed) do if not declared[key] then invalid() end end
144
+ for key, _ in pairs(declared) do if not needed[key] then invalid() end end
145
+ local function valueKey(k) local key = declared[space .. ':v:' .. k]; if not key then invalid() end; return key end
146
+ local function tagKey(t) local key = declared[space .. ':t:' .. t]; if not key then invalid() end; return key end
147
+ local function epochKey(t) local key = declared[space .. ':e:' .. t]; if not key then invalid() end; return key end
148
+ local function sequence()
149
+ local key = declared[space .. ':s']; if not key then invalid() end
150
+ local raw = redis.call('GET', key)
151
+ if not raw then return 0, key end
152
+ return counter(tonumber(raw)), key
153
+ end
154
+ local RETENTION = ${e}
155
+ local function keyType(k)
156
+ local t = redis.call('TYPE', k)
157
+ return type(t) == 'table' and t.ok or t
158
+ end
159
+ for _, key in ipairs(KEYS) do
160
+ if c.op ~= 'clear' then
161
+ local t = keyType(key)
162
+ local kind = string.sub(key, #space + 1, #space + 3)
163
+ local wanted = key == root or key == space .. ':s' or kind == ':v:' or kind == ':e:'
164
+ if t ~= 'none' and (wanted and t ~= 'string' or not wanted and t ~= 'zset') then invalid() end
165
+ end
166
+ end
167
+ local function entry(k)
168
+ local raw = redis.call('GET', valueKey(k))
169
+ if not raw then return nil end
170
+ local e = cjson.decode(raw)
171
+ fields(e, { 'version', 'tags', 'value' })
172
+ snapshot(e)
173
+ if type(e.value) ~= 'string' then invalid() end
174
+ return e
175
+ end
176
+ local function meta(e) return e and { version = e.version, tags = e.tags } or cjson.null end
177
+ local tm = redis.call('TIME')
178
+ local now = tonumber(tm[1]) * 1000 + math.floor(tonumber(tm[2]) / 1000)
179
+ local NOEXP = 9000000000000000
180
+ if c.op == 'write' and not null(c.ttlMs) and c.ttlMs > NOEXP - now - 1 then invalid() end
181
+ local function touch(tag)
182
+ local key = tagKey(tag)
183
+ redis.call('ZREMRANGEBYSCORE', key, '-inf', string.format('%.0f', now))
184
+ local top = redis.call('ZREVRANGE', key, 0, 0, 'WITHSCORES')
185
+ if #top == 0 then redis.call('DEL', key)
186
+ elseif tonumber(top[2]) >= NOEXP then redis.call('PERSIST', key)
187
+ else redis.call('PEXPIRE', key, math.max(1, math.ceil(tonumber(top[2]) - now))) end
188
+ end
189
+ local function detach(k, e)
190
+ local m = e.version .. ':' .. k
191
+ for _, tag in ipairs(e.tags) do redis.call('ZREM', tagKey(tag), m); touch(tag) end
192
+ end
193
+ if c.op == 'revision' then
194
+ return reply('ok', redis.call('GET', root) or cjson.null)
195
+ elseif c.op == 'fence' then
196
+ local n = sequence()
197
+ return reply('ok', n)
198
+ elseif c.op == 'inspect' then return reply('ok', meta(entry(c.key)))
199
+ elseif c.op == 'inspect-values' then
200
+ local out = {}
201
+ for _, key in ipairs(c.keys) do table.insert(out, { key = key, snapshot = meta(entry(key)) }) end
202
+ return reply('ok', out)
203
+ elseif c.op == 'tag' then return reply('ok', redis.call('ZRANGE', tagKey(c.tag), 0, -1))
204
+ elseif c.op == 'scan' then
205
+ local escaped = {}
206
+ for i = 1, #space do
207
+ local char = string.sub(space, i, i)
208
+ if char == '\\' or char == '*' or char == '?' or char == '[' or char == ']' then table.insert(escaped, '\\') end
209
+ table.insert(escaped, char)
210
+ end
211
+ local page = redis.call('SCAN', c.cursor, 'MATCH', table.concat(escaped) .. ':*', 'COUNT', 200)
212
+ local relative = {}
213
+ for _, key in ipairs(page[2]) do table.insert(relative, string.sub(key, #space + 1)) end
214
+ return reply('ok', { cursor = page[1], keys = relative })
215
+ elseif c.op == 'read' then
216
+ local e = entry(c.key)
217
+ if not e then return reply('ok') end
218
+ if not same(e, c.snapshot) then return reply('conflict') end
219
+ local m = e.version .. ':' .. c.key
220
+ for _, tag in ipairs(e.tags) do
221
+ local score = redis.call('ZSCORE', tagKey(tag), m)
222
+ if not score or tonumber(score) <= now then return reply('ok') end
223
+ end
224
+ return reply('ok', e.value)
225
+ elseif c.op == 'write' or c.op == 'remove' then
226
+ -- A compute fenced before an invalidation of one of its tags is void: its
227
+ -- value may predate the write that invalidated it. Nothing changes.
228
+ if c.op == 'write' and not null(c.fence) then
229
+ for _, tag in ipairs(c.tags) do
230
+ local raw = redis.call('GET', epochKey(tag))
231
+ if raw and counter(tonumber(raw)) > c.fence then return reply('ok', 0) end
232
+ end
233
+ end
234
+ local old = entry(c.key)
235
+ if not same(old, c.snapshot) then return reply('conflict') end
236
+ if c.op == 'remove' or c.ttlMs == 0 then
237
+ if not old then return reply('ok', 0) end
238
+ redis.call('SET', root, c.revision)
239
+ local removed = redis.call('DEL', valueKey(c.key))
240
+ detach(c.key, old)
241
+ return reply('ok', removed)
242
+ end
243
+ if policy.maxTagMembers then
244
+ local oldMember = old and old.version .. ':' .. c.key or nil
245
+ local nextMember = c.version .. ':' .. c.key
246
+ for _, tag in ipairs(c.tags) do
247
+ local key = tagKey(tag)
248
+ local count = redis.call('ZCOUNT', key, '(' .. string.format('%.0f', now), '+inf')
249
+ local prior = oldMember and redis.call('ZSCORE', key, oldMember) or nil
250
+ local next = redis.call('ZSCORE', key, nextMember)
251
+ if prior and tonumber(prior) > now then count = count - 1 end
252
+ if not next or tonumber(next) <= now then count = count + 1 end
253
+ if count > policy.maxTagMembers then return reply('limit') end
254
+ end
255
+ end
256
+ -- Revise first: even an interrupted/erroring writer invalidates clear snapshots.
257
+ redis.call('SET', root, c.revision)
258
+ local expires = null(c.ttlMs) and NOEXP or now + c.ttlMs
259
+ local m = c.version .. ':' .. c.key
260
+ -- Prepare independent new memberships. No old live membership is removed yet.
261
+ for _, tag in ipairs(c.tags) do redis.call('ZADD', tagKey(tag), expires, m); touch(tag) end
262
+ local raw = cjson.encode({ version = c.version, tags = c.tags, value = c.value })
263
+ if null(c.ttlMs) then redis.call('SET', valueKey(c.key), raw)
264
+ else redis.call('PSETEX', valueKey(c.key), c.ttlMs, raw) end
265
+ if old then detach(c.key, old) end
266
+ return reply('ok', 1)
267
+ elseif c.op == 'invalidate' then
268
+ local tag = tagKey(c.tag)
269
+ if redis.call('ZCARD', tag) ~= #c.members then return reply('conflict') end
270
+ for _, m in ipairs(c.members) do if not redis.call('ZSCORE', tag, m) then return reply('conflict') end end
271
+ local expected = {}
272
+ for _, s in ipairs(c.snapshots) do expected[s.key] = s.snapshot end
273
+ local selected = {}
274
+ for _, m in ipairs(c.members) do
275
+ local v, k = member(m)
276
+ if not selected[k] then
277
+ local e = entry(k)
278
+ if e and e.version == v and contains(e.tags, c.tag) then
279
+ if not same(e, expected[k]) then return reply('conflict') end
280
+ selected[k] = e
281
+ end
282
+ end
283
+ end
284
+ redis.call('SET', root, c.revision)
285
+ local n, sequenceKey = sequence()
286
+ redis.call('SET', sequenceKey, n + 1)
287
+ redis.call('SET', epochKey(c.tag), n + 1, 'PX', RETENTION)
288
+ local removed = 0
289
+ for k, e in pairs(selected) do
290
+ removed = removed + redis.call('DEL', valueKey(k))
291
+ detach(k, e)
292
+ end
293
+ redis.call('DEL', tag)
294
+ return reply('ok', removed)
295
+ elseif c.op == 'clear' then
296
+ local observed = redis.call('GET', root) or cjson.null
297
+ if observed ~= c.revision then return reply('conflict') end
298
+ -- DEL is bounded per native command; all keys were declared before execution.
299
+ for first = 1, #KEYS, 500 do
300
+ local batch = {}
301
+ for i = first, math.min(first + 499, #KEYS) do table.insert(batch, KEYS[i]) end
302
+ redis.call('DEL', unpack(batch))
303
+ end
304
+ return reply('ok')
305
+ end
306
+ invalid()
307
+ `;
308
+ //#endregion
309
+ export { t as CACHE_ATOMIC_SCRIPT };
@@ -0,0 +1,141 @@
1
+ import { CACHE_FENCE_RETENTION_MS as e, CacheAtomicProtocolError as t, cacheAtomicKeys as n, cacheAtomicMember as r, cacheAtomicMembership as i, cacheAtomicSnapshot as a, decodeCacheAtomicCommand as o, decodeCacheAtomicPolicy as s } from "./cacheAtomicProtocol.js";
2
+ //#region src/cacheAtomicState.ts
3
+ var c = 9e15, l = () => {
4
+ throw new t();
5
+ }, u = (t, u, d, f = {}) => {
6
+ let p = o(d), m = s(f);
7
+ if (!Number.isSafeInteger(t.now) || t.now < 0 || t.now >= c) return l();
8
+ let h = u[0];
9
+ if (h === void 0) return l();
10
+ let g = n(h, p), _ = new Set(u);
11
+ if (u.length !== g.length || _.size !== u.length || g.some((e) => !_.has(e))) return l();
12
+ let v = h.slice(0, -2), y = (e) => `${v}:v:${e}`, b = (e) => `${v}:t:${e}`, x = `${v}:s`, S = (e) => `${v}:e:${e}`, C = (e) => e === h || e === x || e.startsWith(`${v}:v:`) || e.startsWith(`${v}:e:`);
13
+ if (p.op !== "clear") for (let e of u) {
14
+ let n = t.kind(e);
15
+ if (n !== "none" && n !== (C(e) ? "string" : "zset")) return l();
16
+ }
17
+ let w = (e) => {
18
+ if (e === null) return 0;
19
+ let t = Number(e);
20
+ return Number.isSafeInteger(t) && t >= 0 ? t : l();
21
+ }, T = (e) => {
22
+ let n = t.get(y(e));
23
+ if (n === null) return null;
24
+ let r;
25
+ try {
26
+ r = JSON.parse(n);
27
+ } catch {
28
+ return l();
29
+ }
30
+ if (typeof r != "object" || !r || Array.isArray(r)) return l();
31
+ let i = r;
32
+ if (Object.keys(i).length !== 3 || typeof i.value != "string") return l();
33
+ let o = a({
34
+ version: i.version,
35
+ tags: i.tags
36
+ });
37
+ return o === null ? l() : {
38
+ ...o,
39
+ value: i.value
40
+ };
41
+ }, E = (e, t) => e === null || t === null ? e === t : e.version === t.version && e.tags.length === t.tags.length && e.tags.every((e, n) => e === t.tags[n]), D = (e) => e === null ? null : {
42
+ version: e.version,
43
+ tags: e.tags
44
+ }, O = (e, t = null) => {
45
+ let n = JSON.stringify({
46
+ status: e,
47
+ value: t
48
+ });
49
+ return m.maxReplyBytes !== void 0 && new TextEncoder().encode(n).length > m.maxReplyBytes ? JSON.stringify({
50
+ status: "limit",
51
+ value: null
52
+ }) : n;
53
+ }, k = (e) => {
54
+ let n = b(e);
55
+ for (let e of t.members(n)) e.score <= t.now && t.removeMember(n, e.member);
56
+ let r = t.members(n);
57
+ if (r.length === 0) {
58
+ t.remove(n);
59
+ return;
60
+ }
61
+ let i = -Infinity;
62
+ for (let e of r) i = Math.max(i, e.score);
63
+ t.expire(n, i >= c ? null : Math.max(1, Math.ceil(i - t.now)));
64
+ }, A = (e, n) => {
65
+ for (let r of n.tags) t.removeMember(b(r), i(e, n.version)), k(r);
66
+ }, j = (e) => t.set(h, e.revision);
67
+ switch (p.op) {
68
+ case "revision": return O("ok", t.get(h));
69
+ case "fence": return O("ok", w(t.get(x)));
70
+ case "inspect": return O("ok", D(T(p.key)));
71
+ case "inspect-values": return O("ok", p.keys.map((e) => ({
72
+ key: e,
73
+ snapshot: D(T(e))
74
+ })));
75
+ case "tag": return O("ok", t.members(b(p.tag)).slice().sort((e, t) => e.score - t.score || (e.member < t.member ? -1 : +(e.member > t.member))).map((e) => e.member));
76
+ case "scan": return p.cursor === "0" ? O("ok", {
77
+ cursor: "0",
78
+ keys: t.keys().filter((e) => e.startsWith(`${v}:`)).map((e) => e.slice(v.length))
79
+ }) : l();
80
+ case "read": {
81
+ let e = T(p.key);
82
+ if (e === null) return O("ok");
83
+ if (!E(e, p.snapshot)) return O("conflict");
84
+ let n = i(p.key, e.version);
85
+ return e.tags.some((e) => !t.members(b(e)).some((e) => e.member === n && e.score > t.now)) ? O("ok") : O("ok", e.value);
86
+ }
87
+ case "write":
88
+ case "remove": {
89
+ if (p.op === "write" && p.fence !== null) {
90
+ let e = p.fence;
91
+ if (p.tags.some((n) => w(t.get(S(n))) > e)) return O("ok", 0);
92
+ }
93
+ let e = T(p.key);
94
+ if (!E(e, p.snapshot)) return O("conflict");
95
+ if (p.op === "remove" || p.ttlMs === 0) {
96
+ if (e === null) return O("ok", 0);
97
+ j(p);
98
+ let n = t.remove(y(p.key));
99
+ return A(p.key, e), O("ok", n);
100
+ }
101
+ if (p.ttlMs !== null && p.ttlMs > c - t.now - 1) return l();
102
+ if (m.maxTagMembers !== void 0) for (let n of p.tags) {
103
+ let r = t.members(b(n)).filter((e) => e.score > t.now), a = e === null ? null : i(p.key, e.version), o = i(p.key, p.version);
104
+ if (r.length - +!!r.some((e) => e.member === a) + +!r.some((e) => e.member === o) > m.maxTagMembers) return O("limit");
105
+ }
106
+ j(p);
107
+ for (let e of p.tags) t.addMember(b(e), i(p.key, p.version), p.ttlMs === null ? c : t.now + p.ttlMs), k(e);
108
+ return t.set(y(p.key), JSON.stringify({
109
+ version: p.version,
110
+ tags: p.tags,
111
+ value: p.value
112
+ }), p.ttlMs ?? void 0), e !== null && A(p.key, e), O("ok", 1);
113
+ }
114
+ case "invalidate": {
115
+ let n = t.members(b(p.tag));
116
+ if (n.length !== p.members.length || p.members.some((e) => !n.some((t) => t.member === e))) return O("conflict");
117
+ let i = /* @__PURE__ */ new Map(), a = new Map(p.snapshots.map((e) => [e.key, e.snapshot]));
118
+ for (let e of p.members) {
119
+ let t = r(e);
120
+ if (i.has(t.key)) continue;
121
+ let n = T(t.key);
122
+ if (n !== null && n.version === t.version && n.tags.includes(p.tag)) {
123
+ if (!E(n, a.get(t.key) ?? null)) return O("conflict");
124
+ i.set(t.key, n);
125
+ }
126
+ }
127
+ j(p);
128
+ let o = w(t.get(x)) + 1;
129
+ t.set(x, String(o)), t.set(S(p.tag), String(o), e);
130
+ let s = 0;
131
+ for (let [e, n] of i) s += t.remove(y(e)), A(e, n);
132
+ return t.remove(b(p.tag)), O("ok", s);
133
+ }
134
+ case "clear":
135
+ if (t.get(h) !== p.revision) return O("conflict");
136
+ for (let e of u) t.remove(e);
137
+ return O("ok");
138
+ }
139
+ };
140
+ //#endregion
141
+ export { u as applyCacheAtomicCommand };