@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.
@@ -1,6 +1,6 @@
1
1
  # Third-Party Notices — @voltro/kv
2
2
 
3
- This package depends on the open-source software listed below. Each is the
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 };
@@ -0,0 +1,5 @@
1
+ import { Context as e } from "effect";
2
+ //#region src/KvStore.ts
3
+ var t = class extends e.Service()("@voltro/KvStore") {};
4
+ //#endregion
5
+ export { t as KvStore };
@@ -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 };
@@ -1,15 +1,16 @@
1
- import { ConfigError } from 'effect';
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<[ Duration, number], unknown, never>;
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.TaggedErrorClass<KvError, "KvError", {
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: typeof Schema.String;
31
+ readonly operation: Schema.String;
41
32
  /** The key (or connection name) involved; '' where not applicable. */
42
- key: typeof Schema.String;
33
+ readonly key: Schema.String;
43
34
  /** `String(originalError)` — round-trip-safe diagnostic form. */
44
- cause: typeof Schema.String;
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.TagClass<RedisConnections, "@voltro/RedisConnections", RespConnections>;
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 | ConfigError.ConfigError>;
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, ConfigError.ConfigError>;
88
+ readonly config: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
98
89
  }
99
90
 
100
91
  export { }
@@ -1,2 +1,81 @@
1
- import { a as e, i as t, n, r, t as i } from "./connection-DtnqPJzH.js";
2
- export { i as RedisConnections, n as commandRetry, r as connect, t as connectionConfig, e as layerConnections };
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,9 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/errors.ts
3
+ var t = class extends e.TaggedError()("KvError", {
4
+ operation: e.String,
5
+ key: e.String,
6
+ cause: e.String
7
+ }) {};
8
+ //#endregion
9
+ export { t as KvError };
@@ -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 { ConfigError } from 'effect';
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<[ Duration, number], unknown, never>;
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: Effect.Service.Class<Kv, "@voltro/Kv", {
37
- readonly effect: Effect.Effect<{
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.TaggedErrorClass<KvError, "KvError", {
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: typeof Schema.String;
61
+ readonly operation: Schema.String;
59
62
  /** The key (or connection name) involved; '' where not applicable. */
60
- key: typeof Schema.String;
63
+ readonly key: Schema.String;
61
64
  /** `String(originalError)` — round-trip-safe diagnostic form. */
62
- cause: typeof Schema.String;
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.TagClass<KvStore, "@voltro/KvStore", KvStoreShape>;
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.TagClass<RedisConnections, "@voltro/RedisConnections", RespConnections>;
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 | ConfigError.ConfigError>;
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, ConfigError.ConfigError>;
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 { a as e, i as t, n, o as r, r as i, t as a } from "./connection-DtnqPJzH.js";
2
- import { Clock as o, Context as s, Effect as c, HashMap as l, Layer as u, Option as d, Ref as f } from "effect";
3
- //#region src/KvStore.ts
4
- var p = class extends s.Tag("@voltro/KvStore")() {}, m = class extends c.Service()("@voltro/Kv", { effect: c.gen(function* () {
5
- let e = yield* p;
6
- return {
7
- get: (t) => e.get(t),
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.84.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": "^3.22.2"
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 };