@voltro/kv 0.86.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +5484 -0
  2. package/README.md +110 -0
  3. package/THIRD-PARTY-NOTICES.md +2 -482
  4. package/dist/Kv.js +1 -2
  5. package/dist/KvStore.js +1 -2
  6. package/dist/analytics.d.ts +337 -0
  7. package/dist/analytics.js +15 -0
  8. package/dist/backendMemory.js +1 -2
  9. package/dist/backendRedis.js +1 -2
  10. package/dist/cacheAtomicProtocol.js +257 -0
  11. package/dist/cacheAtomicScript.js +326 -0
  12. package/dist/cacheAtomicState.js +140 -0
  13. package/dist/connection.d.ts +990 -9
  14. package/dist/connection.js +336 -52
  15. package/dist/connectionAnalyticsConfig.js +152 -0
  16. package/dist/connectionConfigError.js +10 -0
  17. package/dist/connectionConfigProvider.js +110 -0
  18. package/dist/connectionDefinitions.js +284 -0
  19. package/dist/connectionMemory.js +206 -0
  20. package/dist/connectionMetrics.js +78 -0
  21. package/dist/connectionNamespace.js +18 -0
  22. package/dist/connectionNats.js +148 -0
  23. package/dist/connectionNatsConfig.js +86 -0
  24. package/dist/connectionNatsSubjects.js +15 -0
  25. package/dist/connectionOwner.js +43 -0
  26. package/dist/connectionPool.js +738 -0
  27. package/dist/connectionPrefix.js +36 -0
  28. package/dist/connectionScripts.js +3 -0
  29. package/dist/connectionSqlConfig.js +118 -0
  30. package/dist/connectionStorageConfig.js +189 -0
  31. package/dist/connectionStorageFacade.js +77 -0
  32. package/dist/connectionSubscriber.js +130 -0
  33. package/dist/driverModule.js +9 -0
  34. package/dist/errors.js +6 -3
  35. package/dist/expirySweep.js +1 -2
  36. package/dist/hot-contract.d.ts +297 -0
  37. package/dist/hot-contract.js +17 -0
  38. package/dist/hot-testing.d.ts +242 -0
  39. package/dist/hot-testing.js +1 -0
  40. package/dist/hot.d.ts +936 -0
  41. package/dist/hot.js +7 -0
  42. package/dist/hotMemory.js +45 -0
  43. package/dist/hotOperations.js +222 -0
  44. package/dist/hotRateLimit.js +102 -0
  45. package/dist/hotReadAvailability.js +18 -0
  46. package/dist/hotRedis.js +138 -0
  47. package/dist/hotRedisScript.js +3 -0
  48. package/dist/hotRouting.js +77 -0
  49. package/dist/hotState.js +181 -0
  50. package/dist/hotStoreConformance.js +365 -0
  51. package/dist/index.d.ts +113 -9
  52. package/dist/nativeRedisReads.js +77 -0
  53. package/dist/nativeRedisTopology.js +222 -0
  54. package/dist/redisDiagnostics.js +26 -0
  55. package/dist/redisReadPolicy.js +38 -0
  56. package/dist/sql.d.ts +77 -0
  57. package/dist/sql.js +0 -0
  58. package/dist/storage.d.ts +166 -0
  59. package/dist/storage.js +47 -0
  60. package/dist/storageAddress.js +19 -0
  61. package/package.json +49 -6
package/dist/index.d.ts CHANGED
@@ -21,7 +21,42 @@ export declare const commandRetry: Schedule.Schedule< Duration, unknown, never,
21
21
  */
22
22
  export declare const connect: (config: RedisConnectionConfig) => Effect.Effect<RespClient, KvError>;
23
23
 
24
- export declare const connectionConfig: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
24
+ /** Resolve an explicit kind:name, a configured default, or the established env convention. */
25
+ export declare function connectionConfig(reference: string): Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
26
+
27
+ export declare function connectionConfig(reference: string, definitions: RedisConnectionsConfig, context?: ConnectionContext): Effect.Effect<RedisConnectionConfig, Config.ConfigError | KvError>;
28
+
29
+ declare interface ConnectionContext {
30
+ readonly tenantId: string;
31
+ }
32
+
33
+ /** An environment declaration, never the environment value itself. */
34
+ declare interface ConnectionEnv {
35
+ readonly _tag: 'ConnectionEnv';
36
+ readonly name: string;
37
+ }
38
+
39
+ /** A connection's kind is part of its reference, so consumers cannot mix ports. */
40
+ declare type ConnectionKind = 'redis' | 'nats' | 'storage' | 'analytics' | 'sql';
41
+
42
+ declare interface ConnectionMetadata {
43
+ readonly tls?: 'auto' | 'required';
44
+ readonly persistence?: 'required' | 'optional' | 'none';
45
+ readonly keyPrefix?: string;
46
+ /** Explicit opt-in when a production connection intentionally uses a shared namespace. */
47
+ readonly allowSharedNamespace?: boolean;
48
+ readonly region?: string;
49
+ readonly label?: string;
50
+ readonly acl?: string;
51
+ }
52
+
53
+ declare type ConnectionRef<K extends ConnectionKind = ConnectionKind> = `${K}:${string}`;
54
+
55
+ declare interface DynamicRedisConnection {
56
+ readonly _tag: 'DynamicRedisConnection';
57
+ readonly resolve: (context: ConnectionContext) => Effect.Effect<RedisConnectionConfig, KvError>;
58
+ readonly ttlMs: number;
59
+ }
25
60
 
26
61
  export declare class Kv extends Kv_base {
27
62
  /** `Kv` over whatever `KvStore` is provided. */
@@ -53,6 +88,7 @@ declare const Kv_base: Context.ServiceClass<Kv, "@voltro/Kv", {
53
88
  };
54
89
 
55
90
  export declare class KvError extends KvError_base {
91
+ get message(): string;
56
92
  }
57
93
 
58
94
  declare const KvError_base: Schema.Class<KvError, Schema.TaggedStruct<"KvError", {
@@ -125,14 +161,40 @@ export declare interface MemoryKvOptions {
125
161
  readonly now?: () => number;
126
162
  }
127
163
 
128
- export declare interface RedisConnectionConfig {
129
- /** `redis://…` / `rediss://…` for the `resp` driver, the REST url for `http`. */
164
+ /** A local registry entry. Its clock is injectable; it opens no transport. */
165
+ declare interface MemoryRedisConnection {
166
+ readonly _tag: 'MemoryRedisConnection';
167
+ readonly now?: () => number;
168
+ }
169
+
170
+ export declare interface RedisConnectionConfig extends ConnectionMetadata, RedisTimeouts {
171
+ /** Native Redis, Cluster/Sentinel seed URLs, or the REST URL for HTTP. */
130
172
  readonly url: string;
131
- readonly driver: 'resp' | 'http';
173
+ readonly driver: 'resp' | 'http' | 'memory';
174
+ readonly memoryClock?: () => number;
132
175
  /** Auth token — `http` (Upstash REST) driver only. */
133
176
  readonly token?: string;
134
177
  /** Optional key namespace a deployment may layer its own prefix under. */
135
- readonly keyPrefix?: string;
178
+ readonly cluster?: boolean;
179
+ readonly replicas?: ReadonlyArray<string>;
180
+ readonly readFrom?: 'primary' | 'replica' | 'nearest';
181
+ readonly maxmemoryPolicyHint?: string;
182
+ readonly source?: 'config' | 'environment' | 'override';
183
+ }
184
+
185
+ declare interface RedisConnectionDefinition extends ConnectionMetadata, RedisTimeouts {
186
+ readonly url: ConnectionEnv;
187
+ readonly driver?: 'resp' | 'http';
188
+ readonly token?: ConnectionEnv;
189
+ readonly cluster?: boolean;
190
+ readonly replicas?: ReadonlyArray<ConnectionEnv>;
191
+ readonly readFrom?: 'primary' | 'replica' | 'nearest';
192
+ readonly maxmemoryPolicyHint?: string;
193
+ }
194
+
195
+ declare interface RedisConnectionDiagnostics {
196
+ /** Every current Cluster primary, or the single standalone/Sentinel primary. */
197
+ readonly nodes: ReadonlyArray<RedisNodeDiagnostics>;
136
198
  }
137
199
 
138
200
  export declare class RedisConnections extends RedisConnections_base {
@@ -140,6 +202,17 @@ export declare class RedisConnections extends RedisConnections_base {
140
202
 
141
203
  declare const RedisConnections_base: Context.ServiceClass<RedisConnections, "@voltro/RedisConnections", RespConnections>;
142
204
 
205
+ declare interface RedisConnectionsConfig {
206
+ readonly redis?: Readonly<Record<string, RedisConnectionDefinition | DynamicRedisConnection | MemoryRedisConnection>>;
207
+ readonly defaults?: {
208
+ readonly redis?: string;
209
+ };
210
+ /** App-owned selection by plugin name and declared role; plugins never choose endpoints. */
211
+ readonly bindings?: Readonly<Record<string, Readonly<Record<string, ConnectionRef>>>>;
212
+ readonly maxDynamicPools?: number;
213
+ readonly rotationGraceMs?: number;
214
+ }
215
+
143
216
  export declare interface RedisKvOptions {
144
217
  /** Named connection to draw from the registry. Default `'kv'` — reads
145
218
  * `KV_REDIS_URL` → `REDIS_URL`. */
@@ -148,11 +221,28 @@ export declare interface RedisKvOptions {
148
221
  readonly keyPrefix?: string;
149
222
  }
150
223
 
151
- /** The minimal command surface both ioredis and @upstash/redis satisfy —
152
- * the superset the framework's command-only RESP consumers (cache, rate
153
- * limiter, KV) need. `eval` runs a Lua script server-side (numKeys splits
154
- * KEYS from ARGV, the ioredis convention); `zrange` reads a full sorted set. */
224
+ /** Only enumerated server settings leave the transport; never raw INFO/errors. */
225
+ declare interface RedisNodeDiagnostics {
226
+ readonly persistence: 'aof' | 'snapshot' | 'none' | 'unknown';
227
+ readonly maxmemoryPolicy: 'noeviction' | 'allkeys-lru' | 'allkeys-lfu' | 'allkeys-random' | 'volatile-lru' | 'volatile-lfu' | 'volatile-random' | 'volatile-ttl' | 'unknown';
228
+ }
229
+
230
+ /** Optional native RESP transport deadlines in integer milliseconds. */
231
+ declare interface RedisTimeouts {
232
+ /** TCP/TLS connection establishment limit; does not prove command completion. */
233
+ readonly connectTimeoutMs?: number;
234
+ /** Limit for a command reply. A write may still complete after this expires. */
235
+ readonly commandTimeoutMs?: number;
236
+ }
237
+
155
238
  export declare interface RespClient {
239
+ readonly readOnly?: RespReadClient;
240
+ /** False for a local test connection. PING never turns it into a network backend. */
241
+ readonly crossNode?: boolean;
242
+ /** Read-only round-trip probe. Custom drivers may omit it; no readiness is inferred. */
243
+ readonly ping?: () => Promise<string>;
244
+ /** Explicit read-only server-settings observation. Missing/denied fields stay unknown. */
245
+ readonly diagnostics?: () => Promise<RedisConnectionDiagnostics>;
156
246
  readonly get: (key: string) => Promise<string | null>;
157
247
  readonly set: (key: string, value: string) => Promise<unknown>;
158
248
  readonly psetex: (key: string, ms: number, value: string) => Promise<unknown>;
@@ -164,6 +254,8 @@ export declare interface RespClient {
164
254
  readonly cursor: string;
165
255
  readonly keys: ReadonlyArray<string>;
166
256
  }>;
257
+ /** Native command transport is ready again after reconnect. Registry-owned. */
258
+ readonly onReconnect?: (listener: () => void) => () => void;
167
259
  readonly close: () => Promise<void>;
168
260
  }
169
261
 
@@ -177,4 +269,16 @@ export declare interface RespConnections {
177
269
  readonly config: (name: string) => Effect.Effect<RedisConnectionConfig, Config.ConfigError>;
178
270
  }
179
271
 
272
+ /** The minimal command surface both ioredis and @upstash/redis satisfy —
273
+ * the superset the framework's command-only RESP consumers (cache, rate
274
+ * limiter, KV) need. `eval` runs a Lua script server-side (numKeys splits
275
+ * KEYS from ARGV, the ioredis convention); `zrange` reads a full sorted set. */
276
+ /** Read-only commands. Native and HTTP Lua use EVAL_RO, never writable EVAL. */
277
+ declare interface RespReadClient {
278
+ readonly get: (key: string) => Promise<string | null>;
279
+ readonly exists: (key: string) => Promise<number>;
280
+ readonly zrange: (key: string) => Promise<ReadonlyArray<string>>;
281
+ readonly eval: (script: string, numKeys: number, ...args: ReadonlyArray<string | number>) => Promise<unknown>;
282
+ }
283
+
180
284
  export { }
@@ -0,0 +1,77 @@
1
+ import { createNativeRedisConnection as e, disconnectNativeRedis as t, nativeRedisTopology as n } from "./nativeRedisTopology.js";
2
+ import { measuredRedisReader as r } from "./redisReadPolicy.js";
3
+ // src/nativeRedisReads.ts
4
+ var i = () => {
5
+ throw Error("No confirmed native Redis reader is available");
6
+ }, a = (e, t, n) => {
7
+ let r = (t ? e.slice(1) : e).filter((e) => e.status === "ready" && n.has(e));
8
+ return r.sort((e, t) => n.get(e) - n.get(t)), r[0] ?? i();
9
+ }, o = (e) => {
10
+ let t = n(e);
11
+ if (t.kind === "standalone" && e.readFrom === "replica" && !e.replicas?.length) throw Error("Standalone replica reads require replica endpoints");
12
+ let r = t.kind === "standalone" ? t.db : t.kind === "sentinel" ? t.options.db ?? 0 : 0;
13
+ return (e.replicas ?? []).map((i) => {
14
+ let a = {
15
+ ...e,
16
+ url: i,
17
+ cluster: t.kind === "cluster",
18
+ replicas: [],
19
+ readFrom: "primary"
20
+ }, o = n(a);
21
+ if ((o.kind === "standalone" ? o.db : o.kind === "sentinel" ? o.options.db ?? 0 : 0) !== r || t.kind === "cluster" != (o.kind === "cluster") || o.kind === "sentinel" || /^rediss(?:\+cluster|\+sentinel)?:/i.test(e.url) && !/^rediss(?:\+cluster)?:/i.test(i)) throw Error("Replica endpoints must retain topology, database and TLS admission");
22
+ return a;
23
+ });
24
+ }, s = async (n, s, c) => {
25
+ let l = n.readFrom ?? "primary", u = o(n), d = [], f = /* @__PURE__ */ new Map(), p = async () => {
26
+ await Promise.all(d.map(t));
27
+ }, m = async (t) => {
28
+ c.throwIfAborted();
29
+ let n = /* @__PURE__ */ new Map(), r = await e(t, "command", l, (e) => a(e, l === "replica", n));
30
+ d.push(r), f.set(r, n), r.client.on("error", () => {});
31
+ let i = () => {
32
+ r.client.disconnect();
33
+ };
34
+ c.addEventListener("abort", i, { once: !0 });
35
+ try {
36
+ c.throwIfAborted(), await r.client.connect(), await r.ready(), c.throwIfAborted();
37
+ } finally {
38
+ c.removeEventListener("abort", i);
39
+ }
40
+ return r;
41
+ };
42
+ try {
43
+ if (l !== "primary") {
44
+ if (u.length > 0) for (let e of u) await m(e);
45
+ else s.kind === "standalone" ? l === "replica" && i() : await m(n);
46
+ }
47
+ let e = l === "replica" || s.kind === "cluster" && l !== "primary" ? d : [s, ...d], t = async (e) => {
48
+ if (e.kind === "cluster" && await new Promise((t, n) => e.client.refreshSlotsCache((e) => e ? n(e) : t())), await e.ready(), e.kind === "cluster") {
49
+ let t = f.get(e) ?? i(), n = await Promise.all(e.client.nodes("all").map(async (e) => {
50
+ if (e.status !== "ready") return;
51
+ let t = performance.now();
52
+ try {
53
+ if (await e.ping() !== "PONG") return;
54
+ let n = performance.now() - t;
55
+ if (Number(await e.call("eval_ro", "return 1", 0)) === 1) return [e, n];
56
+ } catch {}
57
+ }));
58
+ t.clear();
59
+ for (let e of n) e !== void 0 && t.set(...e);
60
+ (l === "replica" ? e.client.nodes("slave") : e.client.nodes("all")).some((e) => t.has(e)) || i();
61
+ } else {
62
+ if (await e.client.ping() !== "PONG" && i(), e !== s) {
63
+ let [t, n] = await Promise.all([s.client.info("replication"), e.client.info("replication")]), r = (e, t) => e.split(/\r?\n/).find((e) => e.startsWith(t + ":"))?.slice(t.length + 1);
64
+ r(n, "role") === "slave" && r(n, "master_link_status") === "up" && r(t, "master_replid") && r(t, "master_replid") === r(n, "master_replid") || i();
65
+ }
66
+ Number(await e.client.call("eval_ro", "return 1", 0)) !== 1 && i();
67
+ }
68
+ };
69
+ return l !== "primary" && await r(e, t), {
70
+ close: p,
71
+ select: async () => l === "primary" ? s : r(e, t)
72
+ };
73
+ } catch (e) {
74
+ throw await p(), e;
75
+ }
76
+ };
77
+ export { o as nativeRedisReadConfigs, s as openNativeRedisReaders, a as selectNativeReadNode };
@@ -0,0 +1,222 @@
1
+ import { connectionMetadataProblem as e, redisTimeoutsProblem as t } from "./connectionDefinitions.js";
2
+ import { missingDriver as n } from "./driverModule.js";
3
+ // src/nativeRedisTopology.ts
4
+ var r = (e) => {
5
+ throw Error(`Invalid native Redis topology: ${e}`);
6
+ }, i = (e) => !/^\d+$/.test(e) || !Number.isSafeInteger(Number(e)) ? r("database must be a nonnegative integer") : Number(e), a = (n) => {
7
+ let a = t(n, n.driver);
8
+ if (a) return r(a);
9
+ try {
10
+ let t = e(n);
11
+ if (t) return r(t);
12
+ if (typeof n.url != "string" || !n.url.trim()) return r("URL is required");
13
+ if (n.url !== n.url.trim()) return r("URL must not contain surrounding whitespace");
14
+ if (n.cluster !== void 0 && typeof n.cluster != "boolean") return r("cluster must be boolean");
15
+ let a = /^([a-z][a-z\d+.-]*):\/\/([^/?#]+)(.*)$/i.exec(n.url);
16
+ if (!a) return r("URL must declare a native Redis scheme and endpoint");
17
+ let o = a[1].toLowerCase(), s = o === "redis+cluster" || o === "rediss+cluster", c = o === "redis+sentinel" || o === "rediss+sentinel";
18
+ if (!s && !c && o !== "redis" && o !== "rediss") return r("scheme is not native Redis");
19
+ if (s && n.cluster === !1) return r("cluster URL conflicts with cluster: false");
20
+ if (c && n.cluster === !0) return r("Sentinel URL conflicts with cluster: true");
21
+ let l = o.startsWith("rediss");
22
+ if (n.tls === "required" && !l) return r("TLS requires a secure Redis scheme");
23
+ let u = s || n.cluster === !0, d = a[2], f = d.lastIndexOf("@"), p = f < 0 ? "" : d.slice(0, f + 1), m = (f < 0 ? d : d.slice(f + 1)).split(",");
24
+ if (!u && !c && m.length !== 1) return r("standalone URL must contain one endpoint");
25
+ let h = m.map((e) => {
26
+ if (!e) return r("endpoint list contains an empty entry");
27
+ let t = new URL(`redis://${e}`);
28
+ if (t.username || t.password || t.pathname !== "" || t.search || t.hash || !t.hostname || /\s|%/.test(t.hostname)) return r("endpoint shape is invalid");
29
+ let n = t.port ? Number(t.port) : c ? 26379 : 6379;
30
+ return !Number.isInteger(n) || n < 1 || n > 65535 ? r("endpoint port is invalid") : {
31
+ host: t.hostname.replace(/^\[|\]$/g, ""),
32
+ port: n
33
+ };
34
+ }), g = new URL(`redis://${p}${m[0]}${a[3]}`);
35
+ if (g.hash) return r("URL fragments are not permitted");
36
+ let _ = decodeURIComponent(g.username), v = decodeURIComponent(g.password), y = c ? [
37
+ "db",
38
+ "sentinelUsername",
39
+ "sentinelPassword"
40
+ ] : ["db"];
41
+ for (let e of g.searchParams.keys()) if (!y.includes(e) || g.searchParams.getAll(e).length !== 1) return r("query parameter is unknown or repeated");
42
+ if (!u && !c) {
43
+ let e = g.pathname === "" || g.pathname === "/" ? void 0 : i(g.pathname.slice(1)), t = g.searchParams.get("db");
44
+ return t !== null && e !== void 0 && i(t) !== e ? r("database declarations conflict") : {
45
+ kind: "standalone",
46
+ url: n.url,
47
+ db: t === null ? e ?? 0 : i(t)
48
+ };
49
+ }
50
+ let b = {
51
+ ..._ ? { username: _ } : {},
52
+ ...v ? { password: v } : {},
53
+ ...l ? { tls: {} } : {}
54
+ }, x = [...new Map(h.map((e) => [JSON.stringify([e.host, e.port]), e])).values()];
55
+ if (u) {
56
+ let e = g.pathname === "" || g.pathname === "/" ? 0 : i(g.pathname.slice(1)), t = g.searchParams.get("db");
57
+ return e !== 0 || t !== null && i(t) !== 0 ? r("Cluster requires database zero") : {
58
+ kind: "cluster",
59
+ nodes: x,
60
+ options: b
61
+ };
62
+ }
63
+ let S = decodeURIComponent(g.pathname.slice(1));
64
+ if (!S || /[\s/\\?#]/.test(S)) return r("Sentinel requires one master group name");
65
+ let C = g.searchParams.get("sentinelUsername"), w = g.searchParams.get("sentinelPassword"), T = g.searchParams.get("db");
66
+ return {
67
+ kind: "sentinel",
68
+ nodes: x,
69
+ name: S,
70
+ options: {
71
+ ...b,
72
+ db: T === null ? 0 : i(T),
73
+ ...C === null ? {} : { sentinelUsername: C },
74
+ ...w === null ? {} : { sentinelPassword: w },
75
+ ...l ? {
76
+ enableTLSForSentinelMode: !0,
77
+ sentinelTLS: {}
78
+ } : {}
79
+ }
80
+ };
81
+ } catch (e) {
82
+ if (e instanceof Error && e.message.startsWith("Invalid native Redis topology:")) throw e;
83
+ return r("URL or encoded component is invalid");
84
+ }
85
+ }, o = (e, t = "master") => {
86
+ let n = /* @__PURE__ */ new WeakMap(), r = () => /* @__PURE__ */ Error("Native Redis Cluster sockets are not confirmed"), i = (e) => {
87
+ if (e.status === "ready") return Promise.resolve();
88
+ let t = n.get(e);
89
+ if (t !== void 0) return t;
90
+ if (![
91
+ "wait",
92
+ "connecting",
93
+ "connect"
94
+ ].includes(e.status)) return Promise.reject(r());
95
+ let i = new Promise((t, n) => {
96
+ let i = !1, a = () => {
97
+ e.off("ready", s), e.off("close", o), e.off("end", o), e.off("error", o);
98
+ }, o = () => {
99
+ i || (i = !0, a(), n(r()));
100
+ }, s = () => {
101
+ e.status === "ready" ? i || (i = !0, a(), t()) : o();
102
+ };
103
+ if (e.once("ready", s), e.once("close", o), e.once("end", o), e.once("error", o), e.status === "wait") try {
104
+ e.connect().then(s, o);
105
+ } catch {
106
+ o();
107
+ }
108
+ else e.status === "ready" && s();
109
+ });
110
+ return n.set(e, i), i.then(() => n.delete(e), () => n.delete(e)), i;
111
+ };
112
+ return async () => {
113
+ if (e.status !== "ready") throw r();
114
+ let n = e.nodes(t);
115
+ if (n.length === 0) throw r();
116
+ await Promise.all(n.map(i));
117
+ let a = e.nodes(t);
118
+ if (e.status !== "ready" || a.length !== n.length || a.some((e) => e.status !== "ready" || !n.includes(e))) throw r();
119
+ };
120
+ }, s = (e, t) => {
121
+ let n = 0, r, i = () => /* @__PURE__ */ Error("Native Redis database is not confirmed");
122
+ return t !== 0 && (e.on("close", () => {
123
+ n++, r = void 0;
124
+ }), e.on("ready", () => {
125
+ let a = ++n, o = Promise.resolve().then(() => {
126
+ if (n !== a || e.status !== "ready") throw i();
127
+ return e.select(t);
128
+ }).then((t) => {
129
+ if (t !== "OK" || n !== a || e.status !== "ready") throw i();
130
+ }, () => {
131
+ throw i();
132
+ });
133
+ r = o, o.catch(() => {
134
+ n === a && e.disconnect();
135
+ });
136
+ })), async () => {
137
+ let n = r;
138
+ if (e.status !== "ready" || t !== 0 && n === void 0 || (await n, e.status !== "ready" || r !== n)) throw i();
139
+ };
140
+ }, c = (e) => {
141
+ let t = (e.kind === "cluster" ? [e.client, ...e.client.nodes("all")] : [e.client]).map((e) => [
142
+ "end",
143
+ "wait",
144
+ "close",
145
+ "reconnecting"
146
+ ].includes(e.status) ? Promise.resolve() : new Promise((t) => {
147
+ let n = () => {
148
+ e.off("close", n), e.off("end", n), t();
149
+ };
150
+ e.once("close", n), e.once("end", n);
151
+ }));
152
+ return e.client.status !== "end" && e.client.disconnect(), Promise.all(t).then(() => void 0);
153
+ }, l = (e, t = {}) => ({
154
+ protocol: 2,
155
+ lazyConnect: !0,
156
+ autoResendUnfulfilledCommands: !1,
157
+ maxRetriesPerRequest: 0,
158
+ enableOfflineQueue: !1,
159
+ autoResubscribe: !1,
160
+ disconnectTimeout: 0,
161
+ ...t.connectTimeoutMs === void 0 ? {} : { connectTimeout: t.connectTimeoutMs },
162
+ ...t.commandTimeoutMs === void 0 ? {} : {
163
+ commandTimeout: t.commandTimeoutMs,
164
+ sentinelCommandTimeout: t.commandTimeoutMs
165
+ },
166
+ ...e === "subscriber" ? {
167
+ retryStrategy: () => null,
168
+ sentinelRetryStrategy: () => null
169
+ } : {}
170
+ }), u = async (e, t = "command", r = "primary", i) => {
171
+ let c = a(e), u = await import("ioredis").catch(n("ioredis", "A Redis connection")), d = u.default, f = l(t, e);
172
+ if (c.kind === "cluster") {
173
+ let e = new u.Cluster([...c.nodes], {
174
+ lazyConnect: !0,
175
+ enableOfflineQueue: !1,
176
+ scaleReads: i ?? "master",
177
+ retryDelayOnFailover: 0,
178
+ redisOptions: {
179
+ ...c.options,
180
+ ...f
181
+ },
182
+ ...t === "subscriber" ? { clusterRetryStrategy: () => null } : {}
183
+ });
184
+ return {
185
+ kind: "cluster",
186
+ client: e,
187
+ ready: o(e, r === "primary" ? "master" : "all")
188
+ };
189
+ }
190
+ let p = c.kind === "standalone" ? new d(c.url, {
191
+ ...f,
192
+ db: c.db,
193
+ .../^rediss:\/\//i.test(c.url) ? { tls: {} } : {}
194
+ }) : new d({
195
+ ...c.options,
196
+ ...f,
197
+ name: c.name,
198
+ sentinels: [...c.nodes],
199
+ role: r === "primary" ? "master" : "slave"
200
+ }), m = c.kind === "standalone" ? c.db : c.options.db ?? 0;
201
+ return {
202
+ kind: c.kind,
203
+ client: p,
204
+ ready: s(p, m)
205
+ };
206
+ }, d = async (e, t, n, r) => {
207
+ if (e.kind !== "cluster") {
208
+ let [i, a] = await e.client.scan(t, "MATCH", n, "COUNT", r);
209
+ return {
210
+ cursor: i,
211
+ keys: a
212
+ };
213
+ }
214
+ let i = e.client.nodes("master").sort((e, t) => `${e.options.host}:${e.options.port}`.localeCompare(`${t.options.host}:${t.options.port}`)), a = t === "0" ? ["0", "0"] : t.split(":"), o = Number(a[0]);
215
+ if (a.length !== 2 || !/^\d+$/.test(a[0]) || !/^\d+$/.test(a[1]) || !Number.isSafeInteger(o) || o >= i.length) throw Error("Invalid Cluster scan cursor or unavailable primary");
216
+ let [s, c] = await i[o].scan(a[1], "MATCH", n, "COUNT", r);
217
+ return {
218
+ cursor: s === "0" ? o + 1 < i.length ? `${o + 1}:0` : "0" : `${o}:${s}`,
219
+ keys: c
220
+ };
221
+ };
222
+ export { u as createNativeRedisConnection, c as disconnectNativeRedis, o as nativeClusterReadiness, s as nativeDatabaseReadiness, l as nativeRedisOptions, a as nativeRedisTopology, d as scanNativeRedis };
@@ -0,0 +1,26 @@
1
+ // src/redisDiagnostics.ts
2
+ var e = [
3
+ "noeviction",
4
+ "allkeys-lru",
5
+ "allkeys-lfu",
6
+ "allkeys-random",
7
+ "volatile-lru",
8
+ "volatile-lfu",
9
+ "volatile-random",
10
+ "volatile-ttl"
11
+ ], t = (e, t) => {
12
+ if (typeof e != "string" || e.length > 1048576) return;
13
+ let n = e.split(/\r?\n/).filter((e) => e.startsWith(t + ":")).map((e) => e.slice(t.length + 1));
14
+ return n.length === 1 ? n[0] : void 0;
15
+ }, n = (n, r, i) => {
16
+ let a = t(n, "aof_enabled"), o = t(r, "maxmemory_policy"), s = Array.isArray(i) && i.length === 2 && i[0] === "save" && typeof i[1] == "string" ? i[1].trim() : void 0, c = s?.split(/\s+/), l = s === "" ? !1 : c !== void 0 && c.length % 2 == 0 && c.every((e) => /^\d+$/.test(e) && Number.isSafeInteger(Number(e))) && c.every((e, t) => t % 2 == 1 || Number(e) > 0) ? !0 : void 0;
17
+ return {
18
+ persistence: a === "1" ? "aof" : a === "0" && l !== void 0 ? l ? "snapshot" : "none" : "unknown",
19
+ maxmemoryPolicy: e.find((e) => e === o) ?? "unknown"
20
+ };
21
+ }, r = async (e) => n(...(await Promise.allSettled([
22
+ e.info("persistence"),
23
+ e.info("memory"),
24
+ e.config("GET", "save")
25
+ ])).map((e) => e.status === "fulfilled" ? e.value : void 0));
26
+ export { r as readRedisNodeDiagnostics, n as redisNodeDiagnostics };
@@ -0,0 +1,38 @@
1
+ // src/redisReadPolicy.ts
2
+ var e = async (e, t) => {
3
+ let n = (await Promise.all(e.map(async (e) => {
4
+ let n = performance.now();
5
+ try {
6
+ return await t(e), {
7
+ candidate: e,
8
+ elapsed: performance.now() - n
9
+ };
10
+ } catch {
11
+ return;
12
+ }
13
+ }))).filter((e) => e !== void 0).sort((e, t) => e.elapsed - t.elapsed);
14
+ if (n.length === 0) throw Error("No confirmed Redis reader is available");
15
+ return n[0].candidate;
16
+ }, t = (t, n, r) => {
17
+ let i = r === "replica" ? n : [t, ...n], a = async () => {
18
+ let n = r === void 0 || r === "primary" ? t : await e(i, async (e) => {
19
+ if (e.ping === void 0 || await e.ping() !== "PONG" || e.readOnly === void 0) throw Error("Redis reader is not ready");
20
+ if (Number(await e.readOnly.eval("return 1", 0)) !== 1) throw Error("Redis read-only Lua is unavailable");
21
+ });
22
+ if (n.readOnly === void 0) throw Error("Redis read-only Lua is unavailable");
23
+ return n.readOnly;
24
+ };
25
+ return {
26
+ ...t,
27
+ readOnly: {
28
+ get: async (e) => (await a()).get(e),
29
+ exists: async (e) => (await a()).exists(e),
30
+ zrange: async (e) => (await a()).zrange(e),
31
+ eval: async (e, t, ...n) => (await a()).eval(e, t, ...n)
32
+ },
33
+ close: async () => {
34
+ await Promise.all([t, ...n].map((e) => e.close()));
35
+ }
36
+ };
37
+ };
38
+ export { t as httpRedisReaders, e as measuredRedisReader };
package/dist/sql.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { Config } from 'effect';
2
+ import { Effect } from 'effect';
3
+ import { Schema } from 'effect';
4
+ import { SqlClient } from 'effect/sql';
5
+ import { Stream } from 'effect';
6
+ import { YieldableError } from 'effect/Cause';
7
+
8
+ declare interface ConnectionContext {
9
+ readonly tenantId: string;
10
+ }
11
+
12
+ /** An environment declaration, never the environment value itself. */
13
+ declare interface ConnectionEnv {
14
+ readonly _tag: 'ConnectionEnv';
15
+ readonly name: string;
16
+ }
17
+
18
+ export declare interface DynamicSqlConnection {
19
+ readonly _tag: 'DynamicSqlConnection';
20
+ readonly resolve: (context: ConnectionContext) => Effect.Effect<SqlConnectionConfig, KvError>;
21
+ readonly ttlMs: number;
22
+ }
23
+
24
+ declare class KvError extends KvError_base {
25
+ get message(): string;
26
+ }
27
+
28
+ declare const KvError_base: Schema.Class<KvError, Schema.TaggedStruct<"KvError", {
29
+ /** Which op failed: 'get' | 'set' | 'delete' | 'has' | 'clear' |
30
+ * 'list' | 'connect' | 'serialize' | 'deserialize'. */
31
+ readonly operation: Schema.String;
32
+ /** The key (or connection name) involved; '' where not applicable. */
33
+ readonly key: Schema.String;
34
+ /** `String(originalError)` — round-trip-safe diagnostic form. */
35
+ readonly cause: Schema.String;
36
+ }>, YieldableError>;
37
+
38
+ /** A whole operation (including its transaction) borrows one physical generation.
39
+ * Do not return the client, a deferred statement or a stream from use: execute it
40
+ * inside the callback. Use stream for a lazy result that must retain its lease. */
41
+ export declare interface SqlConnection {
42
+ readonly dialect: SqlConnectionConfig['dialect'];
43
+ readonly use: <A, E, R>(run: (client: SqlClient.SqlClient) => Effect.Effect<A, E, R>) => Effect.Effect<A, E | KvError | Config.ConfigError, R>;
44
+ readonly stream: <A, E, R>(run: (client: SqlClient.SqlClient) => Stream.Stream<A, E, R>) => Stream.Stream<A, E | KvError | Config.ConfigError, R>;
45
+ }
46
+
47
+ /** Additional SQL target. The primary DataStore retains its own configuration. */
48
+ export declare interface SqlConnectionConfig {
49
+ readonly dialect: SqlConnectionDialect;
50
+ readonly url: string;
51
+ readonly tls?: 'auto' | 'required';
52
+ readonly maxConnections?: number;
53
+ readonly acquireTimeoutMs?: number;
54
+ readonly acquireQueueLimit?: number;
55
+ readonly statementTimeoutMs?: number;
56
+ readonly schema?: string;
57
+ readonly label?: string;
58
+ readonly region?: string;
59
+ readonly source?: 'config' | 'override';
60
+ }
61
+
62
+ export declare type SqlConnectionConnector = (config: SqlConnectionConfig) => Effect.Effect<SqlConnectionTransport, KvError>;
63
+
64
+ export declare interface SqlConnectionDefinition extends Omit<SqlConnectionConfig, 'url' | 'source'> {
65
+ readonly url: ConnectionEnv;
66
+ }
67
+
68
+ export declare type SqlConnectionDialect = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
69
+
70
+ /** Connector-owned resources; a consumer never receives physical disposal. */
71
+ export declare interface SqlConnectionTransport {
72
+ readonly client: SqlClient.SqlClient;
73
+ readonly probe: Effect.Effect<void, KvError>;
74
+ readonly close: Effect.Effect<void>;
75
+ }
76
+
77
+ export { }
package/dist/sql.js ADDED
File without changes