@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,285 @@
1
+ import { sqlDefinitionProblem as e } from "./connectionSqlConfig.js";
2
+ import { analyticsDefinitionProblem as t } from "./connectionAnalyticsConfig.js";
3
+ //#region src/connectionDefinitions.ts
4
+ var n = (e) => {
5
+ if (!/^[A-Z][A-Z0-9_]*$/.test(e)) throw Error("Connection env names must use uppercase letters, digits and underscores");
6
+ return Object.freeze({
7
+ _tag: "ConnectionEnv",
8
+ name: e
9
+ });
10
+ }, r = (e, t) => {
11
+ for (let n of ["connectTimeoutMs", "commandTimeoutMs"]) {
12
+ let r = e[n];
13
+ if (r !== void 0) {
14
+ if (!Number.isSafeInteger(r) || r < 1 || r > 2147483647) return `Redis ${n} must be an integer from 1 to 2147483647 milliseconds`;
15
+ if (t !== void 0 && t !== "resp") return "Native Redis timeouts require the RESP driver";
16
+ }
17
+ }
18
+ }, i = (e = {}) => {
19
+ if (e.now !== void 0 && typeof e.now != "function") throw Error("Memory connection now must be a function");
20
+ return Object.freeze({
21
+ _tag: "MemoryRedisConnection",
22
+ ...e.now === void 0 ? {} : { now: e.now }
23
+ });
24
+ }, a = (e, t = {}) => {
25
+ let n = t.ttlMs ?? 6e4;
26
+ if (!Number.isSafeInteger(n) || n <= 0) throw Error("Dynamic connection ttlMs must be a positive safe integer");
27
+ return Object.freeze({
28
+ _tag: "DynamicRedisConnection",
29
+ resolve: e,
30
+ ttlMs: n
31
+ });
32
+ }, o = (e, t = {}) => {
33
+ let n = t.ttlMs ?? 6e4;
34
+ if (!Number.isSafeInteger(n) || n <= 0) throw Error("Dynamic connection ttlMs must be a positive safe integer");
35
+ return Object.freeze({
36
+ _tag: "DynamicNatsConnection",
37
+ resolve: e,
38
+ ttlMs: n
39
+ });
40
+ }, s = (e, t) => {
41
+ let n = e.startsWith("nats:") ? e.slice(5) : e, r = Object.keys(t.nats ?? {});
42
+ return n || t.defaults?.nats || (r.length === 1 ? r[0] : "");
43
+ }, c = (e, t) => {
44
+ let n = e.startsWith("redis:") ? e.slice(6) : e, r = Object.keys(t.redis ?? {});
45
+ return n || t.defaults?.redis || (r.length === 1 ? r[0] : "");
46
+ }, l = (n) => {
47
+ if (typeof n != "object" || !n || Array.isArray(n)) return "connections must be a named object";
48
+ if (Object.keys(n).some((e) => ![
49
+ "redis",
50
+ "nats",
51
+ "storage",
52
+ "analytics",
53
+ "sql",
54
+ "defaults",
55
+ "bindings",
56
+ "maxDynamicPools",
57
+ "rotationGraceMs"
58
+ ].includes(e))) return "Connection declarations require an implemented registry kind or option";
59
+ if (n.defaults !== void 0) {
60
+ if (typeof n.defaults != "object" || n.defaults === null || Array.isArray(n.defaults)) return "Connection defaults must be a named object";
61
+ if (Object.keys(n.defaults).some((e) => ![
62
+ "redis",
63
+ "nats",
64
+ "storage",
65
+ "analytics",
66
+ "sql"
67
+ ].includes(e))) return "Connection defaults require an implemented registry kind";
68
+ if (Object.values(n.defaults).some((e) => e !== void 0 && (typeof e != "string" || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(e)))) return "Connection defaults must name declared entries";
69
+ }
70
+ if (n.redis !== void 0 && (typeof n.redis != "object" || n.redis === null || Array.isArray(n.redis))) return "connections.redis must be a named object";
71
+ let i = /* @__PURE__ */ new Set(), a = (e) => typeof e == "object" && !!e && "_tag" in e && e._tag === "ConnectionEnv" && "name" in e && typeof e.name == "string" && /^[A-Z][A-Z0-9_]*$/.test(e.name);
72
+ for (let [e, t] of Object.entries(n.redis ?? {})) {
73
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(e)) return "Connection names must start with a letter and contain only letters, digits, underscores or dashes";
74
+ let n = e.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
75
+ if (i.has(n)) return "Connection names must not collide after environment-name normalization";
76
+ if (i.add(n), typeof t != "object" || !t || Array.isArray(t)) return "A connection definition must be a named object";
77
+ if ("_tag" in t) {
78
+ if ("connectTimeoutMs" in t || "commandTimeoutMs" in t) return "Native Redis timeouts belong to a RESP declaration or resolved dynamic configuration";
79
+ if (t._tag === "MemoryRedisConnection") {
80
+ if (t.now !== void 0 && typeof t.now != "function") return "Memory connection now must be a function";
81
+ continue;
82
+ }
83
+ if (t._tag !== "DynamicRedisConnection" || typeof t.resolve != "function" || !Number.isSafeInteger(t.ttlMs) || t.ttlMs <= 0) return "A dynamic connection needs a resolver and a positive ttlMs";
84
+ continue;
85
+ }
86
+ if (!a(t.url) || t.token !== void 0 && !a(t.token)) return "Connection endpoints and credentials must use env() declarations";
87
+ let o = u(t);
88
+ if (o) return o;
89
+ let s = r(t, t.driver);
90
+ if (s) return s;
91
+ if (t.driver !== void 0 && !["resp", "http"].includes(t.driver)) return "Redis driver must be resp or http";
92
+ if (t.cluster !== void 0 && typeof t.cluster != "boolean") return "Redis cluster must be a boolean";
93
+ if (t.readFrom !== void 0 && ![
94
+ "primary",
95
+ "replica",
96
+ "nearest"
97
+ ].includes(t.readFrom)) return "Redis readFrom must be primary, replica or nearest";
98
+ if (t.maxmemoryPolicyHint !== void 0 && typeof t.maxmemoryPolicyHint != "string") return "Redis maxmemoryPolicyHint must be a string";
99
+ if (t.persistence !== void 0 && ![
100
+ "required",
101
+ "optional",
102
+ "none"
103
+ ].includes(t.persistence)) return "Connection persistence must be required, optional or none";
104
+ if (t.replicas !== void 0 && (!Array.isArray(t.replicas) || !t.replicas.every(a))) return "Replica endpoints must use env() declarations";
105
+ }
106
+ if (n.defaults?.redis !== void 0 && !Object.hasOwn(n.redis ?? {}, n.defaults.redis)) return "The default Redis connection must name a declared entry";
107
+ if (n.nats !== void 0 && (typeof n.nats != "object" || n.nats === null || Array.isArray(n.nats))) return "connections.nats must be a named object";
108
+ i.clear();
109
+ for (let [e, t] of Object.entries(n.nats ?? {})) {
110
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(e)) return "Connection names must start with a letter and contain only letters, digits, underscores or dashes";
111
+ let n = e.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
112
+ if (i.has(n)) return "Connection names must not collide after environment-name normalization";
113
+ if (i.add(n), typeof t != "object" || !t || Array.isArray(t)) return "A connection definition must be a named object";
114
+ if ("_tag" in t) {
115
+ if (t._tag !== "DynamicNatsConnection" || typeof t.resolve != "function" || !Number.isSafeInteger(t.ttlMs) || t.ttlMs <= 0) return "A dynamic NATS connection needs a resolver and a positive ttlMs";
116
+ continue;
117
+ }
118
+ if (!a(t.url) || [
119
+ t.token,
120
+ t.user,
121
+ t.pass
122
+ ].some((e) => e !== void 0 && !a(e))) return "Connection endpoints and credentials must use env() declarations";
123
+ let r = u(t);
124
+ if (r) return r;
125
+ if (t.user === void 0 != (t.pass === void 0)) return "NATS user and pass must be declared together";
126
+ if (t.token !== void 0 && t.user !== void 0) return "NATS token and user/pass authentication are exclusive";
127
+ }
128
+ if (n.defaults?.nats !== void 0 && !Object.hasOwn(n.nats ?? {}, n.defaults.nats)) return "The default NATS connection must name a declared entry";
129
+ if (n.storage !== void 0 && (typeof n.storage != "object" || n.storage === null || Array.isArray(n.storage))) return "connections.storage must be a named object";
130
+ i.clear();
131
+ for (let [e, t] of Object.entries(n.storage ?? {})) {
132
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(e)) return "Storage connection names must be named identifiers";
133
+ let n = e.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
134
+ if (i.has(n)) return "Connection names must not collide after environment-name normalization";
135
+ if (i.add(n), typeof t != "object" || !t || Array.isArray(t)) return "A storage connection definition must be a named object";
136
+ if ("_tag" in t) {
137
+ if (t._tag !== "DynamicStorageConnection" || typeof t.resolve != "function" || !Number.isSafeInteger(t.ttlMs) || t.ttlMs <= 0) return "Dynamic storage needs a resolver and positive ttlMs";
138
+ continue;
139
+ }
140
+ if (Object.keys(t).some((e) => ![
141
+ "provider",
142
+ "bucket",
143
+ "root",
144
+ "endpoint",
145
+ "accessKeyId",
146
+ "secretAccessKey",
147
+ "accountName",
148
+ "accountKey",
149
+ "connectionString",
150
+ "cdnBaseUrl",
151
+ "forcePathStyle",
152
+ "publicAcl",
153
+ "tls",
154
+ "persistence",
155
+ "keyPrefix",
156
+ "allowSharedNamespace",
157
+ "region",
158
+ "label",
159
+ "acl"
160
+ ].includes(e))) return "Storage declarations contain an unknown field";
161
+ if (t.persistence !== void 0 && ![
162
+ "required",
163
+ "optional",
164
+ "none"
165
+ ].includes(t.persistence)) return "Storage persistence must be required, optional or none";
166
+ if (![
167
+ "s3",
168
+ "minio",
169
+ "azure",
170
+ "filesystem",
171
+ "memory",
172
+ "database"
173
+ ].includes(t.provider)) return "Storage requires an implemented provider";
174
+ let r = u(t);
175
+ if (r) return r;
176
+ for (let e of [
177
+ "endpoint",
178
+ "accessKeyId",
179
+ "secretAccessKey",
180
+ "accountKey",
181
+ "connectionString"
182
+ ]) if (t[e] !== void 0 && !a(t[e])) return "Storage endpoints and credentials must use env() declarations";
183
+ for (let e of [
184
+ "bucket",
185
+ "root",
186
+ "accountName",
187
+ "cdnBaseUrl"
188
+ ]) if (t[e] !== void 0 && typeof t[e] != "string" && !a(t[e])) return "Storage locations must use strings or env() declarations";
189
+ for (let e of ["publicAcl", "forcePathStyle"]) if (t[e] !== void 0 && typeof t[e] != "boolean") return "Storage policies must be boolean";
190
+ if (t.accessKeyId === void 0 != (t.secretAccessKey === void 0)) return "Storage access key and secret must be declared together";
191
+ if ((t.provider === "s3" || t.provider === "minio" || t.provider === "azure") && t.bucket === void 0) return "Object storage requires a declared bucket or container";
192
+ if (t.provider === "filesystem" && t.root === void 0) return "Filesystem storage requires a declared root";
193
+ }
194
+ if (n.defaults?.storage !== void 0 && !Object.hasOwn(n.storage ?? {}, n.defaults.storage)) return "The default storage connection must name a declared entry";
195
+ if (n.analytics !== void 0 && (typeof n.analytics != "object" || n.analytics === null || Array.isArray(n.analytics))) return "connections.analytics must be a named object";
196
+ i.clear();
197
+ for (let [e, r] of Object.entries(n.analytics ?? {})) {
198
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(e)) return "Analytics connection names must be named identifiers";
199
+ let n = e.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
200
+ if (i.has(n)) return "Connection names must not collide after environment-name normalization";
201
+ i.add(n);
202
+ let a = t(r);
203
+ if (a) return a;
204
+ }
205
+ if (n.defaults?.analytics !== void 0 && !Object.hasOwn(n.analytics ?? {}, n.defaults.analytics)) return "The default analytics connection must name a declared entry";
206
+ if (n.sql !== void 0 && (typeof n.sql != "object" || n.sql === null || Array.isArray(n.sql))) return "connections.sql must be a named object";
207
+ i.clear();
208
+ for (let [t, r] of Object.entries(n.sql ?? {})) {
209
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(t)) return "SQL connection names must be named identifiers";
210
+ let n = t.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
211
+ if (i.has(n)) return "Connection names must not collide after environment-name normalization";
212
+ i.add(n);
213
+ let a = e(r);
214
+ if (a) return a;
215
+ }
216
+ if (n.defaults?.sql !== void 0 && !Object.hasOwn(n.sql ?? {}, n.defaults.sql)) return "The default SQL connection must name a declared entry";
217
+ if (n.bindings !== void 0) {
218
+ if (typeof n.bindings != "object" || n.bindings === null || Array.isArray(n.bindings)) return "Connection bindings must be a named object";
219
+ for (let [e, t] of Object.entries(n.bindings)) {
220
+ if (!e.trim() || typeof t != "object" || !t || Array.isArray(t)) return "Connection bindings must map plugin names to role references";
221
+ for (let [e, n] of Object.entries(t)) if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(e) || typeof n != "string" || !/^(redis|nats|storage|analytics|sql):[A-Za-z][A-Za-z0-9_-]*$/.test(n)) return "Connection bindings require named roles and kind:name references";
222
+ }
223
+ }
224
+ }, u = (e) => {
225
+ for (let t of [
226
+ "keyPrefix",
227
+ "region",
228
+ "label",
229
+ "acl"
230
+ ]) if (e[t] !== void 0 && typeof e[t] != "string") return `Connection ${t} must be a string`;
231
+ if (e.allowSharedNamespace !== void 0 && typeof e.allowSharedNamespace != "boolean") return "Connection allowSharedNamespace must be a boolean";
232
+ if (e.tls !== void 0 && !["auto", "required"].includes(e.tls)) return "Connection tls must be auto or required";
233
+ }, d = (e) => {
234
+ if (e.to.length === 0) throw Error("A connection route needs at least one target");
235
+ let t = Object.freeze([...e.to]);
236
+ return Object.freeze({
237
+ _tag: "ConnectionRoute",
238
+ keyAffinity: !0,
239
+ connections: t,
240
+ select: ({ key: n }) => {
241
+ let r = 2166136261;
242
+ for (let t of e.key(n)) r = Math.imul(r ^ t.codePointAt(0), 16777619) >>> 0;
243
+ return t[r % t.length];
244
+ }
245
+ });
246
+ }, f = (e) => {
247
+ if (e.length === 0) throw Error("A connection route needs at least one target");
248
+ let t = Object.freeze([...e]), n = 0;
249
+ return Object.freeze({
250
+ _tag: "ConnectionRoute",
251
+ keyAffinity: !1,
252
+ connections: t,
253
+ select: () => {
254
+ let e = t[n];
255
+ return n = (n + 1) % t.length, e;
256
+ }
257
+ });
258
+ }, p = (e) => Object.freeze({
259
+ _tag: "ConnectionRoute",
260
+ keyAffinity: !0,
261
+ connections: Object.freeze([e]),
262
+ select: ({ tenantId: t }) => {
263
+ if (!t) throw Error("A tenant connection route requires tenantId");
264
+ return e;
265
+ }
266
+ }), m = (e) => {
267
+ let t = /* @__PURE__ */ new Set();
268
+ for (let n of Object.values(e.redis ?? {})) if (!("_tag" in n)) {
269
+ t.add(n.url.name), n.token && t.add(n.token.name);
270
+ for (let e of n.replicas ?? []) t.add(e.name);
271
+ }
272
+ for (let n of Object.values(e.nats ?? {})) if (!("_tag" in n)) {
273
+ t.add(n.url.name);
274
+ for (let e of [
275
+ n.token,
276
+ n.user,
277
+ n.pass
278
+ ]) e !== void 0 && t.add(e.name);
279
+ }
280
+ for (let n of Object.values(e.storage ?? {})) if (!("_tag" in n)) for (let e of Object.values(n)) typeof e == "object" && e && "_tag" in e && e._tag === "ConnectionEnv" && t.add(e.name);
281
+ for (let n of [...Object.values(e.analytics ?? {}), ...Object.values(e.sql ?? {})]) if (!("_tag" in n)) for (let e of Object.values(n)) typeof e == "object" && e && "_tag" in e && e._tag === "ConnectionEnv" && t.add(e.name);
282
+ return [...t].sort();
283
+ };
284
+ //#endregion
285
+ export { p as byTenant, l as connectionDefinitionsProblem, u as connectionMetadataProblem, m as declaredConnectionEnv, a as dynamic, o as dynamicNats, n as env, i as memory, s as natsConnectionName, c as redisConnectionName, r as redisTimeoutsProblem, f as roundRobin, d as shardBy };
@@ -0,0 +1,207 @@
1
+ import "./connectionScripts.js";
2
+ import { decodeCacheAtomicCommand as e, isCacheAtomicRead as t } from "./cacheAtomicProtocol.js";
3
+ import { CACHE_ATOMIC_SCRIPT as n } from "./cacheAtomicScript.js";
4
+ import { isHotReadOperation as r } from "./hotOperations.js";
5
+ import { applyHotOperations as i, compareHotMembers as a } from "./hotState.js";
6
+ import { HotError as o } from "./hot-contract.js";
7
+ import "./hotRedisScript.js";
8
+ import { applyCacheAtomicCommand as s } from "./cacheAtomicState.js";
9
+ //#region src/connectionMemory.ts
10
+ var c = (c = Date.now) => {
11
+ let l = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Set(), p = !1, m = () => {
12
+ if (p) throw Error("Memory connection is closed");
13
+ }, h = (e, t = c()) => {
14
+ (l.get(e)?.expiresAt ?? Infinity) <= t && l.delete(e), (u.get(e)?.expiresAt ?? Infinity) <= t && u.delete(e), (d.get(e)?.expiresAt ?? Infinity) <= t && d.delete(e);
15
+ }, g = (e) => (h(e), l.has(e) || u.has(e) || d.has(e)), _ = (e) => {
16
+ let t = g(e);
17
+ return l.delete(e), u.delete(e), d.delete(e), +!!t;
18
+ }, v = () => [.../* @__PURE__ */ new Set([
19
+ ...l.keys(),
20
+ ...u.keys(),
21
+ ...d.keys()
22
+ ])].filter(g).sort(a), y = (e, t) => {
23
+ let n = "^";
24
+ for (let t = 0; t < e.length; t++) {
25
+ let r = e[t];
26
+ if (r === "\\" && t + 1 < e.length) n += e[++t].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ else if (r === "*") n += ".*";
28
+ else if (r === "?") n += ".";
29
+ else if (r === "[") throw Error("Memory SCAN requires literal keys or * and ? patterns");
30
+ else n += r.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ }
32
+ return RegExp(n + "$", "su").test(t);
33
+ };
34
+ return {
35
+ command: () => {
36
+ let p = !1, b = () => {
37
+ if (m(), p) throw Error("Memory command generation is closed");
38
+ }, x = (e, t, n = Infinity) => (b(), _(e), l.set(e, {
39
+ value: t,
40
+ expiresAt: n
41
+ }), "OK"), S = {
42
+ readOnly: {
43
+ get: (e) => S.get(e),
44
+ exists: (e) => S.exists(e),
45
+ zrange: (e) => S.zrange(e),
46
+ eval: async (i, a, ...o) => {
47
+ if (b(), i === n) {
48
+ let n = e(JSON.parse(String(o[a])));
49
+ if (!t(n)) throw Error("Memory read-only Lua refuses mutations");
50
+ return S.eval(i, a, ...o);
51
+ }
52
+ if (i !== "\nlocal ops = cjson.decode(ARGV[1])\nlocal tm = redis.call('TIME')\nlocal now = tonumber(tm[1]) * 1000 + math.floor(tonumber(tm[2]) / 1000)\nlocal out = {}\nlocal function fail(op, reason)\n return cjson.encode({ failure = { operation = op.op, key = op.key, reason = reason } })\nend\nlocal function ttl(k, o)\n if o and o.ttlMs then redis.call('PEXPIRE', k, string.format('%.0f', o.ttlMs)) end\nend\nlocal function numberString(k, prefix)\n local raw = redis.call('GET', k)\n if not raw then return nil end\n if string.sub(raw, 1, 2) ~= prefix then error('WRONGTYPE') end\n return string.sub(raw, 3)\nend\nlocal function run(o, k)\n local op = o.op\n local expected\n if string.sub(op, 1, 1) == 'z' then expected = 'zset'\n elseif string.sub(op, 1, 1) == 'h' then expected = 'hash'\n elseif op == 'sadd' or op == 'srem' or op == 'smembers' or op == 'sismember' then expected = 'set' end\n if expected then\n local actual = redis.call('TYPE', k).ok\n if actual ~= 'none' and actual ~= expected then error('WRONGTYPE') end\n end\n if op == 'zadd' then\n local pending = {}\n local mode = o.options and o.options.mode or 'set'\n for _, m in ipairs(o.members) do\n local old\n if mode ~= 'set' then old = pending[m.member] or tonumber(redis.call('ZSCORE', k, m.member)) end\n local score = m.score\n if old then\n if mode == 'max' then score = math.max(old, score)\n elseif mode == 'min' then score = math.min(old, score)\n elseif mode == 'incr' then score = old + score end\n end\n if score == math.huge or score == -math.huge or score ~= score then error('HOT_INVALID') end\n pending[m.member] = score\n end\n -- RESP-family Lua bridges differ in number-to-argument precision.\n for member, score in pairs(pending) do redis.call('ZADD', k, string.format('%.17g', score), member) end\n ttl(k, o.options); return cjson.null\n elseif op == 'zrem' then\n local count = 0\n for _, member in ipairs(o.members) do count = count + redis.call('ZREM', k, member) end\n return count\n elseif op == 'zrank' then\n local cmd = o.order == 'desc' and 'ZREVRANK' or 'ZRANK'\n return redis.call(cmd, k, o.member) or cjson.null\n elseif op == 'zscore' then\n return tonumber(redis.call('ZSCORE', k, o.member)) or cjson.null\n elseif op == 'zcard' then return redis.call('ZCARD', k)\n elseif op == 'zrange' then\n local raw\n local limit = o.options and o.options.limit\n if limit == 0 then return {} end\n if o.range.byRank then\n local cmd = o.order == 'desc' and 'ZREVRANGE' or 'ZRANGE'\n local lo, hi = o.range.byRank[1], o.range.byRank[2]\n if limit then\n local count = redis.call('ZCARD', k)\n if lo < 0 then lo = count + lo end\n if hi < 0 then hi = count + hi end\n lo = math.max(0, lo)\n hi = math.min(hi, lo + limit - 1)\n if hi < lo then return {} end\n end\n raw = redis.call(cmd, k, string.format('%.0f', lo), string.format('%.0f', hi), 'WITHSCORES')\n else\n local cmd = o.order == 'desc' and 'ZREVRANGEBYSCORE' or 'ZRANGEBYSCORE'\n local lo, hi = o.range.byScore[1], o.range.byScore[2]\n if lo > hi then return {} end\n if o.order == 'desc' then lo, hi = hi, lo end\n if limit then raw = redis.call(cmd, k, string.format('%.17g', lo), string.format('%.17g', hi), 'WITHSCORES', 'LIMIT', 0, limit)\n else raw = redis.call(cmd, k, string.format('%.17g', lo), string.format('%.17g', hi), 'WITHSCORES') end\n end\n local result = {}\n for i = 1, #raw / 2 do\n local score = raw[i * 2]\n if type(score) == 'number' then score = string.format('%.17g', score) end\n table.insert(result, { member = raw[i * 2 - 1], score = score })\n end\n return result\n elseif op == 'zremRange' then\n local pair = o.range.byRank or o.range.byScore\n local cmd = o.range.byRank and 'ZREMRANGEBYRANK' or 'ZREMRANGEBYSCORE'\n local format = o.range.byRank and '%.0f' or '%.17g'\n return redis.call(cmd, k, string.format(format, pair[1]), string.format(format, pair[2]))\n elseif op == 'incrBy' then\n local value = tonumber(numberString(k, 'c:')) or 0\n value = value + o.delta\n if math.abs(value) > 9007199254740991 then error('HOT_INVALID') end\n if o.options and o.options.max and value > o.options.max then error('HOT_LIMIT') end\n redis.call('SET', k, 'c:' .. string.format('%.0f', value), 'KEEPTTL')\n ttl(k, o.options); return value\n elseif op == 'tokenBucket' then\n local raw = numberString(k, 'b:')\n local old = raw and cjson.decode(raw) or { tokens = o.config.capacity, updatedAt = now }\n local available = math.min(o.config.capacity, tonumber(old.tokens) + math.max(0, now - tonumber(old.updatedAt)) * o.config.refillPerSec / 1000)\n local allowed = available >= o.cost\n local remaining = available\n if allowed then remaining = remaining - o.cost end\n local retry = allowed and 0 or math.ceil((o.cost - available) / o.config.refillPerSec * 1000)\n local life = math.max(1, math.ceil(o.config.capacity / o.config.refillPerSec * 1000))\n redis.call('SET', k, 'b:' .. cjson.encode({ tokens = string.format('%.17g', remaining), updatedAt = string.format('%.0f', math.max(now, tonumber(old.updatedAt))) }), 'PX', string.format('%.0f', life))\n return { allowed = allowed, remaining = string.format('%.17g', remaining), retryAfterMs = string.format('%.0f', retry) }\n elseif op == 'rateLimit' then\n local raw = numberString(k, 'r:')\n local s = raw and cjson.decode(raw) or nil\n local cfg = o.config\n local at = o.nowMs or now\n local limit, windowMs, capacity = cfg.limit, cfg.windowMs, cfg.burst\n local allowed, remaining, resetAt, retryAfter\n local state\n if cfg.algorithm == 'fixed-window' then\n local start = s and s.kind == 'fixed' and tonumber(s.windowStart) or at\n local count = s and s.kind == 'fixed' and tonumber(s.count) or 0\n if at - start >= windowMs then start = at; count = 0 end\n resetAt = start + windowMs\n allowed = count < limit\n if allowed then count = count + 1 end\n remaining = allowed and limit - count or 0\n retryAfter = allowed and 0 or resetAt - at\n state = { kind = 'fixed', windowStart = start, count = count }\n elseif cfg.algorithm == 'sliding-window' then\n local start = s and s.kind == 'sliding' and tonumber(s.currStart) or at\n local current = s and s.kind == 'sliding' and tonumber(s.currCount) or 0\n local previous = s and s.kind == 'sliding' and tonumber(s.prevCount) or 0\n local elapsed = math.floor((at - start) / windowMs)\n if elapsed >= 2 then previous = 0; current = 0; start = at\n elseif elapsed == 1 then previous = current; current = 0; start = start + windowMs end\n local weight = (windowMs - (at - start)) / windowMs\n local estimate = previous * weight + current\n resetAt = start + windowMs\n allowed = estimate < limit\n if allowed then current = current + 1 end\n remaining = allowed and math.max(0, math.floor(limit - estimate - 1)) or 0\n retryAfter = allowed and 0 or resetAt - at\n state = { kind = 'sliding', currStart = start, currCount = current, prevCount = previous }\n else\n local tokens = s and s.kind == 'token' and tonumber(s.tokens) or capacity\n local last = s and s.kind == 'token' and tonumber(s.lastRefill) or at\n local refill = limit / windowMs\n tokens = math.min(capacity, tokens + (at - last) * refill)\n allowed = tokens >= 1\n if allowed then tokens = tokens - 1 end\n retryAfter = allowed and 0 or math.ceil((1 - tokens) / refill)\n remaining = allowed and math.floor(tokens) or 0\n resetAt = at + (allowed and math.ceil((capacity - tokens) / refill) or retryAfter)\n state = { kind = 'token', tokens = tokens, lastRefill = at }\n end\n for field, value in pairs(state) do\n if type(value) == 'number' then state[field] = string.format('%.17g', value) end\n end\n redis.call('SET', k, 'r:' .. cjson.encode(state), 'PX', string.format('%.0f', math.ceil(math.max(windowMs * 2, 60000) + 1000)))\n return { allowed = allowed, remaining = string.format('%.17g', remaining), resetAtMs = string.format('%.17g', resetAt), retryAfterMs = string.format('%.17g', retryAfter) }\n elseif op == 'hset' then\n for f, v in pairs(o.fields) do redis.call('HSET', k, f, v) end\n ttl(k, o.options); return cjson.null\n elseif op == 'hget' then return redis.call('HGET', k, o.field) or cjson.null\n elseif op == 'hgetAll' then\n local raw = redis.call('HGETALL', k)\n local fields = {}\n for i = 1, #raw, 2 do fields[raw[i]] = raw[i + 1] end\n return fields\n elseif op == 'hdel' then\n local count = 0\n for _, field in ipairs(o.fields) do count = count + redis.call('HDEL', k, field) end\n return count\n elseif op == 'sadd' or op == 'srem' then\n local count = 0\n for _, member in ipairs(o.members) do count = count + redis.call(op == 'sadd' and 'SADD' or 'SREM', k, member) end\n if op == 'sadd' then ttl(k, o.options) end\n return count\n elseif op == 'smembers' then\n local members = redis.call('SMEMBERS', k); table.sort(members); return members\n elseif op == 'sismember' then return redis.call('SISMEMBER', k, o.member) == 1\n elseif op == 'setNx' then return redis.call('SET', k, 'l:' .. o.value, 'NX', 'PX', string.format('%.0f', o.ttlMs)) ~= false\n elseif op == 'compareDelete' or op == 'compareExpire' then\n local value = numberString(k, 'l:')\n if value == nil or value ~= o.value then return false end\n if op == 'compareDelete' then return redis.call('DEL', k) == 1 end\n return redis.call('PEXPIRE', k, string.format('%.0f', o.ttlMs)) == 1\n elseif op == 'expire' then redis.call('PEXPIRE', k, string.format('%.0f', o.ttlMs)); return cjson.null\n elseif op == 'delete' then return redis.call('DEL', k) == 1 end\n error('HOT_INVALID')\nend\nfor i, op in ipairs(ops) do\n local ok, result = pcall(run, op, KEYS[i])\n if not ok then\n local reason = 'unavailable'\n if string.find(result, 'WRONGTYPE', 1, true) then reason = 'wrongType'\n elseif string.find(result, 'HOT_LIMIT', 1, true) then reason = 'limit'\n elseif string.find(result, 'HOT_INVALID', 1, true) then reason = 'invalid' end\n return fail(op, reason)\n end\n -- Redis cjson numbers have only 14 significant digits. Use strings on\n -- known numeric result slots so safe integers and fractional scores survive.\n if type(result) == 'number' then result = string.format('%.17g', result) end\n table.insert(out, { value = result })\nend\nreturn cjson.encode({ results = out })\n") throw Error("Memory read-only Lua requires a framework read script");
53
+ let s = JSON.parse(String(o[a]));
54
+ if (!Array.isArray(s) || !s.every((e) => r(e))) throw Error("Memory read-only Lua refuses mutations");
55
+ return S.eval(i, a, ...o);
56
+ }
57
+ },
58
+ crossNode: !1,
59
+ ping: async () => (b(), "PONG"),
60
+ get: async (e) => {
61
+ if (b(), h(e), u.has(e) || d.has(e)) throw Error("Memory key does not contain a plain string");
62
+ return l.get(e)?.value ?? null;
63
+ },
64
+ set: async (e, t) => x(e, t),
65
+ psetex: async (e, t, n) => {
66
+ if (!Number.isSafeInteger(t) || t <= 0) throw Error("Memory PSETEX requires a positive integer");
67
+ return x(e, n, c() + t);
68
+ },
69
+ del: async (e) => (b(), [...new Set(e)].reduce((e, t) => e + _(t), 0)),
70
+ exists: async (e) => (b(), +!!g(e)),
71
+ zrange: async (e) => {
72
+ if (b(), h(e), l.has(e) || d.has(e)) throw Error("Memory key is not a tag sorted set");
73
+ return [...u.get(e)?.values.entries() ?? []].sort((e, t) => e[1] - t[1] || a(e[0], t[0])).map(([e]) => e);
74
+ },
75
+ scan: async (e, t, n) => {
76
+ if (b(), e !== "0") throw Error("Memory SCAN requires its terminal cursor 0");
77
+ if (!Number.isSafeInteger(n) || n <= 0) throw Error("Memory SCAN requires a positive count");
78
+ return {
79
+ cursor: "0",
80
+ keys: v().filter((e) => y(t, e))
81
+ };
82
+ },
83
+ eval: async (e, t, ...r) => {
84
+ if (b(), !Number.isSafeInteger(t) || t < 0 || t > r.length) throw Error("Memory EVAL requires a valid key count");
85
+ if (e === n) {
86
+ if (r.length !== t + 1 && r.length !== t + 2) throw Error("Invalid atomic cache arguments");
87
+ let e = c(), n = (t) => (h(t, e), l.has(t) ? "string" : u.has(t) ? "zset" : d.has(t) ? "other" : "none");
88
+ return s({
89
+ now: e,
90
+ kind: n,
91
+ get: (e) => {
92
+ if (!["none", "string"].includes(n(e))) throw Error("Invalid cache string state");
93
+ return l.get(e)?.value ?? null;
94
+ },
95
+ set: (t, n, r) => {
96
+ l.set(t, {
97
+ value: n,
98
+ expiresAt: r === void 0 ? Infinity : e + r
99
+ }), u.delete(t), d.delete(t);
100
+ },
101
+ remove: (e) => {
102
+ let t = n(e) === "none" ? 0 : 1;
103
+ return l.delete(e), u.delete(e), d.delete(e), t;
104
+ },
105
+ members: (e) => (n(e), [...u.get(e)?.values ?? []].map(([e, t]) => ({
106
+ member: e,
107
+ score: t
108
+ }))),
109
+ addMember: (e, t, n) => {
110
+ let r = u.get(e) ?? {
111
+ values: /* @__PURE__ */ new Map(),
112
+ expiresAt: Infinity
113
+ };
114
+ r.values.set(t, n), u.set(e, r);
115
+ },
116
+ removeMember: (e, t) => {
117
+ u.get(e)?.values.delete(t);
118
+ },
119
+ expire: (t, n) => {
120
+ let r = u.get(t);
121
+ r !== void 0 && (r.expiresAt = n === null ? Infinity : e + n);
122
+ },
123
+ keys: () => [.../* @__PURE__ */ new Set([
124
+ ...l.keys(),
125
+ ...u.keys(),
126
+ ...d.keys()
127
+ ])].filter((e) => n(e) !== "none").sort(a)
128
+ }, r.slice(0, t).map(String), JSON.parse(String(r[t])), r.length === t + 2 ? JSON.parse(String(r[t + 1])) : {});
129
+ }
130
+ if (e === "return redis.call('PUBLISH', ARGV[1], ARGV[2])") {
131
+ if (t !== 0 || r.length !== 2) throw Error("Invalid publication arguments");
132
+ let e = String(r[0]), n = String(r[1]), i = 0;
133
+ for (let t of [...f]) t.channels.has(e) && (i++, t.deliver(e, n));
134
+ return i;
135
+ }
136
+ if (e === "\nlocal ops = cjson.decode(ARGV[1])\nlocal tm = redis.call('TIME')\nlocal now = tonumber(tm[1]) * 1000 + math.floor(tonumber(tm[2]) / 1000)\nlocal out = {}\nlocal function fail(op, reason)\n return cjson.encode({ failure = { operation = op.op, key = op.key, reason = reason } })\nend\nlocal function ttl(k, o)\n if o and o.ttlMs then redis.call('PEXPIRE', k, string.format('%.0f', o.ttlMs)) end\nend\nlocal function numberString(k, prefix)\n local raw = redis.call('GET', k)\n if not raw then return nil end\n if string.sub(raw, 1, 2) ~= prefix then error('WRONGTYPE') end\n return string.sub(raw, 3)\nend\nlocal function run(o, k)\n local op = o.op\n local expected\n if string.sub(op, 1, 1) == 'z' then expected = 'zset'\n elseif string.sub(op, 1, 1) == 'h' then expected = 'hash'\n elseif op == 'sadd' or op == 'srem' or op == 'smembers' or op == 'sismember' then expected = 'set' end\n if expected then\n local actual = redis.call('TYPE', k).ok\n if actual ~= 'none' and actual ~= expected then error('WRONGTYPE') end\n end\n if op == 'zadd' then\n local pending = {}\n local mode = o.options and o.options.mode or 'set'\n for _, m in ipairs(o.members) do\n local old\n if mode ~= 'set' then old = pending[m.member] or tonumber(redis.call('ZSCORE', k, m.member)) end\n local score = m.score\n if old then\n if mode == 'max' then score = math.max(old, score)\n elseif mode == 'min' then score = math.min(old, score)\n elseif mode == 'incr' then score = old + score end\n end\n if score == math.huge or score == -math.huge or score ~= score then error('HOT_INVALID') end\n pending[m.member] = score\n end\n -- RESP-family Lua bridges differ in number-to-argument precision.\n for member, score in pairs(pending) do redis.call('ZADD', k, string.format('%.17g', score), member) end\n ttl(k, o.options); return cjson.null\n elseif op == 'zrem' then\n local count = 0\n for _, member in ipairs(o.members) do count = count + redis.call('ZREM', k, member) end\n return count\n elseif op == 'zrank' then\n local cmd = o.order == 'desc' and 'ZREVRANK' or 'ZRANK'\n return redis.call(cmd, k, o.member) or cjson.null\n elseif op == 'zscore' then\n return tonumber(redis.call('ZSCORE', k, o.member)) or cjson.null\n elseif op == 'zcard' then return redis.call('ZCARD', k)\n elseif op == 'zrange' then\n local raw\n local limit = o.options and o.options.limit\n if limit == 0 then return {} end\n if o.range.byRank then\n local cmd = o.order == 'desc' and 'ZREVRANGE' or 'ZRANGE'\n local lo, hi = o.range.byRank[1], o.range.byRank[2]\n if limit then\n local count = redis.call('ZCARD', k)\n if lo < 0 then lo = count + lo end\n if hi < 0 then hi = count + hi end\n lo = math.max(0, lo)\n hi = math.min(hi, lo + limit - 1)\n if hi < lo then return {} end\n end\n raw = redis.call(cmd, k, string.format('%.0f', lo), string.format('%.0f', hi), 'WITHSCORES')\n else\n local cmd = o.order == 'desc' and 'ZREVRANGEBYSCORE' or 'ZRANGEBYSCORE'\n local lo, hi = o.range.byScore[1], o.range.byScore[2]\n if lo > hi then return {} end\n if o.order == 'desc' then lo, hi = hi, lo end\n if limit then raw = redis.call(cmd, k, string.format('%.17g', lo), string.format('%.17g', hi), 'WITHSCORES', 'LIMIT', 0, limit)\n else raw = redis.call(cmd, k, string.format('%.17g', lo), string.format('%.17g', hi), 'WITHSCORES') end\n end\n local result = {}\n for i = 1, #raw / 2 do\n local score = raw[i * 2]\n if type(score) == 'number' then score = string.format('%.17g', score) end\n table.insert(result, { member = raw[i * 2 - 1], score = score })\n end\n return result\n elseif op == 'zremRange' then\n local pair = o.range.byRank or o.range.byScore\n local cmd = o.range.byRank and 'ZREMRANGEBYRANK' or 'ZREMRANGEBYSCORE'\n local format = o.range.byRank and '%.0f' or '%.17g'\n return redis.call(cmd, k, string.format(format, pair[1]), string.format(format, pair[2]))\n elseif op == 'incrBy' then\n local value = tonumber(numberString(k, 'c:')) or 0\n value = value + o.delta\n if math.abs(value) > 9007199254740991 then error('HOT_INVALID') end\n if o.options and o.options.max and value > o.options.max then error('HOT_LIMIT') end\n redis.call('SET', k, 'c:' .. string.format('%.0f', value), 'KEEPTTL')\n ttl(k, o.options); return value\n elseif op == 'tokenBucket' then\n local raw = numberString(k, 'b:')\n local old = raw and cjson.decode(raw) or { tokens = o.config.capacity, updatedAt = now }\n local available = math.min(o.config.capacity, tonumber(old.tokens) + math.max(0, now - tonumber(old.updatedAt)) * o.config.refillPerSec / 1000)\n local allowed = available >= o.cost\n local remaining = available\n if allowed then remaining = remaining - o.cost end\n local retry = allowed and 0 or math.ceil((o.cost - available) / o.config.refillPerSec * 1000)\n local life = math.max(1, math.ceil(o.config.capacity / o.config.refillPerSec * 1000))\n redis.call('SET', k, 'b:' .. cjson.encode({ tokens = string.format('%.17g', remaining), updatedAt = string.format('%.0f', math.max(now, tonumber(old.updatedAt))) }), 'PX', string.format('%.0f', life))\n return { allowed = allowed, remaining = string.format('%.17g', remaining), retryAfterMs = string.format('%.0f', retry) }\n elseif op == 'rateLimit' then\n local raw = numberString(k, 'r:')\n local s = raw and cjson.decode(raw) or nil\n local cfg = o.config\n local at = o.nowMs or now\n local limit, windowMs, capacity = cfg.limit, cfg.windowMs, cfg.burst\n local allowed, remaining, resetAt, retryAfter\n local state\n if cfg.algorithm == 'fixed-window' then\n local start = s and s.kind == 'fixed' and tonumber(s.windowStart) or at\n local count = s and s.kind == 'fixed' and tonumber(s.count) or 0\n if at - start >= windowMs then start = at; count = 0 end\n resetAt = start + windowMs\n allowed = count < limit\n if allowed then count = count + 1 end\n remaining = allowed and limit - count or 0\n retryAfter = allowed and 0 or resetAt - at\n state = { kind = 'fixed', windowStart = start, count = count }\n elseif cfg.algorithm == 'sliding-window' then\n local start = s and s.kind == 'sliding' and tonumber(s.currStart) or at\n local current = s and s.kind == 'sliding' and tonumber(s.currCount) or 0\n local previous = s and s.kind == 'sliding' and tonumber(s.prevCount) or 0\n local elapsed = math.floor((at - start) / windowMs)\n if elapsed >= 2 then previous = 0; current = 0; start = at\n elseif elapsed == 1 then previous = current; current = 0; start = start + windowMs end\n local weight = (windowMs - (at - start)) / windowMs\n local estimate = previous * weight + current\n resetAt = start + windowMs\n allowed = estimate < limit\n if allowed then current = current + 1 end\n remaining = allowed and math.max(0, math.floor(limit - estimate - 1)) or 0\n retryAfter = allowed and 0 or resetAt - at\n state = { kind = 'sliding', currStart = start, currCount = current, prevCount = previous }\n else\n local tokens = s and s.kind == 'token' and tonumber(s.tokens) or capacity\n local last = s and s.kind == 'token' and tonumber(s.lastRefill) or at\n local refill = limit / windowMs\n tokens = math.min(capacity, tokens + (at - last) * refill)\n allowed = tokens >= 1\n if allowed then tokens = tokens - 1 end\n retryAfter = allowed and 0 or math.ceil((1 - tokens) / refill)\n remaining = allowed and math.floor(tokens) or 0\n resetAt = at + (allowed and math.ceil((capacity - tokens) / refill) or retryAfter)\n state = { kind = 'token', tokens = tokens, lastRefill = at }\n end\n for field, value in pairs(state) do\n if type(value) == 'number' then state[field] = string.format('%.17g', value) end\n end\n redis.call('SET', k, 'r:' .. cjson.encode(state), 'PX', string.format('%.0f', math.ceil(math.max(windowMs * 2, 60000) + 1000)))\n return { allowed = allowed, remaining = string.format('%.17g', remaining), resetAtMs = string.format('%.17g', resetAt), retryAfterMs = string.format('%.17g', retryAfter) }\n elseif op == 'hset' then\n for f, v in pairs(o.fields) do redis.call('HSET', k, f, v) end\n ttl(k, o.options); return cjson.null\n elseif op == 'hget' then return redis.call('HGET', k, o.field) or cjson.null\n elseif op == 'hgetAll' then\n local raw = redis.call('HGETALL', k)\n local fields = {}\n for i = 1, #raw, 2 do fields[raw[i]] = raw[i + 1] end\n return fields\n elseif op == 'hdel' then\n local count = 0\n for _, field in ipairs(o.fields) do count = count + redis.call('HDEL', k, field) end\n return count\n elseif op == 'sadd' or op == 'srem' then\n local count = 0\n for _, member in ipairs(o.members) do count = count + redis.call(op == 'sadd' and 'SADD' or 'SREM', k, member) end\n if op == 'sadd' then ttl(k, o.options) end\n return count\n elseif op == 'smembers' then\n local members = redis.call('SMEMBERS', k); table.sort(members); return members\n elseif op == 'sismember' then return redis.call('SISMEMBER', k, o.member) == 1\n elseif op == 'setNx' then return redis.call('SET', k, 'l:' .. o.value, 'NX', 'PX', string.format('%.0f', o.ttlMs)) ~= false\n elseif op == 'compareDelete' or op == 'compareExpire' then\n local value = numberString(k, 'l:')\n if value == nil or value ~= o.value then return false end\n if op == 'compareDelete' then return redis.call('DEL', k) == 1 end\n return redis.call('PEXPIRE', k, string.format('%.0f', o.ttlMs)) == 1\n elseif op == 'expire' then redis.call('PEXPIRE', k, string.format('%.0f', o.ttlMs)); return cjson.null\n elseif op == 'delete' then return redis.call('DEL', k) == 1 end\n error('HOT_INVALID')\nend\nfor i, op in ipairs(ops) do\n local ok, result = pcall(run, op, KEYS[i])\n if not ok then\n local reason = 'unavailable'\n if string.find(result, 'WRONGTYPE', 1, true) then reason = 'wrongType'\n elseif string.find(result, 'HOT_LIMIT', 1, true) then reason = 'limit'\n elseif string.find(result, 'HOT_INVALID', 1, true) then reason = 'invalid' end\n return fail(op, reason)\n end\n -- Redis cjson numbers have only 14 significant digits. Use strings on\n -- known numeric result slots so safe integers and fractional scores survive.\n if type(result) == 'number' then result = string.format('%.17g', result) end\n table.insert(out, { value = result })\nend\nreturn cjson.encode({ results = out })\n") {
137
+ let e = JSON.parse(String(r[t]));
138
+ if (!Array.isArray(e) || e.length !== t || r.length !== t + 1) throw Error("Invalid HotStore batch");
139
+ try {
140
+ let t = e.map((e, t) => {
141
+ let n = String(r[t]);
142
+ if (h(n), l.has(n) || u.has(n)) throw new o({
143
+ operation: e.op,
144
+ key: e.key,
145
+ reason: "wrongType"
146
+ });
147
+ return {
148
+ ...e,
149
+ key: n
150
+ };
151
+ }), n = i(d, t, c());
152
+ return JSON.stringify({ results: n.map((e) => ({ value: e })) });
153
+ } catch (n) {
154
+ if (!(n instanceof o)) throw n;
155
+ let i = r.slice(0, t).findIndex((e) => e === n.key);
156
+ return JSON.stringify({ failure: {
157
+ operation: n.operation,
158
+ key: i < 0 ? n.key : e[i].key,
159
+ reason: n.reason
160
+ } });
161
+ }
162
+ }
163
+ throw Error("Memory connections execute framework scripts only; arbitrary Lua requires a Redis transport");
164
+ },
165
+ close: async () => {
166
+ p = !0;
167
+ }
168
+ };
169
+ return S;
170
+ },
171
+ subscriber: () => {
172
+ m();
173
+ let e = !1, t = /* @__PURE__ */ new Set(), n = /* @__PURE__ */ new Set(), r = {
174
+ channels: n,
175
+ deliver: (e, n) => {
176
+ for (let r of [...t]) try {
177
+ r(e, n);
178
+ } catch {}
179
+ }
180
+ };
181
+ f.add(r);
182
+ let i = () => {
183
+ if (m(), e) throw Error("Memory subscriber is closed");
184
+ };
185
+ return {
186
+ subscribe: async (e) => {
187
+ i(), n.add(e);
188
+ },
189
+ unsubscribe: async (e) => {
190
+ i(), n.delete(e);
191
+ },
192
+ onMessage: (e) => (i(), t.add(e), () => {
193
+ t.delete(e);
194
+ }),
195
+ onTransportEvent: () => (i(), () => {}),
196
+ close: async () => {
197
+ e || (e = !0, f.delete(r), n.clear(), t.clear());
198
+ }
199
+ };
200
+ },
201
+ dispose: () => {
202
+ p = !0, l.clear(), u.clear(), d.clear(), f.clear();
203
+ }
204
+ };
205
+ };
206
+ //#endregion
207
+ export { c as makeMemoryConnectionState };
@@ -0,0 +1,79 @@
1
+ import { Effect as e, Metric as t } from "effect";
2
+ //#region src/connectionMetrics.ts
3
+ var n = /* @__PURE__ */ new Map(), r = t.gauge("voltro_connection_up", { description: "Whether every current pool for this reference has observed readiness and admits commands; not a persistence or capacity measurement." }), i = t.gauge("voltro_connection_pools", { description: "Owned physical generations, including draining generations." }), a = t.gauge("voltro_connection_active_commands", { description: "Borrowed generation leases, including complete streams and transactions." }), o = t.counter("voltro_connection_commands_total", { description: "Admitted commands and scoped streams; includes read-only probes." }), s = t.counter("voltro_connection_errors_total", { description: "Failed registry operations; includes rejected circuit admission and connection construction." }), c = t.gauge("voltro_connection_latency_ms", { description: "Duration of the most recently completed command or scoped stream in milliseconds." }), l = t.histogram("voltro_connection_duration_seconds", {
4
+ boundaries: t.exponentialBoundaries({
5
+ start: .001,
6
+ factor: 2,
7
+ count: 16
8
+ }),
9
+ description: "Complete command and scoped stream duration in seconds."
10
+ }), u = t.counter("voltro_connection_rotations_total", { description: "Completed whole-registry rotations by outcome, including rolled-back attempts." }), d = t.histogram("voltro_connection_rotation_duration_seconds", {
11
+ boundaries: t.exponentialBoundaries({
12
+ start: .001,
13
+ factor: 2,
14
+ count: 16
15
+ }),
16
+ description: "Whole-registry rotation duration in seconds."
17
+ }), f = (f) => {
18
+ let p = /* @__PURE__ */ new WeakMap(), m = (e, n) => t.withAttributes(t.withAttributes(e, { namespace: f }), { connection: n }), h = (o, s) => {
19
+ let c = [...n.get(o)?.values() ?? []], l = c.filter((e) => e.current);
20
+ e.runSync(e.all([
21
+ t.update(m(r, s), l.length > 0 && l.every((e) => e.available) ? 1 : 0),
22
+ t.update(m(i, s), c.length),
23
+ t.update(m(a, s), c.reduce((e, t) => e + t.active, 0))
24
+ ], { discard: !0 }));
25
+ }, g = (e, t) => {
26
+ let n = p.get(e);
27
+ n !== void 0 && (t(n.measurement), h(n.key, n.reference));
28
+ };
29
+ return {
30
+ register: (r, i) => {
31
+ let a = JSON.stringify([f, i]), c = {}, l = {
32
+ current: !1,
33
+ available: !1,
34
+ active: 0
35
+ }, u = n.get(a) ?? /* @__PURE__ */ new Map();
36
+ u.set(c, l), n.set(a, u), p.set(r, {
37
+ key: a,
38
+ reference: i,
39
+ token: c,
40
+ measurement: l
41
+ }), e.runSync(e.andThen(t.update(m(o, i), 0), t.update(m(s, i), 0))), h(a, i);
42
+ },
43
+ available: (e, t) => g(e, (e) => {
44
+ e.available = t;
45
+ }),
46
+ activate: (e) => g(e, (e) => {
47
+ e.current = !0;
48
+ }),
49
+ retire: (e) => g(e, (e) => {
50
+ e.current = !1;
51
+ }),
52
+ acquire: (e) => g(e, (e) => {
53
+ e.active++;
54
+ }),
55
+ release: (e) => g(e, (e) => {
56
+ e.active--;
57
+ }),
58
+ close: (e) => {
59
+ let t = p.get(e);
60
+ if (t === void 0) return;
61
+ let r = n.get(t.key);
62
+ r?.delete(t.token), r?.size === 0 && n.delete(t.key), p.delete(e), h(t.key, t.reference);
63
+ },
64
+ command: (n) => {
65
+ e.runSync(t.update(m(o, n), 1));
66
+ },
67
+ error: (n) => {
68
+ e.runSync(t.update(m(s, n), 1));
69
+ },
70
+ completed: (n, r) => {
71
+ e.runSync(e.andThen(t.update(m(c, n), r), t.update(m(l, n), r / 1e3)));
72
+ },
73
+ rotated: (n, r) => {
74
+ e.runSync(e.andThen(t.update(t.withAttributes(t.withAttributes(u, { namespace: f }), { outcome: n }), 1), t.update(t.withAttributes(d, { namespace: f }), r / 1e3)));
75
+ }
76
+ };
77
+ };
78
+ //#endregion
79
+ export { f as makeConnectionMetrics };
@@ -0,0 +1,19 @@
1
+ //#region src/connectionNamespace.ts
2
+ var e = (e) => encodeURIComponent(e).replace(/[!'()*]/g, (e) => `%${e.charCodeAt(0).toString(16).toUpperCase()}`), t = (t) => {
3
+ if (typeof t.appName != "string" || !t.appName.trim() || typeof t.environment != "string" || !t.environment.trim()) throw Error("Connection namespace requires a nonempty app name and environment");
4
+ return `voltro:${e(t.appName)}:${e(t.environment)}:`;
5
+ }, n = (e, n) => {
6
+ let r = t(n), i = e.keyPrefix ?? r;
7
+ if (typeof i != "string") throw Error("Connection keyPrefix must be a string");
8
+ if (e.allowSharedNamespace !== void 0 && typeof e.allowSharedNamespace != "boolean") throw Error("Connection allowSharedNamespace must be a boolean");
9
+ if (n.environment === "production") {
10
+ if (!i.trim()) throw Error("Production connection keyPrefix must be nonempty");
11
+ if (i !== r && e.allowSharedNamespace !== !0) throw Error("A custom production connection keyPrefix requires allowSharedNamespace: true");
12
+ }
13
+ return {
14
+ ...e,
15
+ keyPrefix: i
16
+ };
17
+ };
18
+ //#endregion
19
+ export { n as applyConnectionNamespace, t as connectionNamespace };