@voltro/kv 0.84.0 → 0.86.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 +1215 -0
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/Kv.js +19 -0
- package/dist/KvStore.js +5 -0
- package/dist/backendMemory.js +36 -0
- package/dist/backendRedis.js +48 -0
- package/dist/connection.d.ts +12 -21
- package/dist/connection.js +81 -2
- package/dist/errors.js +9 -0
- package/dist/expirySweep.js +26 -0
- package/dist/index.d.ts +28 -25
- package/dist/index.js +7 -83
- package/package.json +2 -2
- package/dist/connection-DtnqPJzH.js +0 -84
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Third-Party Notices — @voltro/kv
|
|
2
2
|
|
|
3
|
-
This package
|
|
3
|
+
This package uses the open-source software listed below. Each is the
|
|
4
4
|
property of its respective copyright holders and is used under the terms of
|
|
5
5
|
its license. This file is provided for attribution; it grants no rights in
|
|
6
6
|
@voltro/kv itself, which is proprietary (see LICENSE).
|
package/dist/Kv.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { KvStore as e } from "./KvStore.js";
|
|
2
|
+
import { Context as t, Effect as n, Layer as r, Option as i } from "effect";
|
|
3
|
+
//#region src/Kv.ts
|
|
4
|
+
var a = class a extends t.Service()("@voltro/Kv", { make: n.gen(function* () {
|
|
5
|
+
let t = yield* e;
|
|
6
|
+
return {
|
|
7
|
+
get: (e) => t.get(e),
|
|
8
|
+
getOrElse: (e, r) => t.get(e).pipe(n.map((e) => i.isSome(e) ? e.value : r())),
|
|
9
|
+
set: (e, n, r) => t.set(e, n, r),
|
|
10
|
+
delete: t.delete,
|
|
11
|
+
has: t.has,
|
|
12
|
+
list: t.list,
|
|
13
|
+
clear: t.clear
|
|
14
|
+
};
|
|
15
|
+
}) }) {
|
|
16
|
+
static layer = r.effect(a, a.make);
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { a as Kv };
|
package/dist/KvStore.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { KvStore as e } from "./KvStore.js";
|
|
2
|
+
import { makeExpirySweep as t } from "./expirySweep.js";
|
|
3
|
+
import { Clock as n, Effect as r, HashMap as i, Layer as a, Option as o, Ref as s } from "effect";
|
|
4
|
+
//#region src/backendMemory.ts
|
|
5
|
+
var c = (e, t) => t === void 0 ? Infinity : e + t, l = (e) => r.gen(function* () {
|
|
6
|
+
let a = yield* s.make(i.empty()), l = e.now === void 0 ? n.currentTimeMillis : r.sync(e.now), u = t(), d = u.sweep;
|
|
7
|
+
return {
|
|
8
|
+
get: (e) => l.pipe(r.flatMap((t) => s.modify(a, (n) => {
|
|
9
|
+
n = d(n, t);
|
|
10
|
+
let r = i.get(n, e);
|
|
11
|
+
return o.isNone(r) ? [o.none(), n] : r.value.expiresAt <= t ? [o.none(), i.remove(n, e)] : [o.some(r.value.value), n];
|
|
12
|
+
}))),
|
|
13
|
+
set: (e, t, n) => l.pipe(r.flatMap((r) => s.update(a, (a) => i.set(d(a, r), e, {
|
|
14
|
+
value: t,
|
|
15
|
+
expiresAt: c(r, n?.ttlMs)
|
|
16
|
+
})))),
|
|
17
|
+
delete: (e) => l.pipe(r.flatMap((t) => s.modify(a, (n) => {
|
|
18
|
+
n = d(n, t);
|
|
19
|
+
let r = i.get(n, e);
|
|
20
|
+
return o.isNone(r) ? [!1, n] : [r.value.expiresAt > t, i.remove(n, e)];
|
|
21
|
+
}))),
|
|
22
|
+
has: (e) => l.pipe(r.flatMap((t) => s.modify(a, (n) => {
|
|
23
|
+
n = d(n, t);
|
|
24
|
+
let r = i.get(n, e);
|
|
25
|
+
return o.isNone(r) ? [!1, n] : r.value.expiresAt <= t ? [!1, i.remove(n, e)] : [!0, n];
|
|
26
|
+
}))),
|
|
27
|
+
list: (e) => l.pipe(r.flatMap((t) => s.modify(a, (n) => {
|
|
28
|
+
let r = [], a = n;
|
|
29
|
+
for (let [o, s] of n) s.expiresAt <= t ? a = i.remove(a, o) : (e === "" || o.startsWith(e)) && r.push(o);
|
|
30
|
+
return u.reset(), [r, a];
|
|
31
|
+
}))),
|
|
32
|
+
clear: () => r.sync(u.reset).pipe(r.andThen(s.set(a, i.empty())))
|
|
33
|
+
};
|
|
34
|
+
}), u = (t = {}) => a.effect(e, l(t));
|
|
35
|
+
//#endregion
|
|
36
|
+
export { u as layerMemory };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { KvStore as e } from "./KvStore.js";
|
|
2
|
+
import { KvError as t } from "./errors.js";
|
|
3
|
+
import { RedisConnections as n, commandRetry as r } from "./connection.js";
|
|
4
|
+
import { Effect as i, Layer as a, Option as o } from "effect";
|
|
5
|
+
//#region src/backendRedis.ts
|
|
6
|
+
var s = (e, n = "voltro:kv") => {
|
|
7
|
+
let a = (e) => `${n}:${e}`, s = (e) => e.slice(n.length + 1), c = (n, a, o) => i.tryPromise({
|
|
8
|
+
try: () => o(e),
|
|
9
|
+
catch: (e) => new t({
|
|
10
|
+
operation: n,
|
|
11
|
+
key: a,
|
|
12
|
+
cause: String(e)
|
|
13
|
+
})
|
|
14
|
+
}).pipe(i.retry(r)), l = (e, n) => i.try({
|
|
15
|
+
try: () => JSON.stringify(n),
|
|
16
|
+
catch: (n) => new t({
|
|
17
|
+
operation: "serialize",
|
|
18
|
+
key: e,
|
|
19
|
+
cause: String(n)
|
|
20
|
+
})
|
|
21
|
+
}), u = (e, n) => i.try({
|
|
22
|
+
try: () => o.some(JSON.parse(n)),
|
|
23
|
+
catch: (n) => new t({
|
|
24
|
+
operation: "deserialize",
|
|
25
|
+
key: e,
|
|
26
|
+
cause: String(n)
|
|
27
|
+
})
|
|
28
|
+
}), d = (e, t, n) => c("list", "", (n) => n.scan(t, e, 200)).pipe(i.flatMap(({ cursor: t, keys: r }) => {
|
|
29
|
+
for (let e of r) n.push(s(e));
|
|
30
|
+
return t === "0" ? i.succeed(n) : d(e, t, n);
|
|
31
|
+
})), f = (e) => c("clear", "", (t) => t.scan(e, `${n}:*`, 200)).pipe(i.flatMap(({ cursor: e, keys: t }) => (t.length > 0 ? c("clear", "", (e) => e.del(t)) : i.succeed(0)).pipe(i.flatMap(() => e === "0" ? i.void : f(e)))));
|
|
32
|
+
return {
|
|
33
|
+
get: (e) => c("get", e, (t) => t.get(a(e))).pipe(i.flatMap((t) => t === null ? i.succeed(o.none()) : u(e, t))),
|
|
34
|
+
set: (e, t, n) => l(e, t).pipe(i.flatMap((t) => n?.ttlMs !== void 0 && n.ttlMs > 0 ? c("set", e, (r) => r.psetex(a(e), Math.ceil(n.ttlMs), t)) : c("set", e, (n) => n.set(a(e), t))), i.asVoid),
|
|
35
|
+
delete: (e) => c("delete", e, (t) => t.del([a(e)])).pipe(i.map((e) => e > 0)),
|
|
36
|
+
has: (e) => c("has", e, (t) => t.exists(a(e))).pipe(i.map((e) => e > 0)),
|
|
37
|
+
list: (e) => d(`${n}:${e}*`, "0", []),
|
|
38
|
+
clear: () => f("0")
|
|
39
|
+
};
|
|
40
|
+
}, c = (r = {}) => a.effect(e, i.gen(function* () {
|
|
41
|
+
return s(yield* (yield* n).client(r.connectionName ?? "kv").pipe(i.catch((e) => e instanceof t ? i.fail(e) : i.fail(new t({
|
|
42
|
+
operation: "connect",
|
|
43
|
+
key: "",
|
|
44
|
+
cause: String(e)
|
|
45
|
+
})))), r.keyPrefix ?? "voltro:kv");
|
|
46
|
+
}));
|
|
47
|
+
//#endregion
|
|
48
|
+
export { c as layerRedis, s as makeRedisKvStore };
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from 'effect';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
3
|
import { Duration } from 'effect/Duration';
|
|
4
4
|
import { Effect } from 'effect';
|
|
5
5
|
import { Layer } from 'effect';
|
|
6
6
|
import { Schedule } from 'effect';
|
|
7
7
|
import { Schema } from 'effect';
|
|
8
|
+
import { YieldableError } from 'effect/Cause';
|
|
8
9
|
|
|
9
10
|
/** Bounded retry for transient command failures (network blip). Connect is
|
|
10
11
|
* NOT retried — a bad url should fail loud at boot. Re-exported so consumers
|
|
11
12
|
* wrap their own command runners consistently. */
|
|
12
|
-
export declare const commandRetry: Schedule.Schedule<
|
|
13
|
+
export declare const commandRetry: Schedule.Schedule< Duration, unknown, never, never>;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Build a RESP command client from resolved config. The driver decides the
|
|
@@ -19,30 +20,20 @@ export declare const commandRetry: Schedule.Schedule<[ Duration, number], unknow
|
|
|
19
20
|
*/
|
|
20
21
|
export declare const connect: (config: RedisConnectionConfig) => Effect.Effect<RespClient, KvError>;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
* Resolve a named connection's config from the environment. Reads the
|
|
24
|
-
* per-name override first, then the shared default — e.g. `client('cache')`
|
|
25
|
-
* reads `CACHE_REDIS_URL`, falling back to `REDIS_URL`. Driver / token /
|
|
26
|
-
* key-prefix follow the same `<NAME>_REDIS_*` → `REDIS_*` pattern. A bad
|
|
27
|
-
* driver literal or a fully-missing url is a `ConfigError` (fail-loud at
|
|
28
|
-
* boot), matching the cache layer's stance.
|
|
29
|
-
*/
|
|
30
|
-
export declare const connectionConfig: (name: string) => Effect.Effect<RedisConnectionConfig, ConfigError.ConfigError>;
|
|
23
|
+
export declare const connectionConfig: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
|
|
31
24
|
|
|
32
25
|
declare class KvError extends KvError_base {
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
declare const KvError_base: Schema.
|
|
36
|
-
readonly _tag: Schema.tag<"KvError">;
|
|
37
|
-
} & {
|
|
28
|
+
declare const KvError_base: Schema.Class<KvError, Schema.TaggedStruct<"KvError", {
|
|
38
29
|
/** Which op failed: 'get' | 'set' | 'delete' | 'has' | 'clear' |
|
|
39
30
|
* 'list' | 'connect' | 'serialize' | 'deserialize'. */
|
|
40
|
-
operation:
|
|
31
|
+
readonly operation: Schema.String;
|
|
41
32
|
/** The key (or connection name) involved; '' where not applicable. */
|
|
42
|
-
key:
|
|
33
|
+
readonly key: Schema.String;
|
|
43
34
|
/** `String(originalError)` — round-trip-safe diagnostic form. */
|
|
44
|
-
cause:
|
|
45
|
-
}>;
|
|
35
|
+
readonly cause: Schema.String;
|
|
36
|
+
}>, YieldableError>;
|
|
46
37
|
|
|
47
38
|
/**
|
|
48
39
|
* `Layer<RedisConnections>` — the pooled registry. Each named connection is
|
|
@@ -66,7 +57,7 @@ export declare interface RedisConnectionConfig {
|
|
|
66
57
|
export declare class RedisConnections extends RedisConnections_base {
|
|
67
58
|
}
|
|
68
59
|
|
|
69
|
-
declare const RedisConnections_base: Context.
|
|
60
|
+
declare const RedisConnections_base: Context.ServiceClass<RedisConnections, "@voltro/RedisConnections", RespConnections>;
|
|
70
61
|
|
|
71
62
|
/** The minimal command surface both ioredis and @upstash/redis satisfy —
|
|
72
63
|
* the superset the framework's command-only RESP consumers (cache, rate
|
|
@@ -91,10 +82,10 @@ export declare interface RespConnections {
|
|
|
91
82
|
/** Get-or-create the pooled RESP command client for a named connection.
|
|
92
83
|
* One socket per name for the registry's lifetime — concurrent first-callers
|
|
93
84
|
* collapse to a single connect. */
|
|
94
|
-
readonly client: (name: string) => Effect.Effect<RespClient, KvError |
|
|
85
|
+
readonly client: (name: string) => Effect.Effect<RespClient, KvError | Config.ConfigError>;
|
|
95
86
|
/** Resolve a named connection's config WITHOUT opening it — for consumers
|
|
96
87
|
* that build their own client (e.g. a dedicated pub/sub connection). */
|
|
97
|
-
readonly config: (name: string) => Effect.Effect<RedisConnectionConfig,
|
|
88
|
+
readonly config: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
|
|
98
89
|
}
|
|
99
90
|
|
|
100
91
|
export { }
|
package/dist/connection.js
CHANGED
|
@@ -1,2 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { KvError as e } from "./errors.js";
|
|
2
|
+
import { Config as t, Context as n, Effect as r, HashMap as i, Layer as a, Option as o, Schedule as s, Scope as c, SynchronizedRef as l } from "effect";
|
|
3
|
+
//#region src/connection.ts
|
|
4
|
+
var u = (e) => ({
|
|
5
|
+
get: (t) => e.get(t),
|
|
6
|
+
set: (t, n) => e.set(t, n),
|
|
7
|
+
psetex: (t, n, r) => e.psetex(t, n, r),
|
|
8
|
+
del: (t) => t.length === 0 ? Promise.resolve(0) : e.del(...t),
|
|
9
|
+
exists: (t) => e.exists(t),
|
|
10
|
+
eval: (t, n, ...r) => e.eval(t, n, ...r),
|
|
11
|
+
zrange: (t) => e.zrange(t, 0, -1),
|
|
12
|
+
scan: async (t, n, r) => {
|
|
13
|
+
let [i, a] = await e.scan(t, "MATCH", n, "COUNT", r);
|
|
14
|
+
return {
|
|
15
|
+
cursor: i,
|
|
16
|
+
keys: a
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
close: () => e.quit().then(() => void 0).catch(() => {
|
|
20
|
+
try {
|
|
21
|
+
e.disconnect();
|
|
22
|
+
} catch {}
|
|
23
|
+
})
|
|
24
|
+
}), d = (e) => ({
|
|
25
|
+
get: (t) => e.get(t),
|
|
26
|
+
set: (t, n) => e.set(t, n),
|
|
27
|
+
psetex: (t, n, r) => e.psetex(t, n, r),
|
|
28
|
+
del: (t) => t.length === 0 ? Promise.resolve(0) : e.del(...t),
|
|
29
|
+
exists: (t) => e.exists(t),
|
|
30
|
+
eval: (t, n, ...r) => e.eval(t, r.slice(0, n), r.slice(n)),
|
|
31
|
+
zrange: (t) => e.zrange(t, 0, -1),
|
|
32
|
+
scan: async (t, n, r) => {
|
|
33
|
+
let [i, a] = await e.scan(t, {
|
|
34
|
+
match: n,
|
|
35
|
+
count: r
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
cursor: String(i),
|
|
39
|
+
keys: a
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
close: () => Promise.resolve()
|
|
43
|
+
}), f = (e) => e.toUpperCase().replace(/[^A-Z0-9]+/g, "_"), p = ["resp", "http"], m = (e) => r.gen(function* () {
|
|
44
|
+
let n = f(e), r = yield* t.String(`${n}_REDIS_URL`).pipe(t.orElse(() => t.String("REDIS_URL"))), i = yield* t.Literals(p, `${n}_REDIS_DRIVER`).pipe(t.option), a = o.isSome(i) ? i.value : yield* t.Literals(p, "REDIS_DRIVER").pipe(t.withDefault("resp")), s = yield* t.String(`${n}_REDIS_TOKEN`).pipe(t.orElse(() => t.String("REDIS_TOKEN")), t.orElse(() => t.String("UPSTASH_REDIS_REST_TOKEN")), t.option), c = yield* t.String(`${n}_REDIS_KEY_PREFIX`).pipe(t.option);
|
|
45
|
+
return {
|
|
46
|
+
url: r,
|
|
47
|
+
driver: a,
|
|
48
|
+
...o.isSome(s) ? { token: s.value } : {},
|
|
49
|
+
...o.isSome(c) ? { keyPrefix: c.value } : {}
|
|
50
|
+
};
|
|
51
|
+
}), h = (t) => r.tryPromise({
|
|
52
|
+
try: async () => {
|
|
53
|
+
if (t.driver === "http") {
|
|
54
|
+
let e = await import("@upstash/redis"), n = typeof e.default == "function" ? e.default : e.default?.Redis, r = e.Redis ?? n;
|
|
55
|
+
if (!r) throw Error("@upstash/redis: Redis constructor not found in module");
|
|
56
|
+
return d(new r({
|
|
57
|
+
url: t.url,
|
|
58
|
+
token: t.token,
|
|
59
|
+
automaticDeserialization: !1
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
let e = (await import("ioredis")).default, n = new e(t.url);
|
|
63
|
+
return n.on("error", () => {}), u(n);
|
|
64
|
+
},
|
|
65
|
+
catch: (t) => new e({
|
|
66
|
+
operation: "connect",
|
|
67
|
+
key: "",
|
|
68
|
+
cause: String(t)
|
|
69
|
+
})
|
|
70
|
+
}), g = s.max([s.exponential("10 millis"), s.recurs(2)]), _ = class extends n.Service()("@voltro/RedisConnections") {}, v = a.effect(_, r.gen(function* () {
|
|
71
|
+
let e = yield* r.scope, t = yield* l.make(i.empty()), n = (e) => m(e);
|
|
72
|
+
return {
|
|
73
|
+
client: (a) => l.modifyEffect(t, (t) => {
|
|
74
|
+
let s = i.get(t, a);
|
|
75
|
+
return o.isSome(s) ? r.succeed([s.value, t]) : n(a).pipe(r.flatMap(h), r.tap((t) => c.addFinalizer(e, r.promise(() => t.close().catch(() => void 0)))), r.map((e) => [e, i.set(t, a, e)]));
|
|
76
|
+
}),
|
|
77
|
+
config: n
|
|
78
|
+
};
|
|
79
|
+
}));
|
|
80
|
+
//#endregion
|
|
81
|
+
export { _ as RedisConnections, g as commandRetry, h as connect, m as connectionConfig, v as layerConnections };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HashMap as e, Option as t } from "effect";
|
|
2
|
+
//#region src/expirySweep.ts
|
|
3
|
+
var n = () => {
|
|
4
|
+
let n;
|
|
5
|
+
return {
|
|
6
|
+
reset: () => {
|
|
7
|
+
n = void 0;
|
|
8
|
+
},
|
|
9
|
+
sweep: (r, i) => {
|
|
10
|
+
n ??= e.keys(r)[Symbol.iterator]();
|
|
11
|
+
let a = r;
|
|
12
|
+
for (let r = 0; r < 16; r++) {
|
|
13
|
+
let r = n.next();
|
|
14
|
+
if (r.done) {
|
|
15
|
+
n = void 0;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
let o = e.get(a, r.value);
|
|
19
|
+
t.isSome(o) && o.value.expiresAt <= i && (a = e.remove(a, r.value));
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { n as makeExpirySweep };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from 'effect';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
3
|
import { Duration } from 'effect/Duration';
|
|
4
4
|
import { Effect } from 'effect';
|
|
@@ -6,11 +6,12 @@ import { Layer } from 'effect';
|
|
|
6
6
|
import { Option } from 'effect';
|
|
7
7
|
import { Schedule } from 'effect';
|
|
8
8
|
import { Schema } from 'effect';
|
|
9
|
+
import { YieldableError } from 'effect/Cause';
|
|
9
10
|
|
|
10
11
|
/** Bounded retry for transient command failures (network blip). Connect is
|
|
11
12
|
* NOT retried — a bad url should fail loud at boot. Re-exported so consumers
|
|
12
13
|
* wrap their own command runners consistently. */
|
|
13
|
-
export declare const commandRetry: Schedule.Schedule<
|
|
14
|
+
export declare const commandRetry: Schedule.Schedule< Duration, unknown, never, never>;
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Build a RESP command client from resolved config. The driver decides the
|
|
@@ -20,21 +21,25 @@ export declare const commandRetry: Schedule.Schedule<[ Duration, number], unknow
|
|
|
20
21
|
*/
|
|
21
22
|
export declare const connect: (config: RedisConnectionConfig) => Effect.Effect<RespClient, KvError>;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
* Resolve a named connection's config from the environment. Reads the
|
|
25
|
-
* per-name override first, then the shared default — e.g. `client('cache')`
|
|
26
|
-
* reads `CACHE_REDIS_URL`, falling back to `REDIS_URL`. Driver / token /
|
|
27
|
-
* key-prefix follow the same `<NAME>_REDIS_*` → `REDIS_*` pattern. A bad
|
|
28
|
-
* driver literal or a fully-missing url is a `ConfigError` (fail-loud at
|
|
29
|
-
* boot), matching the cache layer's stance.
|
|
30
|
-
*/
|
|
31
|
-
export declare const connectionConfig: (name: string) => Effect.Effect<RedisConnectionConfig, ConfigError.ConfigError>;
|
|
24
|
+
export declare const connectionConfig: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
|
|
32
25
|
|
|
33
26
|
export declare class Kv extends Kv_base {
|
|
27
|
+
/** `Kv` over whatever `KvStore` is provided. */
|
|
28
|
+
static readonly layer: Layer.Layer<Kv, never, KvStore>;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
declare const Kv_base:
|
|
37
|
-
|
|
31
|
+
declare const Kv_base: Context.ServiceClass<Kv, "@voltro/Kv", {
|
|
32
|
+
/** Read a typed value. `None` on miss/expiry. */
|
|
33
|
+
readonly get: <A = unknown>(key: string) => Effect.Effect<Option.Option<A>, KvError>;
|
|
34
|
+
/** Read a typed value, falling back to `orElse()` (run only on miss). */
|
|
35
|
+
readonly getOrElse: <A>(key: string, orElse: () => A) => Effect.Effect<A, KvError>;
|
|
36
|
+
readonly set: <A>(key: string, value: A, options?: KvSetOptions) => Effect.Effect<void, KvError>;
|
|
37
|
+
readonly delete: (key: string) => Effect.Effect<boolean, KvError>;
|
|
38
|
+
readonly has: (key: string) => Effect.Effect<boolean, KvError>;
|
|
39
|
+
readonly list: (prefix: string) => Effect.Effect<ReadonlyArray<string>, KvError>;
|
|
40
|
+
readonly clear: () => Effect.Effect<void, KvError>;
|
|
41
|
+
}> & {
|
|
42
|
+
readonly make: Effect.Effect<{
|
|
38
43
|
/** Read a typed value. `None` on miss/expiry. */
|
|
39
44
|
readonly get: <A = unknown>(key: string) => Effect.Effect<Option.Option<A>, KvError>;
|
|
40
45
|
/** Read a typed value, falling back to `orElse()` (run only on miss). */
|
|
@@ -45,22 +50,20 @@ declare const Kv_base: Effect.Service.Class<Kv, "@voltro/Kv", {
|
|
|
45
50
|
readonly list: (prefix: string) => Effect.Effect<ReadonlyArray<string>, KvError>;
|
|
46
51
|
readonly clear: () => Effect.Effect<void, KvError>;
|
|
47
52
|
}, never, KvStore>;
|
|
48
|
-
}
|
|
53
|
+
};
|
|
49
54
|
|
|
50
55
|
export declare class KvError extends KvError_base {
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
declare const KvError_base: Schema.
|
|
54
|
-
readonly _tag: Schema.tag<"KvError">;
|
|
55
|
-
} & {
|
|
58
|
+
declare const KvError_base: Schema.Class<KvError, Schema.TaggedStruct<"KvError", {
|
|
56
59
|
/** Which op failed: 'get' | 'set' | 'delete' | 'has' | 'clear' |
|
|
57
60
|
* 'list' | 'connect' | 'serialize' | 'deserialize'. */
|
|
58
|
-
operation:
|
|
61
|
+
readonly operation: Schema.String;
|
|
59
62
|
/** The key (or connection name) involved; '' where not applicable. */
|
|
60
|
-
key:
|
|
63
|
+
readonly key: Schema.String;
|
|
61
64
|
/** `String(originalError)` — round-trip-safe diagnostic form. */
|
|
62
|
-
cause:
|
|
63
|
-
}>;
|
|
65
|
+
readonly cause: Schema.String;
|
|
66
|
+
}>, YieldableError>;
|
|
64
67
|
|
|
65
68
|
export declare interface KvSetOptions {
|
|
66
69
|
/** Time-to-live in milliseconds. Omit for no expiry — the entry lives until
|
|
@@ -71,7 +74,7 @@ export declare interface KvSetOptions {
|
|
|
71
74
|
export declare class KvStore extends KvStore_base {
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
declare const KvStore_base: Context.
|
|
77
|
+
declare const KvStore_base: Context.ServiceClass<KvStore, "@voltro/KvStore", KvStoreShape>;
|
|
75
78
|
|
|
76
79
|
export declare interface KvStoreShape {
|
|
77
80
|
/** Read a value. `None` on miss OR expiry (expired entries are lazily
|
|
@@ -135,7 +138,7 @@ export declare interface RedisConnectionConfig {
|
|
|
135
138
|
export declare class RedisConnections extends RedisConnections_base {
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
declare const RedisConnections_base: Context.
|
|
141
|
+
declare const RedisConnections_base: Context.ServiceClass<RedisConnections, "@voltro/RedisConnections", RespConnections>;
|
|
139
142
|
|
|
140
143
|
export declare interface RedisKvOptions {
|
|
141
144
|
/** Named connection to draw from the registry. Default `'kv'` — reads
|
|
@@ -168,10 +171,10 @@ export declare interface RespConnections {
|
|
|
168
171
|
/** Get-or-create the pooled RESP command client for a named connection.
|
|
169
172
|
* One socket per name for the registry's lifetime — concurrent first-callers
|
|
170
173
|
* collapse to a single connect. */
|
|
171
|
-
readonly client: (name: string) => Effect.Effect<RespClient, KvError |
|
|
174
|
+
readonly client: (name: string) => Effect.Effect<RespClient, KvError | Config.ConfigError>;
|
|
172
175
|
/** Resolve a named connection's config WITHOUT opening it — for consumers
|
|
173
176
|
* that build their own client (e.g. a dedicated pub/sub connection). */
|
|
174
|
-
readonly config: (name: string) => Effect.Effect<RedisConnectionConfig,
|
|
177
|
+
readonly config: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,83 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
getOrElse: (t, n) => e.get(t).pipe(c.map((e) => d.isSome(e) ? e.value : n())),
|
|
9
|
-
set: (t, n, r) => e.set(t, n, r),
|
|
10
|
-
delete: e.delete,
|
|
11
|
-
has: e.has,
|
|
12
|
-
list: e.list,
|
|
13
|
-
clear: e.clear
|
|
14
|
-
};
|
|
15
|
-
}) }) {}, h = (e, t) => t === void 0 ? Infinity : e + t, g = (e) => c.gen(function* () {
|
|
16
|
-
let t = yield* f.make(l.empty()), n = e.now === void 0 ? o.currentTimeMillis : c.sync(e.now);
|
|
17
|
-
return {
|
|
18
|
-
get: (e) => n.pipe(c.flatMap((n) => f.modify(t, (t) => {
|
|
19
|
-
let r = l.get(t, e);
|
|
20
|
-
return d.isNone(r) ? [d.none(), t] : r.value.expiresAt <= n ? [d.none(), l.remove(t, e)] : [d.some(r.value.value), t];
|
|
21
|
-
}))),
|
|
22
|
-
set: (e, r, i) => n.pipe(c.flatMap((n) => f.update(t, (t) => l.set(t, e, {
|
|
23
|
-
value: r,
|
|
24
|
-
expiresAt: h(n, i?.ttlMs)
|
|
25
|
-
})))),
|
|
26
|
-
delete: (e) => n.pipe(c.flatMap((n) => f.modify(t, (t) => {
|
|
27
|
-
let r = l.get(t, e);
|
|
28
|
-
return d.isNone(r) ? [!1, t] : [r.value.expiresAt > n, l.remove(t, e)];
|
|
29
|
-
}))),
|
|
30
|
-
has: (e) => n.pipe(c.flatMap((n) => f.modify(t, (t) => {
|
|
31
|
-
let r = l.get(t, e);
|
|
32
|
-
return d.isNone(r) ? [!1, t] : r.value.expiresAt <= n ? [!1, l.remove(t, e)] : [!0, t];
|
|
33
|
-
}))),
|
|
34
|
-
list: (e) => n.pipe(c.flatMap((n) => f.get(t).pipe(c.map((t) => {
|
|
35
|
-
let r = [];
|
|
36
|
-
for (let [i, a] of t) a.expiresAt <= n || (e === "" || i.startsWith(e)) && r.push(i);
|
|
37
|
-
return r;
|
|
38
|
-
})))),
|
|
39
|
-
clear: () => f.set(t, l.empty())
|
|
40
|
-
};
|
|
41
|
-
}), _ = (e = {}) => u.effect(p, g(e)), v = (e, t = "voltro:kv") => {
|
|
42
|
-
let i = (e) => `${t}:${e}`, a = (e) => e.slice(t.length + 1), o = (t, i, a) => c.tryPromise({
|
|
43
|
-
try: () => a(e),
|
|
44
|
-
catch: (e) => new r({
|
|
45
|
-
operation: t,
|
|
46
|
-
key: i,
|
|
47
|
-
cause: String(e)
|
|
48
|
-
})
|
|
49
|
-
}).pipe(c.retry(n)), s = (e, t) => c.try({
|
|
50
|
-
try: () => JSON.stringify(t),
|
|
51
|
-
catch: (t) => new r({
|
|
52
|
-
operation: "serialize",
|
|
53
|
-
key: e,
|
|
54
|
-
cause: String(t)
|
|
55
|
-
})
|
|
56
|
-
}), l = (e, t) => c.try({
|
|
57
|
-
try: () => d.some(JSON.parse(t)),
|
|
58
|
-
catch: (t) => new r({
|
|
59
|
-
operation: "deserialize",
|
|
60
|
-
key: e,
|
|
61
|
-
cause: String(t)
|
|
62
|
-
})
|
|
63
|
-
}), u = (e, t, n) => o("list", "", (n) => n.scan(t, e, 200)).pipe(c.flatMap(({ cursor: t, keys: r }) => {
|
|
64
|
-
for (let e of r) n.push(a(e));
|
|
65
|
-
return t === "0" ? c.succeed(n) : u(e, t, n);
|
|
66
|
-
})), f = (e) => o("clear", "", (n) => n.scan(e, `${t}:*`, 200)).pipe(c.flatMap(({ cursor: e, keys: t }) => (t.length > 0 ? o("clear", "", (e) => e.del(t)) : c.succeed(0)).pipe(c.flatMap(() => e === "0" ? c.void : f(e)))));
|
|
67
|
-
return {
|
|
68
|
-
get: (e) => o("get", e, (t) => t.get(i(e))).pipe(c.flatMap((t) => t === null ? c.succeed(d.none()) : l(e, t))),
|
|
69
|
-
set: (e, t, n) => s(e, t).pipe(c.flatMap((t) => n?.ttlMs !== void 0 && n.ttlMs > 0 ? o("set", e, (r) => r.psetex(i(e), Math.ceil(n.ttlMs), t)) : o("set", e, (n) => n.set(i(e), t))), c.asVoid),
|
|
70
|
-
delete: (e) => o("delete", e, (t) => t.del([i(e)])).pipe(c.map((e) => e > 0)),
|
|
71
|
-
has: (e) => o("has", e, (t) => t.exists(i(e))).pipe(c.map((e) => e > 0)),
|
|
72
|
-
list: (e) => u(`${t}:${e}*`, "0", []),
|
|
73
|
-
clear: () => f("0")
|
|
74
|
-
};
|
|
75
|
-
}, y = (e = {}) => u.effect(p, c.gen(function* () {
|
|
76
|
-
return v(yield* (yield* a).client(e.connectionName ?? "kv").pipe(c.catchAll((e) => e instanceof r ? c.fail(e) : c.fail(new r({
|
|
77
|
-
operation: "connect",
|
|
78
|
-
key: "",
|
|
79
|
-
cause: String(e)
|
|
80
|
-
})))), e.keyPrefix ?? "voltro:kv");
|
|
81
|
-
}));
|
|
82
|
-
//#endregion
|
|
83
|
-
export { m as Kv, r as KvError, p as KvStore, a as RedisConnections, n as commandRetry, i as connect, t as connectionConfig, e as layerConnections, _ as layerMemory, y as layerRedis, v as makeRedisKvStore };
|
|
1
|
+
import { KvStore as e } from "./KvStore.js";
|
|
2
|
+
import { Kv as t } from "./Kv.js";
|
|
3
|
+
import { KvError as n } from "./errors.js";
|
|
4
|
+
import { layerMemory as r } from "./backendMemory.js";
|
|
5
|
+
import { RedisConnections as i, commandRetry as a, connect as o, connectionConfig as s, layerConnections as c } from "./connection.js";
|
|
6
|
+
import { layerRedis as l, makeRedisKvStore as u } from "./backendRedis.js";
|
|
7
|
+
export { t as Kv, n as KvError, e as KvStore, i as RedisConnections, a as commandRetry, o as connect, s as connectionConfig, c as layerConnections, r as layerMemory, l as layerRedis, u as makeRedisKvStore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/kv",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Named RESP connection registry + durable key-value primitive. One config surface for many Redis-family connections (Redis/Valkey/KeyDB/Dragonfly/Upstash) shared across cache, rate-limit, RYW and broadcast; plus a durable, tenant-scoped KV store distinct from the best-effort cache.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"ioredis": "^5.11.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"effect": "
|
|
45
|
+
"effect": "4.0.1"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { Config as e, Context as t, Effect as n, HashMap as r, Layer as i, Option as a, Schedule as o, Schema as s, Scope as c, SynchronizedRef as l } from "effect";
|
|
2
|
-
//#region src/errors.ts
|
|
3
|
-
var u = class extends s.TaggedError()("KvError", {
|
|
4
|
-
operation: s.String,
|
|
5
|
-
key: s.String,
|
|
6
|
-
cause: s.String
|
|
7
|
-
}) {}, d = (e) => ({
|
|
8
|
-
get: (t) => e.get(t),
|
|
9
|
-
set: (t, n) => e.set(t, n),
|
|
10
|
-
psetex: (t, n, r) => e.psetex(t, n, r),
|
|
11
|
-
del: (t) => t.length === 0 ? Promise.resolve(0) : e.del(...t),
|
|
12
|
-
exists: (t) => e.exists(t),
|
|
13
|
-
eval: (t, n, ...r) => e.eval(t, n, ...r),
|
|
14
|
-
zrange: (t) => e.zrange(t, 0, -1),
|
|
15
|
-
scan: async (t, n, r) => {
|
|
16
|
-
let [i, a] = await e.scan(t, "MATCH", n, "COUNT", r);
|
|
17
|
-
return {
|
|
18
|
-
cursor: i,
|
|
19
|
-
keys: a
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
close: () => e.quit().then(() => void 0).catch(() => {
|
|
23
|
-
try {
|
|
24
|
-
e.disconnect();
|
|
25
|
-
} catch {}
|
|
26
|
-
})
|
|
27
|
-
}), f = (e) => ({
|
|
28
|
-
get: (t) => e.get(t),
|
|
29
|
-
set: (t, n) => e.set(t, n),
|
|
30
|
-
psetex: (t, n, r) => e.psetex(t, n, r),
|
|
31
|
-
del: (t) => t.length === 0 ? Promise.resolve(0) : e.del(...t),
|
|
32
|
-
exists: (t) => e.exists(t),
|
|
33
|
-
eval: (t, n, ...r) => e.eval(t, r.slice(0, n), r.slice(n)),
|
|
34
|
-
zrange: (t) => e.zrange(t, 0, -1),
|
|
35
|
-
scan: async (t, n, r) => {
|
|
36
|
-
let [i, a] = await e.scan(t, {
|
|
37
|
-
match: n,
|
|
38
|
-
count: r
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
cursor: String(i),
|
|
42
|
-
keys: a
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
close: () => Promise.resolve()
|
|
46
|
-
}), p = (e) => e.toUpperCase().replace(/[^A-Z0-9]+/g, "_"), m = (t) => n.gen(function* () {
|
|
47
|
-
let n = p(t), r = yield* e.string(`${n}_REDIS_URL`).pipe(e.orElse(() => e.string("REDIS_URL"))), i = yield* e.literal("resp", "http")(`${n}_REDIS_DRIVER`).pipe(e.orElse(() => e.literal("resp", "http")("REDIS_DRIVER")), e.withDefault("resp")), o = yield* e.string(`${n}_REDIS_TOKEN`).pipe(e.orElse(() => e.string("REDIS_TOKEN")), e.orElse(() => e.string("UPSTASH_REDIS_REST_TOKEN")), e.option), s = yield* e.string(`${n}_REDIS_KEY_PREFIX`).pipe(e.option);
|
|
48
|
-
return {
|
|
49
|
-
url: r,
|
|
50
|
-
driver: i,
|
|
51
|
-
...a.isSome(o) ? { token: o.value } : {},
|
|
52
|
-
...a.isSome(s) ? { keyPrefix: s.value } : {}
|
|
53
|
-
};
|
|
54
|
-
}), h = (e) => n.tryPromise({
|
|
55
|
-
try: async () => {
|
|
56
|
-
if (e.driver === "http") {
|
|
57
|
-
let t = await import("@upstash/redis"), n = typeof t.default == "function" ? t.default : t.default?.Redis, r = t.Redis ?? n;
|
|
58
|
-
if (!r) throw Error("@upstash/redis: Redis constructor not found in module");
|
|
59
|
-
return f(new r({
|
|
60
|
-
url: e.url,
|
|
61
|
-
token: e.token,
|
|
62
|
-
automaticDeserialization: !1
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
65
|
-
let t = (await import("ioredis")).default, n = new t(e.url);
|
|
66
|
-
return n.on("error", () => {}), d(n);
|
|
67
|
-
},
|
|
68
|
-
catch: (e) => new u({
|
|
69
|
-
operation: "connect",
|
|
70
|
-
key: "",
|
|
71
|
-
cause: String(e)
|
|
72
|
-
})
|
|
73
|
-
}), g = o.exponential("10 millis").pipe(o.intersect(o.recurs(2))), _ = class extends t.Tag("@voltro/RedisConnections")() {}, v = i.scoped(_, n.gen(function* () {
|
|
74
|
-
let e = yield* n.scope, t = yield* l.make(r.empty()), i = (e) => m(e);
|
|
75
|
-
return {
|
|
76
|
-
client: (o) => l.modifyEffect(t, (t) => {
|
|
77
|
-
let s = r.get(t, o);
|
|
78
|
-
return a.isSome(s) ? n.succeed([s.value, t]) : i(o).pipe(n.flatMap(h), n.tap((t) => c.addFinalizer(e, n.promise(() => t.close().catch(() => void 0)))), n.map((e) => [e, r.set(t, o, e)]));
|
|
79
|
-
}),
|
|
80
|
-
config: i
|
|
81
|
-
};
|
|
82
|
-
}));
|
|
83
|
-
//#endregion
|
|
84
|
-
export { v as a, m as i, g as n, u as o, h as r, _ as t };
|