@voltro/kv 0.86.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.
- package/CHANGELOG.md +5321 -0
- package/README.md +110 -0
- package/THIRD-PARTY-NOTICES.md +864 -1
- package/dist/analytics.d.ts +337 -0
- package/dist/analytics.js +16 -0
- package/dist/cacheAtomicProtocol.js +247 -0
- package/dist/cacheAtomicScript.js +309 -0
- package/dist/cacheAtomicState.js +141 -0
- package/dist/connection.d.ts +990 -9
- package/dist/connection.js +330 -50
- package/dist/connectionAnalyticsConfig.js +153 -0
- package/dist/connectionConfigProvider.js +111 -0
- package/dist/connectionDefinitions.js +285 -0
- package/dist/connectionMemory.js +207 -0
- package/dist/connectionMetrics.js +79 -0
- package/dist/connectionNamespace.js +19 -0
- package/dist/connectionNats.js +148 -0
- package/dist/connectionNatsConfig.js +87 -0
- package/dist/connectionNatsSubjects.js +16 -0
- package/dist/connectionOwner.js +44 -0
- package/dist/connectionPool.js +738 -0
- package/dist/connectionPrefix.js +37 -0
- package/dist/connectionScripts.js +4 -0
- package/dist/connectionSqlConfig.js +119 -0
- package/dist/connectionStorageConfig.js +190 -0
- package/dist/connectionStorageFacade.js +78 -0
- package/dist/connectionSubscriber.js +130 -0
- package/dist/errors.js +5 -1
- package/dist/hot-contract.d.ts +297 -0
- package/dist/hot-contract.js +18 -0
- package/dist/hot-testing.d.ts +229 -0
- package/dist/hot-testing.js +1 -0
- package/dist/hot.d.ts +936 -0
- package/dist/hot.js +7 -0
- package/dist/hotMemory.js +46 -0
- package/dist/hotOperations.js +223 -0
- package/dist/hotRateLimit.js +103 -0
- package/dist/hotReadAvailability.js +19 -0
- package/dist/hotRedis.js +139 -0
- package/dist/hotRedisScript.js +4 -0
- package/dist/hotRouting.js +78 -0
- package/dist/hotState.js +182 -0
- package/dist/hotStoreConformance.js +367 -0
- package/dist/index.d.ts +113 -9
- package/dist/nativeRedisReads.js +78 -0
- package/dist/nativeRedisTopology.js +221 -0
- package/dist/redisDiagnostics.js +27 -0
- package/dist/redisReadPolicy.js +39 -0
- package/dist/sql.d.ts +77 -0
- package/dist/sql.js +0 -0
- package/dist/storage.d.ts +166 -0
- package/dist/storage.js +48 -0
- package/dist/storageAddress.js +20 -0
- package/package.json +43 -3
|
@@ -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 };
|