@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.
- package/CHANGELOG.md +5484 -0
- package/README.md +110 -0
- package/THIRD-PARTY-NOTICES.md +2 -482
- package/dist/Kv.js +1 -2
- package/dist/KvStore.js +1 -2
- package/dist/analytics.d.ts +337 -0
- package/dist/analytics.js +15 -0
- package/dist/backendMemory.js +1 -2
- package/dist/backendRedis.js +1 -2
- package/dist/cacheAtomicProtocol.js +257 -0
- package/dist/cacheAtomicScript.js +326 -0
- package/dist/cacheAtomicState.js +140 -0
- package/dist/connection.d.ts +990 -9
- package/dist/connection.js +336 -52
- package/dist/connectionAnalyticsConfig.js +152 -0
- package/dist/connectionConfigError.js +10 -0
- package/dist/connectionConfigProvider.js +110 -0
- package/dist/connectionDefinitions.js +284 -0
- package/dist/connectionMemory.js +206 -0
- package/dist/connectionMetrics.js +78 -0
- package/dist/connectionNamespace.js +18 -0
- package/dist/connectionNats.js +148 -0
- package/dist/connectionNatsConfig.js +86 -0
- package/dist/connectionNatsSubjects.js +15 -0
- package/dist/connectionOwner.js +43 -0
- package/dist/connectionPool.js +738 -0
- package/dist/connectionPrefix.js +36 -0
- package/dist/connectionScripts.js +3 -0
- package/dist/connectionSqlConfig.js +118 -0
- package/dist/connectionStorageConfig.js +189 -0
- package/dist/connectionStorageFacade.js +77 -0
- package/dist/connectionSubscriber.js +130 -0
- package/dist/driverModule.js +9 -0
- package/dist/errors.js +6 -3
- package/dist/expirySweep.js +1 -2
- package/dist/hot-contract.d.ts +297 -0
- package/dist/hot-contract.js +17 -0
- package/dist/hot-testing.d.ts +242 -0
- package/dist/hot-testing.js +1 -0
- package/dist/hot.d.ts +936 -0
- package/dist/hot.js +7 -0
- package/dist/hotMemory.js +45 -0
- package/dist/hotOperations.js +222 -0
- package/dist/hotRateLimit.js +102 -0
- package/dist/hotReadAvailability.js +18 -0
- package/dist/hotRedis.js +138 -0
- package/dist/hotRedisScript.js +3 -0
- package/dist/hotRouting.js +77 -0
- package/dist/hotState.js +181 -0
- package/dist/hotStoreConformance.js +365 -0
- package/dist/index.d.ts +113 -9
- package/dist/nativeRedisReads.js +77 -0
- package/dist/nativeRedisTopology.js +222 -0
- package/dist/redisDiagnostics.js +26 -0
- package/dist/redisReadPolicy.js +38 -0
- package/dist/sql.d.ts +77 -0
- package/dist/sql.js +0 -0
- package/dist/storage.d.ts +166 -0
- package/dist/storage.js +47 -0
- package/dist/storageAddress.js +19 -0
- 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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|