@voltro/kv 0.87.0 → 0.88.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 +169 -6
- package/THIRD-PARTY-NOTICES.md +2 -1345
- package/dist/Kv.js +1 -2
- package/dist/KvStore.js +1 -2
- package/dist/analytics.js +1 -2
- package/dist/backendMemory.js +1 -2
- package/dist/backendRedis.js +1 -2
- package/dist/cacheAtomicProtocol.js +34 -24
- package/dist/cacheAtomicScript.js +19 -2
- package/dist/cacheAtomicState.js +1 -2
- package/dist/connection.js +74 -70
- package/dist/connectionAnalyticsConfig.js +1 -2
- package/dist/connectionConfigError.js +10 -0
- package/dist/connectionConfigProvider.js +1 -2
- package/dist/connectionDefinitions.js +1 -2
- package/dist/connectionMemory.js +1 -2
- package/dist/connectionMetrics.js +1 -2
- package/dist/connectionNamespace.js +1 -2
- package/dist/connectionNats.js +44 -44
- package/dist/connectionNatsConfig.js +1 -2
- package/dist/connectionNatsSubjects.js +1 -2
- package/dist/connectionOwner.js +1 -2
- package/dist/connectionPool.js +252 -252
- package/dist/connectionPrefix.js +1 -2
- package/dist/connectionScripts.js +1 -2
- package/dist/connectionSqlConfig.js +1 -2
- package/dist/connectionStorageConfig.js +1 -2
- package/dist/connectionStorageFacade.js +1 -2
- package/dist/connectionSubscriber.js +47 -47
- package/dist/driverModule.js +9 -0
- package/dist/errors.js +1 -2
- package/dist/expirySweep.js +1 -2
- package/dist/hot-contract.js +1 -2
- package/dist/hot-testing.d.ts +14 -1
- package/dist/hotMemory.js +1 -2
- package/dist/hotOperations.js +1 -2
- package/dist/hotRateLimit.js +1 -2
- package/dist/hotReadAvailability.js +1 -2
- package/dist/hotRedis.js +1 -2
- package/dist/hotRedisScript.js +1 -2
- package/dist/hotRouting.js +2 -3
- package/dist/hotState.js +1 -2
- package/dist/hotStoreConformance.js +76 -78
- package/dist/nativeRedisReads.js +2 -3
- package/dist/nativeRedisTopology.js +64 -63
- package/dist/redisDiagnostics.js +1 -2
- package/dist/redisReadPolicy.js +1 -2
- package/dist/storage.js +1 -2
- package/dist/storageAddress.js +1 -2
- package/package.json +13 -10
package/dist/connectionPrefix.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/connectionPrefix.ts
|
|
2
2
|
var e = (e) => e.replace(/[\\*?\[\]]/g, "\\$&"), t = (t, n) => {
|
|
3
3
|
if (n === void 0 || n === "") return t;
|
|
4
4
|
let r = (e) => n + e, i = (e, t) => {
|
|
@@ -33,5 +33,4 @@ var e = (e) => e.replace(/[\\*?\[\]]/g, "\\$&"), t = (t, n) => {
|
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
|
-
//#endregion
|
|
37
36
|
export { t as withRespKeyPrefix };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { connectionDefinitionsProblem as e } from "./connectionDefinitions.js";
|
|
2
2
|
import { Config as t, ConfigProvider as n, Effect as r, Option as i } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/connectionSqlConfig.ts
|
|
4
4
|
var a = (e, t = {}) => {
|
|
5
5
|
let n = t.ttlMs ?? 6e4;
|
|
6
6
|
if (typeof e != "function" || !Number.isSafeInteger(n) || n <= 0) throw Error("Dynamic SQL requires a resolver and positive ttlMs");
|
|
@@ -115,5 +115,4 @@ var a = (e, t = {}) => {
|
|
|
115
115
|
source: i.isSome(g) ? "override" : "config"
|
|
116
116
|
};
|
|
117
117
|
});
|
|
118
|
-
//#endregion
|
|
119
118
|
export { a as dynamicSql, d as sqlConfigProblem, p as sqlConnectionConfig, f as sqlConnectionName, u as sqlDefinitionProblem };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { connectionDefinitionsProblem as e, connectionMetadataProblem as t } from "./connectionDefinitions.js";
|
|
2
2
|
import { Config as n, ConfigProvider as r, Effect as i, Option as a } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/connectionStorageConfig.ts
|
|
4
4
|
var o = (e, t = {}) => {
|
|
5
5
|
let n = t.ttlMs ?? 6e4;
|
|
6
6
|
if (typeof e != "function" || !Number.isSafeInteger(n) || n <= 0) throw Error("Dynamic storage requires a resolver and positive ttlMs");
|
|
@@ -186,5 +186,4 @@ var o = (e, t = {}) => {
|
|
|
186
186
|
source: h ? "override" : "config"
|
|
187
187
|
};
|
|
188
188
|
});
|
|
189
|
-
//#endregion
|
|
190
189
|
export { o as dynamicStorage, c as storageConfigProblem, l as storageConnectionConfig, s as storageConnectionName };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StorageError as e } from "./storage.js";
|
|
2
2
|
import { Effect as t, Stream as n } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/connectionStorageFacade.ts
|
|
4
4
|
var r = (r) => {
|
|
5
5
|
let i = (n) => t.fail(new e({
|
|
6
6
|
provider: r.config().provider,
|
|
@@ -74,5 +74,4 @@ var r = (r) => {
|
|
|
74
74
|
}
|
|
75
75
|
}));
|
|
76
76
|
};
|
|
77
|
-
//#endregion
|
|
78
77
|
export { r as makeStorageFacade };
|
|
@@ -1,48 +1,49 @@
|
|
|
1
1
|
import { KvError as e } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { missingDriver as t } from "./driverModule.js";
|
|
3
|
+
import { createNativeRedisConnection as n, disconnectNativeRedis as r, nativeRedisOptions as i, nativeRedisTopology as a } from "./nativeRedisTopology.js";
|
|
4
|
+
import { Effect as o } from "effect";
|
|
5
|
+
// src/connectionSubscriber.ts
|
|
6
|
+
var s = async (e, o) => {
|
|
7
|
+
o.throwIfAborted();
|
|
8
|
+
let s = await n(e, "subscriber"), c, l = () => Promise.all([r(s), c === void 0 ? Promise.resolve() : r({
|
|
8
9
|
kind: "standalone",
|
|
9
|
-
client:
|
|
10
|
-
})]).then(() => void 0),
|
|
11
|
-
|
|
10
|
+
client: c
|
|
11
|
+
})]).then(() => void 0), u = () => {
|
|
12
|
+
l();
|
|
12
13
|
};
|
|
13
|
-
|
|
14
|
+
o.addEventListener("abort", u, { once: !0 }), s.client.on("error", () => {});
|
|
14
15
|
try {
|
|
15
|
-
if (
|
|
16
|
-
client:
|
|
17
|
-
dispose:
|
|
16
|
+
if (o.throwIfAborted(), await s.client.connect(), await s.ready(), o.throwIfAborted(), s.kind !== "cluster") return {
|
|
17
|
+
client: s.client,
|
|
18
|
+
dispose: l
|
|
18
19
|
};
|
|
19
|
-
let
|
|
20
|
-
if (
|
|
21
|
-
let
|
|
22
|
-
if (
|
|
23
|
-
let
|
|
24
|
-
if (
|
|
25
|
-
let { default:
|
|
26
|
-
return
|
|
27
|
-
...
|
|
28
|
-
...
|
|
29
|
-
host:
|
|
30
|
-
port:
|
|
31
|
-
}),
|
|
32
|
-
client:
|
|
33
|
-
dispose:
|
|
20
|
+
let n = s.client.nodes("master").find((e) => e.status === "ready");
|
|
21
|
+
if (n === void 0) throw Error("No ready Cluster primary");
|
|
22
|
+
let u = n.options.host, d = n.options.port;
|
|
23
|
+
if (u === void 0 || d === void 0) throw Error("Cluster primary endpoint missing");
|
|
24
|
+
let f = a(e);
|
|
25
|
+
if (f.kind !== "cluster") throw Error("Expected Cluster topology");
|
|
26
|
+
let { default: p } = await import("ioredis").catch(t("ioredis", "A Redis subscriber connection"));
|
|
27
|
+
return o.throwIfAborted(), c = new p({
|
|
28
|
+
...f.options,
|
|
29
|
+
...i("subscriber"),
|
|
30
|
+
host: u,
|
|
31
|
+
port: d
|
|
32
|
+
}), c.on("error", () => {}), await c.connect(), o.throwIfAborted(), await r(s), o.throwIfAborted(), {
|
|
33
|
+
client: c,
|
|
34
|
+
dispose: l
|
|
34
35
|
};
|
|
35
36
|
} catch (e) {
|
|
36
|
-
throw await
|
|
37
|
+
throw await l(), e;
|
|
37
38
|
} finally {
|
|
38
|
-
|
|
39
|
+
o.removeEventListener("abort", u);
|
|
39
40
|
}
|
|
40
|
-
},
|
|
41
|
+
}, c = (t) => o.tryPromise({
|
|
41
42
|
try: async (e) => {
|
|
42
43
|
if (t.driver !== "resp") throw Error("RESP is required for subscriptions");
|
|
43
|
-
let n = await
|
|
44
|
+
let n = await s(t, e);
|
|
44
45
|
e.aborted && (await n.dispose(), e.throwIfAborted());
|
|
45
|
-
let r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Set(),
|
|
46
|
+
let r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Set(), o = n, c = !1, l, u, d, f = Promise.resolve(), p = Promise.resolve(), m = (e) => {
|
|
46
47
|
let t = p.then(e);
|
|
47
48
|
return p = t.then(() => void 0, () => void 0), t;
|
|
48
49
|
}, h = (e) => {
|
|
@@ -54,12 +55,12 @@ var o = async (e, a) => {
|
|
|
54
55
|
return f = Promise.all([f, t]).then(() => void 0), t;
|
|
55
56
|
}, _ = (e) => {
|
|
56
57
|
e.client.on("message", (t, n) => {
|
|
57
|
-
if (!(c ||
|
|
58
|
+
if (!(c || o !== e)) for (let e of r) try {
|
|
58
59
|
e(t, n);
|
|
59
60
|
} catch {}
|
|
60
61
|
});
|
|
61
62
|
let t = () => {
|
|
62
|
-
c ||
|
|
63
|
+
c || o !== e || (o = void 0, h({ kind: "disconnected" }), g(e), y());
|
|
63
64
|
};
|
|
64
65
|
e.client.on("close", t), e.client.on("reconnecting", t);
|
|
65
66
|
}, v = async () => {
|
|
@@ -70,13 +71,13 @@ var o = async (e, a) => {
|
|
|
70
71
|
};
|
|
71
72
|
e.signal.addEventListener("abort", r, { once: !0 });
|
|
72
73
|
try {
|
|
73
|
-
n = await
|
|
74
|
+
n = await s(t, e.signal);
|
|
74
75
|
let r = n;
|
|
75
76
|
await m(async () => {
|
|
76
77
|
if (e.signal.throwIfAborted(), c) throw Error("Subscriber closed");
|
|
77
78
|
for (let e of a) await r.client.subscribe(e);
|
|
78
79
|
if (e.signal.throwIfAborted(), r.client.status !== "ready") throw Error("Subscriber lost during restoration");
|
|
79
|
-
_(r),
|
|
80
|
+
_(r), o = r;
|
|
80
81
|
}), n = void 0, h({ kind: "reconnected" });
|
|
81
82
|
} catch {
|
|
82
83
|
n !== void 0 && await g(n);
|
|
@@ -86,23 +87,23 @@ var o = async (e, a) => {
|
|
|
86
87
|
}, y = () => {
|
|
87
88
|
c || l !== void 0 || d !== void 0 || (l = setTimeout(() => {
|
|
88
89
|
l = void 0, d = v().finally(() => {
|
|
89
|
-
d = void 0, !c &&
|
|
90
|
+
d = void 0, !c && o === void 0 && y();
|
|
90
91
|
});
|
|
91
92
|
}, 100));
|
|
92
93
|
};
|
|
93
|
-
if (_(n), n.client.status !== "ready") throw
|
|
94
|
+
if (_(n), n.client.status !== "ready") throw o = void 0, await g(n), Error("Subscriber lost before admission");
|
|
94
95
|
let b = () => {
|
|
95
|
-
if (c ||
|
|
96
|
-
return
|
|
96
|
+
if (c || o === void 0 || o.client.status !== "ready") throw Error("Subscriber unavailable");
|
|
97
|
+
return o;
|
|
97
98
|
};
|
|
98
99
|
return {
|
|
99
100
|
subscribe: (e) => m(async () => {
|
|
100
101
|
let t = b();
|
|
101
|
-
if (await t.client.subscribe(e), c ||
|
|
102
|
+
if (await t.client.subscribe(e), c || o !== t) throw Error("Subscriber lost during registration");
|
|
102
103
|
a.add(e);
|
|
103
104
|
}),
|
|
104
105
|
unsubscribe: (e) => m(async () => {
|
|
105
|
-
a.delete(e), !c &&
|
|
106
|
+
a.delete(e), !c && o !== void 0 && await o.client.unsubscribe(e);
|
|
106
107
|
}),
|
|
107
108
|
onMessage: (e) => (r.add(e), () => {
|
|
108
109
|
r.delete(e);
|
|
@@ -113,8 +114,8 @@ var o = async (e, a) => {
|
|
|
113
114
|
close: async () => {
|
|
114
115
|
if (!c) {
|
|
115
116
|
c = !0, l !== void 0 && clearTimeout(l), u?.abort();
|
|
116
|
-
let e =
|
|
117
|
-
|
|
117
|
+
let e = o;
|
|
118
|
+
o = void 0, e !== void 0 && g(e), a.clear(), r.clear(), i.clear();
|
|
118
119
|
}
|
|
119
120
|
await d, await p, await f;
|
|
120
121
|
}
|
|
@@ -126,5 +127,4 @@ var o = async (e, a) => {
|
|
|
126
127
|
cause: "Redis subscriber could not be opened"
|
|
127
128
|
})
|
|
128
129
|
});
|
|
129
|
-
|
|
130
|
-
export { s as connectRedisSubscriber };
|
|
130
|
+
export { c as connectRedisSubscriber };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/driverModule.ts
|
|
2
|
+
var e = (e, t) => {
|
|
3
|
+
if (typeof e != "object" || !e) return !1;
|
|
4
|
+
let { code: n, message: r } = e;
|
|
5
|
+
return (n === "ERR_MODULE_NOT_FOUND" || n === "MODULE_NOT_FOUND") && typeof r == "string" && r.includes(t);
|
|
6
|
+
}, t = (t, n) => (r) => {
|
|
7
|
+
throw e(r, t) ? Error(`${n} needs the "${t}" package, which is not installed. Add "${t}" to this app's dependencies; apps that depend on @voltro/runtime have it already.`, { cause: r }) : r;
|
|
8
|
+
};
|
|
9
|
+
export { t as missingDriver };
|
package/dist/errors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema as e } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/errors.ts
|
|
3
3
|
var t = class extends e.TaggedError()("KvError", {
|
|
4
4
|
operation: e.String,
|
|
5
5
|
key: e.String,
|
|
@@ -9,5 +9,4 @@ var t = class extends e.TaggedError()("KvError", {
|
|
|
9
9
|
return this.cause;
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
-
//#endregion
|
|
13
12
|
export { t as KvError };
|
package/dist/expirySweep.js
CHANGED
package/dist/hot-contract.js
CHANGED
|
@@ -3,7 +3,7 @@ import { hotStoreFromRunner as t, validateHotOp as n } from "./hotOperations.js"
|
|
|
3
3
|
import { applyHotOperations as r, compareHotMembers as i } from "./hotState.js";
|
|
4
4
|
import { readHotStoreWithFallback as a } from "./hotReadAvailability.js";
|
|
5
5
|
import { Context as o, Schema as s } from "effect";
|
|
6
|
-
|
|
6
|
+
// src/HotStore.ts
|
|
7
7
|
var c = class extends s.TaggedError()("HotError", {
|
|
8
8
|
operation: s.String,
|
|
9
9
|
key: s.String,
|
|
@@ -14,5 +14,4 @@ var c = class extends s.TaggedError()("HotError", {
|
|
|
14
14
|
"unavailable"
|
|
15
15
|
])
|
|
16
16
|
}) {}, l = class extends o.Service()("@voltro/HotStore") {};
|
|
17
|
-
//#endregion
|
|
18
17
|
export { c as HotError, l as HotStore, r as applyHotOperations, i as compareHotMembers, e as consumeHotRateLimit, t as hotStoreFromRunner, a as readHotStoreWithFallback, n as validateHotOp };
|
package/dist/hot-testing.d.ts
CHANGED
|
@@ -23,6 +23,19 @@ export declare interface HotConformanceFixture {
|
|
|
23
23
|
readonly capabilities: HotStoreShape['capabilities'];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Runner operations used by the suite; supplied by the test-only caller. */
|
|
27
|
+
export declare interface HotConformanceTestApi {
|
|
28
|
+
readonly describe: (name: string, body: () => void) => unknown;
|
|
29
|
+
readonly it: (name: string, body: () => Promise<void>) => unknown;
|
|
30
|
+
readonly expect: (actual: unknown) => {
|
|
31
|
+
readonly toEqual: (expected: unknown) => unknown;
|
|
32
|
+
readonly toBe: (expected: unknown) => unknown;
|
|
33
|
+
readonly toHaveLength: (expected: number) => unknown;
|
|
34
|
+
readonly toBeGreaterThan: (expected: number) => unknown;
|
|
35
|
+
readonly toBeLessThanOrEqual: (expected: number) => unknown;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
declare interface HotCounterOptions extends HotTtl {
|
|
27
40
|
readonly max?: number;
|
|
28
41
|
}
|
|
@@ -178,7 +191,7 @@ declare class HotStore extends HotStore_base {
|
|
|
178
191
|
declare const HotStore_base: Context.ServiceClass<HotStore, "@voltro/HotStore", HotStoreShape>;
|
|
179
192
|
|
|
180
193
|
/** Run exactly the same behavioral contract against a backend or a replacement port. */
|
|
181
|
-
export declare const hotStoreConformance: (name: string, fixture: () => HotConformanceFixture) => void;
|
|
194
|
+
export declare const hotStoreConformance: (name: string, fixture: () => HotConformanceFixture, tests: HotConformanceTestApi) => void;
|
|
182
195
|
|
|
183
196
|
declare interface HotStoreShape {
|
|
184
197
|
readonly zadd: (key: string, members: ReadonlyArray<HotMember>, options?: HotZaddOptions) => Effect.Effect<void, HotError>;
|
package/dist/hotMemory.js
CHANGED
|
@@ -2,7 +2,7 @@ import { hotStoreFromRunner as e, validateHotOp as t } from "./hotOperations.js"
|
|
|
2
2
|
import { applyHotOperations as n, compareHotMembers as r } from "./hotState.js";
|
|
3
3
|
import { HotError as i, HotStore as a } from "./hot-contract.js";
|
|
4
4
|
import { Clock as o, Effect as s, Layer as c, Stream as l } from "effect";
|
|
5
|
-
|
|
5
|
+
// src/hotMemory.ts
|
|
6
6
|
var u = { sweepBatchSize: 32 }, d = (a = {}) => s.try({
|
|
7
7
|
try: () => {
|
|
8
8
|
let c = /* @__PURE__ */ new Map(), d = a.sweepBatchSize ?? u.sweepBatchSize;
|
|
@@ -42,5 +42,4 @@ var u = { sweepBatchSize: 32 }, d = (a = {}) => s.try({
|
|
|
42
42
|
reason: "invalid"
|
|
43
43
|
})
|
|
44
44
|
}), f = (e = {}) => c.effect(a, d(e));
|
|
45
|
-
//#endregion
|
|
46
45
|
export { u as HOT_MEMORY_DEFAULTS, f as layerHotMemory, d as makeMemoryHotStore };
|
package/dist/hotOperations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HotError as e } from "./hot-contract.js";
|
|
2
2
|
import { Effect as t, Option as n } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/hotOperations.ts
|
|
4
4
|
var r = /* @__PURE__ */ new Set([
|
|
5
5
|
"zrank",
|
|
6
6
|
"zscore",
|
|
@@ -219,5 +219,4 @@ var r = /* @__PURE__ */ new Set([
|
|
|
219
219
|
capabilities: o
|
|
220
220
|
};
|
|
221
221
|
};
|
|
222
|
-
//#endregion
|
|
223
222
|
export { s as hotStoreFromRunner, i as isHotReadOperation, o as validateHotOp, a as validateHotReadOptions };
|
package/dist/hotRateLimit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/hotRateLimit.ts
|
|
2
2
|
var e = (e, i, a) => {
|
|
3
3
|
switch (i.algorithm) {
|
|
4
4
|
case "fixed-window": return t(e, i, a);
|
|
@@ -99,5 +99,4 @@ var e = (e, i, a) => {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
};
|
|
102
|
-
//#endregion
|
|
103
102
|
export { e as consumeHotRateLimit };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HotError as e } from "./hot-contract.js";
|
|
2
2
|
import { Effect as t } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/hotReadAvailability.ts
|
|
4
4
|
var n = (n) => t.suspend(() => n.onUnavailable !== "degrade" && n.onUnavailable !== "fail" ? t.fail(new e({
|
|
5
5
|
operation: "readFallback",
|
|
6
6
|
key: "",
|
|
@@ -15,5 +15,4 @@ var n = (n) => t.suspend(() => n.onUnavailable !== "degrade" && n.onUnavailable
|
|
|
15
15
|
smembers: (...e) => n.store.smembers(...e),
|
|
16
16
|
sismember: (...e) => n.store.sismember(...e)
|
|
17
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
18
|
export { n as readHotStoreWithFallback };
|
package/dist/hotRedis.js
CHANGED
|
@@ -3,7 +3,7 @@ import { hotStoreFromRunner as t, isHotReadOperation as n, validateHotOp as r, v
|
|
|
3
3
|
import { HotError as a, HotStore as o } from "./hot-contract.js";
|
|
4
4
|
import { HOT_SCRIPT as s } from "./hotRedisScript.js";
|
|
5
5
|
import { Effect as c, Layer as l, Option as u, Stream as d } from "effect";
|
|
6
|
-
|
|
6
|
+
// src/hotRedis.ts
|
|
7
7
|
var f = {
|
|
8
8
|
connectionName: "hot",
|
|
9
9
|
namespace: "voltro:hot",
|
|
@@ -135,5 +135,4 @@ var f = {
|
|
|
135
135
|
})
|
|
136
136
|
});
|
|
137
137
|
}));
|
|
138
|
-
//#endregion
|
|
139
138
|
export { f as HOT_REDIS_DEFAULTS, h as layerHotRedis, m as makeRedisHotStore };
|
package/dist/hotRedisScript.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
// src/hotRedisScript.ts
|
|
2
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
3
|
export { e as HOT_SCRIPT };
|
package/dist/hotRouting.js
CHANGED
|
@@ -3,7 +3,7 @@ import { hotStoreFromRunner as t, validateHotOp as n, validateHotReadOptions as
|
|
|
3
3
|
import { HotError as i, HotStore as a } from "./hot-contract.js";
|
|
4
4
|
import { makeRedisHotStore as o } from "./hotRedis.js";
|
|
5
5
|
import { Effect as s, Layer as c, Stream as l } from "effect";
|
|
6
|
-
|
|
6
|
+
// src/hotRouting.ts
|
|
7
7
|
var u = (u) => c.effect(a, s.gen(function* () {
|
|
8
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
9
|
operation: e,
|
|
@@ -14,7 +14,7 @@ var u = (u) => c.effect(a, s.gen(function* () {
|
|
|
14
14
|
let v = !0;
|
|
15
15
|
for (let e of m) {
|
|
16
16
|
if (e !== "" && !/^redis:[A-Za-z][A-Za-z0-9_-]*$/.test(e)) return yield* s.fail(_("route"));
|
|
17
|
-
(
|
|
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
18
|
}
|
|
19
19
|
if (h !== void 0 && (!h || /[{}]/.test(h)) || g !== void 0 && (!Number.isSafeInteger(g) || g < 1)) return yield* s.fail(_("route"));
|
|
20
20
|
let y = (e) => s.try({
|
|
@@ -74,5 +74,4 @@ var u = (u) => c.effect(a, s.gen(function* () {
|
|
|
74
74
|
persistent: !1
|
|
75
75
|
});
|
|
76
76
|
}));
|
|
77
|
-
//#endregion
|
|
78
77
|
export { u as layerRoutedHotRedis };
|
package/dist/hotState.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { consumeHotRateLimit as e } from "./hotRateLimit.js";
|
|
2
2
|
import { validateHotOp as t } from "./hotOperations.js";
|
|
3
3
|
import { HotError as n } from "./hot-contract.js";
|
|
4
|
-
|
|
4
|
+
// src/hotState.ts
|
|
5
5
|
var r = (e, t) => {
|
|
6
6
|
let n = Array.from(e, (e) => e.codePointAt(0) ?? 0), r = Array.from(t, (e) => e.codePointAt(0) ?? 0);
|
|
7
7
|
for (let e = 0; e < Math.min(n.length, r.length); e++) {
|
|
@@ -178,5 +178,4 @@ var r = (e, t) => {
|
|
|
178
178
|
}
|
|
179
179
|
return f;
|
|
180
180
|
};
|
|
181
|
-
//#endregion
|
|
182
181
|
export { s as applyHotOperations, r as compareHotMembers };
|