@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
package/dist/hot.js ADDED
@@ -0,0 +1,7 @@
1
+ import { consumeHotRateLimit as e } from "./hotRateLimit.js";
2
+ import { readHotStoreWithFallback as t } from "./hotReadAvailability.js";
3
+ import { HotError as n, HotStore as r } from "./hot-contract.js";
4
+ import { HOT_MEMORY_DEFAULTS as i, layerHotMemory as a, makeMemoryHotStore as o } from "./hotMemory.js";
5
+ import { HOT_REDIS_DEFAULTS as s, layerHotRedis as c, makeRedisHotStore as l } from "./hotRedis.js";
6
+ import { layerRoutedHotRedis as u } from "./hotRouting.js";
7
+ export { i as HOT_MEMORY_DEFAULTS, s as HOT_REDIS_DEFAULTS, n as HotError, r as HotStore, e as consumeHotRateLimit, a as layerHotMemory, c as layerHotRedis, u as layerRoutedHotRedis, o as makeMemoryHotStore, l as makeRedisHotStore, t as readHotStoreWithFallback };
@@ -0,0 +1,46 @@
1
+ import { hotStoreFromRunner as e, validateHotOp as t } from "./hotOperations.js";
2
+ import { applyHotOperations as n, compareHotMembers as r } from "./hotState.js";
3
+ import { HotError as i, HotStore as a } from "./hot-contract.js";
4
+ import { Clock as o, Effect as s, Layer as c, Stream as l } from "effect";
5
+ //#region src/hotMemory.ts
6
+ var u = { sweepBatchSize: 32 }, d = (a = {}) => s.try({
7
+ try: () => {
8
+ let c = /* @__PURE__ */ new Map(), d = a.sweepBatchSize ?? u.sweepBatchSize;
9
+ if (!Number.isSafeInteger(d) || d < 1) throw Error("HotStore sweepBatchSize must be a positive integer");
10
+ let f = c.entries(), p = a.now === void 0 ? o.currentTimeMillis : s.sync(a.now);
11
+ return e((e) => p.pipe(s.flatMap((r) => s.try({
12
+ try: () => {
13
+ e.forEach(t);
14
+ for (let e = 0; e < d; e++) {
15
+ let e = f.next();
16
+ if (e.done) {
17
+ f = c.entries();
18
+ break;
19
+ }
20
+ e.value[1].expiresAt <= r && c.delete(e.value[0]);
21
+ }
22
+ return n(c, e, r);
23
+ },
24
+ catch: (e) => e instanceof i ? e : new i({
25
+ operation: "multi",
26
+ key: "",
27
+ reason: "invalid"
28
+ })
29
+ }))), (e) => l.unwrap(p.pipe(s.map((t) => {
30
+ let n = [];
31
+ for (let [r, i] of c) i.expiresAt <= t ? c.delete(r) : r.startsWith(e) && n.push(r);
32
+ return l.fromIterable(n.sort(r));
33
+ }))), {
34
+ atomicMulti: !0,
35
+ crossNode: !1,
36
+ persistent: !1
37
+ });
38
+ },
39
+ catch: () => new i({
40
+ operation: "connect",
41
+ key: "",
42
+ reason: "invalid"
43
+ })
44
+ }), f = (e = {}) => c.effect(a, d(e));
45
+ //#endregion
46
+ export { u as HOT_MEMORY_DEFAULTS, f as layerHotMemory, d as makeMemoryHotStore };
@@ -0,0 +1,223 @@
1
+ import { HotError as e } from "./hot-contract.js";
2
+ import { Effect as t, Option as n } from "effect";
3
+ //#region src/hotOperations.ts
4
+ var r = /* @__PURE__ */ new Set([
5
+ "zrank",
6
+ "zscore",
7
+ "zrange",
8
+ "zcard",
9
+ "hget",
10
+ "hgetAll",
11
+ "smembers",
12
+ "sismember"
13
+ ]), i = (e) => r.has(e.op), a = (t, n, r) => {
14
+ if (t !== void 0 && (typeof t != "object" || !t || Array.isArray(t) || t.consistency !== void 0 && t.consistency !== "primary" && t.consistency !== "eventual")) throw new e({
15
+ operation: n,
16
+ key: r,
17
+ reason: "invalid"
18
+ });
19
+ }, o = (t) => {
20
+ let n = () => {
21
+ throw new e({
22
+ operation: t.op,
23
+ key: t.key,
24
+ reason: "invalid"
25
+ });
26
+ }, r = (e) => {
27
+ Number.isFinite(e) || n();
28
+ }, i = (e) => {
29
+ (typeof e != "string" || /[\uD800-\uDFFF]/u.test(e)) && n();
30
+ }, a = (e) => {
31
+ e !== void 0 && (!Number.isSafeInteger(e) || e <= 0) && n();
32
+ }, o = (e) => {
33
+ let t = "byRank" in e ? e.byRank : e.byScore;
34
+ t.forEach(r), "byRank" in e && !t.every(Number.isSafeInteger) && n();
35
+ };
36
+ if (i(t.key), t.key.length === 0 && n(), "member" in t && i(t.member), "field" in t && i(t.field), "value" in t && i(t.value), "members" in t && t.op !== "zadd" && t.members.forEach(i), t.op === "hset") for (let [e, n] of Object.entries(t.fields)) i(e), i(n);
37
+ switch (t.op === "hdel" && t.fields.forEach(i), "options" in t && t.options !== void 0 && "ttlMs" in t.options && a(t.options.ttlMs), t.op) {
38
+ case "zadd":
39
+ t.options?.mode !== void 0 && ![
40
+ "set",
41
+ "max",
42
+ "min",
43
+ "incr"
44
+ ].includes(t.options.mode) && n();
45
+ for (let e of t.members) r(e.score), i(e.member);
46
+ break;
47
+ case "zrange":
48
+ t.options !== void 0 && "consistency" in t.options && t.options.consistency !== void 0 && n(), t.options?.limit !== void 0 && (!Number.isSafeInteger(t.options.limit) || t.options.limit < 0) && n(), o(t.range), ["asc", "desc"].includes(t.order) || n();
49
+ break;
50
+ case "zremRange":
51
+ o(t.range);
52
+ break;
53
+ case "zrank":
54
+ ["asc", "desc"].includes(t.order) || n();
55
+ break;
56
+ case "incrBy":
57
+ Number.isSafeInteger(t.delta) || n(), t.options?.max !== void 0 && !Number.isSafeInteger(t.options.max) && n();
58
+ break;
59
+ case "tokenBucket":
60
+ r(t.config.capacity), r(t.config.refillPerSec), r(t.cost), (t.config.capacity <= 0 || t.config.refillPerSec <= 0 || t.cost <= 0 || t.cost > t.config.capacity) && n(), Number.isSafeInteger(Math.ceil(t.config.capacity / t.config.refillPerSec * 1e3)) || n();
61
+ break;
62
+ case "expire":
63
+ case "setNx":
64
+ a(t.ttlMs);
65
+ break;
66
+ case "compareDelete":
67
+ i(t.value);
68
+ break;
69
+ case "compareExpire":
70
+ i(t.value), t.ttlMs === void 0 && n(), a(t.ttlMs);
71
+ break;
72
+ case "rateLimit":
73
+ if ([
74
+ "fixed-window",
75
+ "sliding-window",
76
+ "token-bucket"
77
+ ].includes(t.config.algorithm) || n(), r(t.config.limit), r(t.config.windowMs), r(t.config.burst), (t.config.limit < 0 || t.config.windowMs <= 0 || t.config.burst < 0 || t.config.algorithm === "token-bucket" && (t.config.limit === 0 || t.config.burst === 0) || !Number.isSafeInteger(Math.ceil(Math.max(t.config.windowMs * 2, 6e4) + 1e3))) && n(), t.config.algorithm === "token-bucket") {
78
+ let e = t.config.limit / t.config.windowMs;
79
+ (!Number.isFinite(e) || e <= 0 || !Number.isSafeInteger(Math.ceil(t.config.burst / e))) && n();
80
+ }
81
+ t.nowMs !== void 0 && (!Number.isFinite(t.nowMs) || Math.abs(t.nowMs) > 2 ** 53 - 1 - t.config.windowMs * 2) && n();
82
+ }
83
+ }, s = (r, i, o) => {
84
+ let s = (n, i) => t.gen(function* () {
85
+ return yield* t.try({
86
+ try: () => a(i, n.op, n.key),
87
+ catch: () => new e({
88
+ operation: n.op,
89
+ key: n.key,
90
+ reason: "invalid"
91
+ })
92
+ }), (yield* r([n], i))[0];
93
+ });
94
+ return {
95
+ zadd: (e, n, r) => s({
96
+ op: "zadd",
97
+ key: e,
98
+ members: n,
99
+ ...r === void 0 ? {} : { options: r }
100
+ }).pipe(t.asVoid),
101
+ zrem: (e, t) => s({
102
+ op: "zrem",
103
+ key: e,
104
+ members: t
105
+ }),
106
+ zrank: (e, r, i, a) => s({
107
+ op: "zrank",
108
+ key: e,
109
+ member: r,
110
+ order: i
111
+ }, a).pipe(t.map(n.fromNullishOr)),
112
+ zscore: (e, r, i) => s({
113
+ op: "zscore",
114
+ key: e,
115
+ member: r
116
+ }, i).pipe(t.map(n.fromNullishOr)),
117
+ zrange: (e, t, n, r) => s({
118
+ op: "zrange",
119
+ key: e,
120
+ range: t,
121
+ order: n,
122
+ ...r?.limit === void 0 ? {} : { options: { limit: r.limit } }
123
+ }, r),
124
+ zcard: (e, t) => s({
125
+ op: "zcard",
126
+ key: e
127
+ }, t),
128
+ zremRange: (e, t) => s({
129
+ op: "zremRange",
130
+ key: e,
131
+ range: t
132
+ }),
133
+ incrBy: (e, t, n) => s({
134
+ op: "incrBy",
135
+ key: e,
136
+ delta: t,
137
+ ...n === void 0 ? {} : { options: n }
138
+ }),
139
+ tokenBucket: (e, t, n = 1) => s({
140
+ op: "tokenBucket",
141
+ key: e,
142
+ config: t,
143
+ cost: n
144
+ }),
145
+ rateLimit: (e, t, n) => s({
146
+ op: "rateLimit",
147
+ key: e,
148
+ config: t,
149
+ ...n === void 0 ? {} : { nowMs: n }
150
+ }),
151
+ hset: (e, n, r) => s({
152
+ op: "hset",
153
+ key: e,
154
+ fields: n,
155
+ ...r === void 0 ? {} : { options: r }
156
+ }).pipe(t.asVoid),
157
+ hget: (e, r, i) => s({
158
+ op: "hget",
159
+ key: e,
160
+ field: r
161
+ }, i).pipe(t.map(n.fromNullishOr)),
162
+ hgetAll: (e, t) => s({
163
+ op: "hgetAll",
164
+ key: e
165
+ }, t),
166
+ hdel: (e, t) => s({
167
+ op: "hdel",
168
+ key: e,
169
+ fields: t
170
+ }),
171
+ sadd: (e, t, n) => s({
172
+ op: "sadd",
173
+ key: e,
174
+ members: t,
175
+ ...n === void 0 ? {} : { options: n }
176
+ }),
177
+ srem: (e, t) => s({
178
+ op: "srem",
179
+ key: e,
180
+ members: t
181
+ }),
182
+ smembers: (e, t) => s({
183
+ op: "smembers",
184
+ key: e
185
+ }, t),
186
+ sismember: (e, t, n) => s({
187
+ op: "sismember",
188
+ key: e,
189
+ member: t
190
+ }, n),
191
+ setNx: (e, t, n) => s({
192
+ op: "setNx",
193
+ key: e,
194
+ value: t,
195
+ ttlMs: n
196
+ }),
197
+ compareDelete: (e, t) => s({
198
+ op: "compareDelete",
199
+ key: e,
200
+ value: t
201
+ }),
202
+ compareExpire: (e, t, n) => s({
203
+ op: "compareExpire",
204
+ key: e,
205
+ value: t,
206
+ ttlMs: n
207
+ }),
208
+ expire: (e, n) => s({
209
+ op: "expire",
210
+ key: e,
211
+ ttlMs: n
212
+ }).pipe(t.asVoid),
213
+ delete: (e) => s({
214
+ op: "delete",
215
+ key: e
216
+ }),
217
+ scanPrefix: i,
218
+ multi: (e) => r(e),
219
+ capabilities: o
220
+ };
221
+ };
222
+ //#endregion
223
+ export { s as hotStoreFromRunner, i as isHotReadOperation, o as validateHotOp, a as validateHotReadOptions };
@@ -0,0 +1,103 @@
1
+ //#region src/hotRateLimit.ts
2
+ var e = (e, i, a) => {
3
+ switch (i.algorithm) {
4
+ case "fixed-window": return t(e, i, a);
5
+ case "sliding-window": return n(e, i, a);
6
+ case "token-bucket": return r(e, i, a);
7
+ }
8
+ }, t = (e, t, n) => {
9
+ let r = e?.kind === "fixed" ? e.windowStart : n, i = e?.kind === "fixed" ? e.count : 0;
10
+ n - r >= t.windowMs && (r = n, i = 0);
11
+ let a = r + t.windowMs;
12
+ return i >= t.limit ? {
13
+ state: {
14
+ kind: "fixed",
15
+ windowStart: r,
16
+ count: i
17
+ },
18
+ decision: {
19
+ allowed: !1,
20
+ remaining: 0,
21
+ resetAtMs: a,
22
+ retryAfterMs: a - n
23
+ }
24
+ } : (i += 1, {
25
+ state: {
26
+ kind: "fixed",
27
+ windowStart: r,
28
+ count: i
29
+ },
30
+ decision: {
31
+ allowed: !0,
32
+ remaining: t.limit - i,
33
+ resetAtMs: a,
34
+ retryAfterMs: 0
35
+ }
36
+ });
37
+ }, n = (e, t, n) => {
38
+ let r = t.windowMs, i = e?.kind === "sliding" ? e.currStart : n, a = e?.kind === "sliding" ? e.currCount : 0, o = e?.kind === "sliding" ? e.prevCount : 0, s = Math.floor((n - i) / r);
39
+ s >= 2 ? (o = 0, a = 0, i = n) : s === 1 && (o = a, a = 0, i += r);
40
+ let c = (r - (n - i)) / r, l = o * c + a, u = i + r;
41
+ return l >= t.limit ? {
42
+ state: {
43
+ kind: "sliding",
44
+ currStart: i,
45
+ currCount: a,
46
+ prevCount: o
47
+ },
48
+ decision: {
49
+ allowed: !1,
50
+ remaining: 0,
51
+ resetAtMs: u,
52
+ retryAfterMs: u - n
53
+ }
54
+ } : (a += 1, {
55
+ state: {
56
+ kind: "sliding",
57
+ currStart: i,
58
+ currCount: a,
59
+ prevCount: o
60
+ },
61
+ decision: {
62
+ allowed: !0,
63
+ remaining: Math.max(0, Math.floor(t.limit - l - 1)),
64
+ resetAtMs: u,
65
+ retryAfterMs: 0
66
+ }
67
+ });
68
+ }, r = (e, t, n) => {
69
+ let r = t.burst, i = t.limit / t.windowMs, a = e?.kind === "token" ? e.tokens : r, o = e?.kind === "token" ? e.lastRefill : n;
70
+ if (a = Math.min(r, a + (n - o) * i), a >= 1) {
71
+ --a;
72
+ let e = n + Math.ceil((r - a) / i);
73
+ return {
74
+ state: {
75
+ kind: "token",
76
+ tokens: a,
77
+ lastRefill: n
78
+ },
79
+ decision: {
80
+ allowed: !0,
81
+ remaining: Math.floor(a),
82
+ resetAtMs: e,
83
+ retryAfterMs: 0
84
+ }
85
+ };
86
+ }
87
+ let s = Math.ceil((1 - a) / i);
88
+ return {
89
+ state: {
90
+ kind: "token",
91
+ tokens: a,
92
+ lastRefill: n
93
+ },
94
+ decision: {
95
+ allowed: !1,
96
+ remaining: 0,
97
+ resetAtMs: n + s,
98
+ retryAfterMs: s
99
+ }
100
+ };
101
+ };
102
+ //#endregion
103
+ export { e as consumeHotRateLimit };
@@ -0,0 +1,19 @@
1
+ import { HotError as e } from "./hot-contract.js";
2
+ import { Effect as t } from "effect";
3
+ //#region src/hotReadAvailability.ts
4
+ var n = (n) => t.suspend(() => n.onUnavailable !== "degrade" && n.onUnavailable !== "fail" ? t.fail(new e({
5
+ operation: "readFallback",
6
+ key: "",
7
+ reason: "invalid"
8
+ })) : n.read({
9
+ zrank: (...e) => n.store.zrank(...e),
10
+ zscore: (...e) => n.store.zscore(...e),
11
+ zrange: (...e) => n.store.zrange(...e),
12
+ zcard: (...e) => n.store.zcard(...e),
13
+ hget: (...e) => n.store.hget(...e),
14
+ hgetAll: (...e) => n.store.hgetAll(...e),
15
+ smembers: (...e) => n.store.smembers(...e),
16
+ sismember: (...e) => n.store.sismember(...e)
17
+ }).pipe(t.catch((e) => typeof e == "object" && e && e._tag === "HotError" && e.reason === "unavailable" && n.onUnavailable === "degrade" ? t.suspend(n.fallback) : t.fail(e))));
18
+ //#endregion
19
+ export { n as readHotStoreWithFallback };
@@ -0,0 +1,139 @@
1
+ import { RedisConnections as e } from "./connection.js";
2
+ import { hotStoreFromRunner as t, isHotReadOperation as n, validateHotOp as r, validateHotReadOptions as i } from "./hotOperations.js";
3
+ import { HotError as a, HotStore as o } from "./hot-contract.js";
4
+ import { HOT_SCRIPT as s } from "./hotRedisScript.js";
5
+ import { Effect as c, Layer as l, Option as u, Stream as d } from "effect";
6
+ //#region src/hotRedis.ts
7
+ var f = {
8
+ connectionName: "hot",
9
+ namespace: "voltro:hot",
10
+ scanCount: 200
11
+ }, p = (e) => e.replace(/[\\*?\[\]]/g, "\\$&"), m = (e, o = f.namespace, l = f.scanCount) => {
12
+ if (o.length === 0 || /[{}]/.test(o)) throw Error("HotStore namespace must be nonempty and contain no braces");
13
+ if (!Number.isSafeInteger(l) || l < 1) throw Error("HotStore scanCount must be a positive integer");
14
+ let m = `{${o}}:`, h = (e) => m + e;
15
+ return t((t, o) => c.gen(function* () {
16
+ if (yield* c.try({
17
+ try: () => {
18
+ if (t.forEach(r), i(o, t[0]?.op ?? "multi", t[0]?.key ?? ""), o?.consistency === "eventual" && (t.length !== 1 || !n(t[0]))) throw new a({
19
+ operation: "multi",
20
+ key: "",
21
+ reason: "invalid"
22
+ });
23
+ },
24
+ catch: (e) => e instanceof a ? e : new a({
25
+ operation: "multi",
26
+ key: "",
27
+ reason: "invalid"
28
+ })
29
+ }), t.length === 0) return [];
30
+ let l = yield* c.tryPromise({
31
+ try: () => {
32
+ let n = o?.consistency === "eventual" ? e.readOnly : e;
33
+ return n === void 0 ? Promise.reject(/* @__PURE__ */ Error("Read-only Lua is unavailable")) : n.eval(s, t.length, ...t.map((e) => h(e.key)), JSON.stringify(t));
34
+ },
35
+ catch: () => new a({
36
+ operation: "multi",
37
+ key: "",
38
+ reason: "unavailable"
39
+ })
40
+ }), u = yield* c.try({
41
+ try: () => JSON.parse(String(l)),
42
+ catch: () => new a({
43
+ operation: "multi",
44
+ key: "",
45
+ reason: "unavailable"
46
+ })
47
+ });
48
+ return u.failure === void 0 ? u.results?.length === t.length ? yield* c.try({
49
+ try: () => u.results.map(({ value: e }, n) => {
50
+ let r = t[n];
51
+ if ([
52
+ "zrem",
53
+ "zrank",
54
+ "zscore",
55
+ "zcard",
56
+ "zremRange",
57
+ "incrBy",
58
+ "hdel",
59
+ "sadd",
60
+ "srem"
61
+ ].includes(r.op)) return e === null ? null : Number(e);
62
+ if (r.op === "zrange") return Array.isArray(e) ? e.map((e) => ({
63
+ member: e.member,
64
+ score: Number(e.score)
65
+ })) : [];
66
+ if (r.op === "tokenBucket") {
67
+ let t = e;
68
+ return {
69
+ allowed: t.allowed,
70
+ remaining: Number(t.remaining),
71
+ retryAfterMs: Number(t.retryAfterMs)
72
+ };
73
+ }
74
+ if (r.op === "rateLimit") {
75
+ let t = e, n = {
76
+ allowed: t.allowed,
77
+ remaining: Number(t.remaining),
78
+ resetAtMs: Number(t.resetAtMs),
79
+ retryAfterMs: Number(t.retryAfterMs)
80
+ };
81
+ if (typeof n.allowed != "boolean" || ![
82
+ n.remaining,
83
+ n.resetAtMs,
84
+ n.retryAfterMs
85
+ ].every(Number.isFinite)) throw new a({
86
+ operation: "rateLimit",
87
+ key: r.key,
88
+ reason: "unavailable"
89
+ });
90
+ return n;
91
+ }
92
+ return r.op === "smembers" && !Array.isArray(e) ? [] : e;
93
+ }),
94
+ catch: (e) => e instanceof a ? e : new a({
95
+ operation: "multi",
96
+ key: "",
97
+ reason: "unavailable"
98
+ })
99
+ }) : yield* c.fail(new a({
100
+ operation: "multi",
101
+ key: "",
102
+ reason: "unavailable"
103
+ })) : yield* c.fail(new a(u.failure));
104
+ }), (t) => d.unwrap(c.sync(() => {
105
+ let n = /* @__PURE__ */ new Set();
106
+ return d.paginate("0", (r) => c.tryPromise({
107
+ try: () => e.scan(r, `${p(m + t)}*`, l),
108
+ catch: () => new a({
109
+ operation: "scanPrefix",
110
+ key: t,
111
+ reason: "unavailable"
112
+ })
113
+ }).pipe(c.map(({ cursor: e, keys: r }) => {
114
+ let i = [];
115
+ for (let e of r) e.startsWith(m + t) && !n.has(e) && (n.add(e), i.push(e.slice(m.length)));
116
+ return [i, e === "0" ? u.none() : u.some(e)];
117
+ })));
118
+ })), {
119
+ atomicMulti: !0,
120
+ crossNode: e.crossNode ?? !0,
121
+ persistent: !1
122
+ });
123
+ }, h = (t = {}) => l.effect(o, c.gen(function* () {
124
+ let n = yield* (yield* e).client(t.connectionName ?? f.connectionName).pipe(c.mapError(() => new a({
125
+ operation: "connect",
126
+ key: "",
127
+ reason: "unavailable"
128
+ })));
129
+ return yield* c.try({
130
+ try: () => m(n, t.namespace, t.scanCount),
131
+ catch: () => new a({
132
+ operation: "connect",
133
+ key: "",
134
+ reason: "invalid"
135
+ })
136
+ });
137
+ }));
138
+ //#endregion
139
+ export { f as HOT_REDIS_DEFAULTS, h as layerHotRedis, m as makeRedisHotStore };
@@ -0,0 +1,4 @@
1
+ //#region src/hotRedisScript.ts
2
+ var 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";
3
+ //#endregion
4
+ export { e as HOT_SCRIPT };
@@ -0,0 +1,78 @@
1
+ import { ConnectionRegistry as e } from "./connectionPool.js";
2
+ import { hotStoreFromRunner as t, validateHotOp as n, validateHotReadOptions as r } from "./hotOperations.js";
3
+ import { HotError as i, HotStore as a } from "./hot-contract.js";
4
+ import { makeRedisHotStore as o } from "./hotRedis.js";
5
+ import { Effect as s, Layer as c, Stream as l } from "effect";
6
+ //#region src/hotRouting.ts
7
+ var u = (u) => c.effect(a, s.gen(function* () {
8
+ let a = yield* e, c = u.routes?.[u.consumer], d = c?.select, f = u.context === void 0 ? void 0 : Object.freeze({ tenantId: u.context.tenantId }), p = u.consumers?.[u.consumer] ?? u.connection, m = [...new Set(c?.connections ?? [p ?? ""])], h = u.namespace, g = u.scanCount, _ = (e, t = "") => new i({
9
+ operation: e,
10
+ key: t,
11
+ reason: "invalid"
12
+ });
13
+ if (!u.consumer || m.length === 0 || c !== void 0 && (c.keyAffinity !== !0 || typeof c.select != "function")) return yield* s.fail(_("route"));
14
+ let v = !0;
15
+ for (let e of m) {
16
+ if (e !== "" && !/^redis:[A-Za-z][A-Za-z0-9_-]*$/.test(e)) return yield* s.fail(_("route"));
17
+ (!(yield* a.validate(e).pipe(s.mapError(() => _("route")))).tenantScoped || f !== void 0) && (yield* a.config(e, f).pipe(s.mapError(() => _("route")))).driver === "memory" && (v = !1);
18
+ }
19
+ if (h !== void 0 && (!h || /[{}]/.test(h)) || g !== void 0 && (!Number.isSafeInteger(g) || g < 1)) return yield* s.fail(_("route"));
20
+ let y = (e) => s.try({
21
+ try: () => {
22
+ let t = d === void 0 ? m[0] : d({
23
+ key: e,
24
+ ...f === void 0 ? {} : { tenantId: f.tenantId }
25
+ });
26
+ if (!m.includes(t)) throw _("route", e);
27
+ return t;
28
+ },
29
+ catch: () => _("route", e)
30
+ }), b = (e) => a.client(e, f).pipe(s.mapError(() => new i({
31
+ operation: "connect",
32
+ key: "",
33
+ reason: "unavailable"
34
+ })), s.map((e) => o(e, h, g)));
35
+ return t((e, t) => s.gen(function* () {
36
+ if (yield* s.try({
37
+ try: () => {
38
+ e.forEach(n), r(t, e[0]?.op ?? "multi", e[0]?.key ?? "");
39
+ },
40
+ catch: (e) => e instanceof i ? e : _("multi")
41
+ }), e.length === 0) return [];
42
+ let a = /* @__PURE__ */ new Map();
43
+ for (let t of e) a.has(t.key) || a.set(t.key, yield* y(t.key));
44
+ let o = new Set(a.values());
45
+ if (o.size !== 1) return yield* s.fail(_("multi"));
46
+ let c = yield* b(o.values().next().value);
47
+ if (t?.consistency === "eventual") {
48
+ if (e.length !== 1) return yield* s.fail(_("multi"));
49
+ let n = e[0];
50
+ return [yield* s.gen(function* () {
51
+ switch (n.op) {
52
+ case "zrank": return yield* c.zrank(n.key, n.member, n.order, t).pipe(s.map((e) => e._tag === "Some" ? e.value : null));
53
+ case "zscore": return yield* c.zscore(n.key, n.member, t).pipe(s.map((e) => e._tag === "Some" ? e.value : null));
54
+ case "zrange": return yield* c.zrange(n.key, n.range, n.order, {
55
+ ...n.options,
56
+ ...t
57
+ });
58
+ case "zcard": return yield* c.zcard(n.key, t);
59
+ case "hget": return yield* c.hget(n.key, n.field, t).pipe(s.map((e) => e._tag === "Some" ? e.value : null));
60
+ case "hgetAll": return yield* c.hgetAll(n.key, t);
61
+ case "smembers": return yield* c.smembers(n.key, t);
62
+ case "sismember": return yield* c.sismember(n.key, n.member, t);
63
+ default: return yield* s.fail(_(n.op, n.key));
64
+ }
65
+ })];
66
+ }
67
+ return yield* c.multi(e);
68
+ }), (e) => l.unwrap(s.sync(() => {
69
+ let t = /* @__PURE__ */ new Set();
70
+ return l.fromIterable(m).pipe(l.flatMap((t) => l.unwrap(b(t).pipe(s.map((t) => t.scanPrefix(e))))), l.filter((e) => !t.has(e) && (t.add(e), !0)));
71
+ })), {
72
+ atomicMulti: m.length === 1,
73
+ crossNode: v,
74
+ persistent: !1
75
+ });
76
+ }));
77
+ //#endregion
78
+ export { u as layerRoutedHotRedis };