@redfx/bun 1.0.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +181 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Walker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @redfx/bun
|
|
2
|
+
|
|
3
|
+
[Bun](https://bun.sh) driver for [@redfx/core](https://www.npmjs.com/package/@redfx/core). It implements the
|
|
4
|
+
`send`-level port over Bun's native `RedisClient` (`Bun.redis`), with pub/sub delivered as a `Stream`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { Redis } from "@redfx/core"
|
|
8
|
+
import { BunRedis } from "@redfx/bun"
|
|
9
|
+
import { Config } from "effect"
|
|
10
|
+
|
|
11
|
+
const RedisLive = BunRedis.layerConfig(Config.string("REDIS_URL"))
|
|
12
|
+
// BunRedis.layer({ url, options, commandTimeout }) single connection
|
|
13
|
+
// BunRedis.layerPooled({ url, size: 10 }) pooled commands, dedicated pub/sub
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires the Bun runtime. Peer dependencies: `effect`, `@redfx/core`.
|
|
17
|
+
|
|
18
|
+
A few Bun-specific notes:
|
|
19
|
+
|
|
20
|
+
- `EXISTS` returns a boolean on Bun where ioredis returns 0/1; redfx normalises this so the command
|
|
21
|
+
surface (`exists` returns `boolean`) matches across adapters.
|
|
22
|
+
- Bun retries a lost connection with backoff before a command rejects (around 30s by default). If
|
|
23
|
+
you rely on the `catchTag("ConnectionError")` fail-open pattern, set tighter client options or a
|
|
24
|
+
`commandTimeout`, e.g. `BunRedis.layer({ url, commandTimeout: "2 seconds" })`.
|
|
25
|
+
- Values are treated as UTF-8 text for now: byte args are decoded with `TextDecoder` and replies
|
|
26
|
+
come back as strings, so non-UTF-8 binary won't round-trip. Use base64 for binary payloads.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConnectionError, type Redis } from "@redfx/core";
|
|
2
|
+
import { RedisClient } from "bun";
|
|
3
|
+
import { type Config, type ConfigError, type Duration, Layer } from "effect";
|
|
4
|
+
type BunRedisOptions = ConstructorParameters<typeof RedisClient>[1];
|
|
5
|
+
export interface ClientConfig {
|
|
6
|
+
readonly url?: string;
|
|
7
|
+
readonly options?: BunRedisOptions;
|
|
8
|
+
/** Effect-level per-command deadline; on expiry the command fails with `TimeoutError`. */
|
|
9
|
+
readonly commandTimeout?: Duration.DurationInput;
|
|
10
|
+
}
|
|
11
|
+
export declare namespace BunRedis {
|
|
12
|
+
const layer: (config?: ClientConfig) => Layer.Layer<Redis, ConnectionError>;
|
|
13
|
+
const layerConfig: (url: Config.Config<string>, config?: Omit<ClientConfig, "url">) => Layer.Layer<Redis, ConnectionError | ConfigError.ConfigError>;
|
|
14
|
+
/** Pools `size` command connections; pub/sub uses a dedicated connection. */
|
|
15
|
+
const layerPooled: (config: ClientConfig & {
|
|
16
|
+
readonly size: number;
|
|
17
|
+
}) => Layer.Layer<Redis, ConnectionError>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,EAKf,KAAK,KAAK,EAIX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EACL,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,QAAQ,EAEb,KAAK,EAGN,MAAM,QAAQ,CAAC;AAEhB,KAAK,eAAe,GAAG,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IACnC,0FAA0F;IAC1F,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC;CAClD;AA6HD,yBAAiB,QAAQ,CAAC;IACjB,MAAM,KAAK,GAAI,SAAQ,YAAiB,KAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,CACA,CAAC;IAE9E,MAAM,WAAW,GACtB,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAC1B,SAAS,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,KACjC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,WAAW,CAAC,WAAW,CACyB,CAAC;IAEzF,6EAA6E;IACtE,MAAM,WAAW,GACtB,QAAQ,YAAY,GAAG;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAC/C,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,CAWlC,CAAC;CACL"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CommandError, ConnectionError, layerConnection, pooledConnection, } from "@redfx/core";
|
|
2
|
+
import { RedisClient } from "bun";
|
|
3
|
+
import { Effect, Layer, Stream, } from "effect";
|
|
4
|
+
const textDecoder = new TextDecoder();
|
|
5
|
+
const toStr = (a) => typeof a === "string" ? a : textDecoder.decode(a);
|
|
6
|
+
const CONNECTION_CODES = new Set([
|
|
7
|
+
"ERR_REDIS_CONNECTION_CLOSED",
|
|
8
|
+
"ECONNREFUSED",
|
|
9
|
+
"ECONNRESET",
|
|
10
|
+
"ETIMEDOUT",
|
|
11
|
+
"ENOTFOUND",
|
|
12
|
+
]);
|
|
13
|
+
const mapError = (command, cause) => {
|
|
14
|
+
const err = cause;
|
|
15
|
+
const message = err.message ?? String(cause);
|
|
16
|
+
if ((err.code !== undefined && CONNECTION_CODES.has(err.code)) ||
|
|
17
|
+
/connection (is )?closed|connection refused|ERR_REDIS_CONNECTION_CLOSED/i.test(message)) {
|
|
18
|
+
return new ConnectionError({ message, cause });
|
|
19
|
+
}
|
|
20
|
+
return new CommandError({ message, command, code: err.code ?? message.split(" ")[0], cause });
|
|
21
|
+
};
|
|
22
|
+
const makeClient = (config) => Effect.acquireRelease(Effect.tryPromise({
|
|
23
|
+
try: async () => {
|
|
24
|
+
const client = new RedisClient(config.url, config.options);
|
|
25
|
+
await client.connect();
|
|
26
|
+
return client;
|
|
27
|
+
},
|
|
28
|
+
catch: (cause) => new ConnectionError({ message: "bun: failed to connect", cause }),
|
|
29
|
+
}), (client) => Effect.sync(() => client.close()));
|
|
30
|
+
const send = (client) => (command) => Effect.tryPromise({
|
|
31
|
+
try: () => client.send(command.name, command.args.map(toStr)),
|
|
32
|
+
catch: (cause) => mapError(command.name, cause),
|
|
33
|
+
});
|
|
34
|
+
// Bun auto-pipelines concurrent sends, so Promise.all is a real pipeline on the wire (order preserved).
|
|
35
|
+
const pipeline = (client) => (commands) => Effect.tryPromise({
|
|
36
|
+
try: () => Promise.all(commands.map((c) => client.send(c.name, c.args.map(toStr)))),
|
|
37
|
+
catch: (cause) => mapError("PIPELINE", cause),
|
|
38
|
+
});
|
|
39
|
+
const subscribeStream = (config) => (channels) => Stream.asyncScoped((emit) => Effect.gen(function* () {
|
|
40
|
+
const sub = yield* makeClient(config);
|
|
41
|
+
// Surface an unexpected drop; the finalizer (runs before our own close) suppresses teardown.
|
|
42
|
+
let active = true;
|
|
43
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
44
|
+
active = false;
|
|
45
|
+
}));
|
|
46
|
+
sub.onclose = (cause) => {
|
|
47
|
+
if (active)
|
|
48
|
+
emit.fail(new ConnectionError({ message: "bun: subscriber connection closed", cause }));
|
|
49
|
+
};
|
|
50
|
+
const listener = (message, channel) => emit.single({ channel, message });
|
|
51
|
+
yield* Effect.forEach(channels, (channel) => Effect.tryPromise({
|
|
52
|
+
try: () => sub.subscribe(channel, listener),
|
|
53
|
+
catch: (cause) => mapError("SUBSCRIBE", cause),
|
|
54
|
+
}));
|
|
55
|
+
}));
|
|
56
|
+
// redfx owns reconnection here (pool invalidation / Stream.retry), so disable bun's: otherwise a
|
|
57
|
+
// killed connection silently queues/resends and hangs in-flight commands instead of failing fast.
|
|
58
|
+
const failFastConfig = (config) => ({
|
|
59
|
+
...config,
|
|
60
|
+
options: { ...config.options, autoReconnect: false, enableOfflineQueue: false },
|
|
61
|
+
});
|
|
62
|
+
const dedicatedStream = (config) => (f) => Stream.unwrapScoped(Effect.map(makeClient(failFastConfig(config)), (client) => f({
|
|
63
|
+
send: send(client),
|
|
64
|
+
pipeline: pipeline(client),
|
|
65
|
+
subscribe: subscribeStream(config),
|
|
66
|
+
dedicated: dedicatedStream(config),
|
|
67
|
+
close: Effect.sync(() => client.close()),
|
|
68
|
+
})));
|
|
69
|
+
const makeConnection = (config) => Effect.gen(function* () {
|
|
70
|
+
const client = yield* makeClient(config);
|
|
71
|
+
return {
|
|
72
|
+
send: send(client),
|
|
73
|
+
pipeline: pipeline(client),
|
|
74
|
+
subscribe: subscribeStream(config),
|
|
75
|
+
dedicated: dedicatedStream(config),
|
|
76
|
+
close: Effect.sync(() => client.close()),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
export var BunRedis;
|
|
80
|
+
(function (BunRedis) {
|
|
81
|
+
BunRedis.layer = (config = {}) => layerConnection(makeConnection(config), { commandTimeout: config.commandTimeout });
|
|
82
|
+
BunRedis.layerConfig = (url, config) => Layer.unwrapEffect(Effect.map(url, (resolved) => BunRedis.layer({ ...config, url: resolved })));
|
|
83
|
+
/** Pools `size` command connections; pub/sub uses a dedicated connection. */
|
|
84
|
+
BunRedis.layerPooled = (config) => layerConnection(pooledConnection(makeConnection(failFastConfig(config)), config.size, subscribeStream(config), dedicatedStream(config)), {
|
|
85
|
+
commandTimeout: config.commandTimeout,
|
|
86
|
+
});
|
|
87
|
+
})(BunRedis || (BunRedis = {}));
|
|
88
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@redfx/bun",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bun driver adapter for redfx: the send-level port over Bun's native RedisClient, with pub/sub as a Stream.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Alex Walker",
|
|
7
|
+
"homepage": "https://github.com/al3xanderwalker/redfx",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/al3xanderwalker/redfx.git",
|
|
11
|
+
"directory": "packages/bun"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"effect",
|
|
15
|
+
"redis",
|
|
16
|
+
"bun",
|
|
17
|
+
"effect-ts"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"bun": "./src/index.ts",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"effect": "^3.21.0",
|
|
37
|
+
"@redfx/core": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "^1.2.0",
|
|
41
|
+
"effect": "^3.21.3",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"@redfx/core": "1.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.json",
|
|
50
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CommandError,
|
|
3
|
+
ConnectionError,
|
|
4
|
+
type ConnectionService,
|
|
5
|
+
layerConnection,
|
|
6
|
+
type PushMessage,
|
|
7
|
+
pooledConnection,
|
|
8
|
+
type Redis,
|
|
9
|
+
type RedisCommand,
|
|
10
|
+
type RedisError,
|
|
11
|
+
type RespValue,
|
|
12
|
+
} from "@redfx/core";
|
|
13
|
+
import { RedisClient } from "bun";
|
|
14
|
+
import {
|
|
15
|
+
type Config,
|
|
16
|
+
type ConfigError,
|
|
17
|
+
type Duration,
|
|
18
|
+
Effect,
|
|
19
|
+
Layer,
|
|
20
|
+
type Scope,
|
|
21
|
+
Stream,
|
|
22
|
+
} from "effect";
|
|
23
|
+
|
|
24
|
+
type BunRedisOptions = ConstructorParameters<typeof RedisClient>[1];
|
|
25
|
+
|
|
26
|
+
export interface ClientConfig {
|
|
27
|
+
readonly url?: string;
|
|
28
|
+
readonly options?: BunRedisOptions;
|
|
29
|
+
/** Effect-level per-command deadline; on expiry the command fails with `TimeoutError`. */
|
|
30
|
+
readonly commandTimeout?: Duration.DurationInput;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const textDecoder = new TextDecoder();
|
|
34
|
+
const toStr = (a: string | Uint8Array): string =>
|
|
35
|
+
typeof a === "string" ? a : textDecoder.decode(a);
|
|
36
|
+
|
|
37
|
+
const CONNECTION_CODES = new Set([
|
|
38
|
+
"ERR_REDIS_CONNECTION_CLOSED",
|
|
39
|
+
"ECONNREFUSED",
|
|
40
|
+
"ECONNRESET",
|
|
41
|
+
"ETIMEDOUT",
|
|
42
|
+
"ENOTFOUND",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const mapError = (command: string, cause: unknown): RedisError => {
|
|
46
|
+
const err = cause as { code?: string; message?: string };
|
|
47
|
+
const message = err.message ?? String(cause);
|
|
48
|
+
if (
|
|
49
|
+
(err.code !== undefined && CONNECTION_CODES.has(err.code)) ||
|
|
50
|
+
/connection (is )?closed|connection refused|ERR_REDIS_CONNECTION_CLOSED/i.test(message)
|
|
51
|
+
) {
|
|
52
|
+
return new ConnectionError({ message, cause });
|
|
53
|
+
}
|
|
54
|
+
return new CommandError({ message, command, code: err.code ?? message.split(" ")[0], cause });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const makeClient = (
|
|
58
|
+
config: ClientConfig,
|
|
59
|
+
): Effect.Effect<RedisClient, ConnectionError, Scope.Scope> =>
|
|
60
|
+
Effect.acquireRelease(
|
|
61
|
+
Effect.tryPromise({
|
|
62
|
+
try: async () => {
|
|
63
|
+
const client = new RedisClient(config.url, config.options);
|
|
64
|
+
await client.connect();
|
|
65
|
+
return client;
|
|
66
|
+
},
|
|
67
|
+
catch: (cause) => new ConnectionError({ message: "bun: failed to connect", cause }),
|
|
68
|
+
}),
|
|
69
|
+
(client) => Effect.sync(() => client.close()),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const send =
|
|
73
|
+
(client: RedisClient) =>
|
|
74
|
+
(command: RedisCommand): Effect.Effect<RespValue, RedisError> =>
|
|
75
|
+
Effect.tryPromise({
|
|
76
|
+
try: () =>
|
|
77
|
+
client.send(command.name, command.args.map(toStr)) as unknown as Promise<RespValue>,
|
|
78
|
+
catch: (cause) => mapError(command.name, cause),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Bun auto-pipelines concurrent sends, so Promise.all is a real pipeline on the wire (order preserved).
|
|
82
|
+
const pipeline =
|
|
83
|
+
(client: RedisClient) =>
|
|
84
|
+
(commands: ReadonlyArray<RedisCommand>): Effect.Effect<ReadonlyArray<RespValue>, RedisError> =>
|
|
85
|
+
Effect.tryPromise({
|
|
86
|
+
try: () =>
|
|
87
|
+
Promise.all(
|
|
88
|
+
commands.map((c) => client.send(c.name, c.args.map(toStr))),
|
|
89
|
+
) as unknown as Promise<ReadonlyArray<RespValue>>,
|
|
90
|
+
catch: (cause) => mapError("PIPELINE", cause),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const subscribeStream =
|
|
94
|
+
(config: ClientConfig) =>
|
|
95
|
+
(channels: ReadonlyArray<string>): Stream.Stream<PushMessage, RedisError> =>
|
|
96
|
+
Stream.asyncScoped<PushMessage, RedisError>((emit) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const sub = yield* makeClient(config);
|
|
99
|
+
// Surface an unexpected drop; the finalizer (runs before our own close) suppresses teardown.
|
|
100
|
+
let active = true;
|
|
101
|
+
yield* Effect.addFinalizer(() =>
|
|
102
|
+
Effect.sync(() => {
|
|
103
|
+
active = false;
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
sub.onclose = (cause) => {
|
|
107
|
+
if (active)
|
|
108
|
+
emit.fail(new ConnectionError({ message: "bun: subscriber connection closed", cause }));
|
|
109
|
+
};
|
|
110
|
+
const listener = (message: string, channel: string) => emit.single({ channel, message });
|
|
111
|
+
yield* Effect.forEach(channels, (channel) =>
|
|
112
|
+
Effect.tryPromise({
|
|
113
|
+
try: () => sub.subscribe(channel, listener),
|
|
114
|
+
catch: (cause) => mapError("SUBSCRIBE", cause),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// redfx owns reconnection here (pool invalidation / Stream.retry), so disable bun's: otherwise a
|
|
121
|
+
// killed connection silently queues/resends and hangs in-flight commands instead of failing fast.
|
|
122
|
+
const failFastConfig = (config: ClientConfig): ClientConfig => ({
|
|
123
|
+
...config,
|
|
124
|
+
options: { ...config.options, autoReconnect: false, enableOfflineQueue: false },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const dedicatedStream =
|
|
128
|
+
(config: ClientConfig): ConnectionService["dedicated"] =>
|
|
129
|
+
(f) =>
|
|
130
|
+
Stream.unwrapScoped(
|
|
131
|
+
Effect.map(makeClient(failFastConfig(config)), (client) =>
|
|
132
|
+
f({
|
|
133
|
+
send: send(client),
|
|
134
|
+
pipeline: pipeline(client),
|
|
135
|
+
subscribe: subscribeStream(config),
|
|
136
|
+
dedicated: dedicatedStream(config),
|
|
137
|
+
close: Effect.sync(() => client.close()),
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const makeConnection = (
|
|
143
|
+
config: ClientConfig,
|
|
144
|
+
): Effect.Effect<ConnectionService, ConnectionError, Scope.Scope> =>
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const client = yield* makeClient(config);
|
|
147
|
+
return {
|
|
148
|
+
send: send(client),
|
|
149
|
+
pipeline: pipeline(client),
|
|
150
|
+
subscribe: subscribeStream(config),
|
|
151
|
+
dedicated: dedicatedStream(config),
|
|
152
|
+
close: Effect.sync(() => client.close()),
|
|
153
|
+
} satisfies ConnectionService;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export namespace BunRedis {
|
|
157
|
+
export const layer = (config: ClientConfig = {}): Layer.Layer<Redis, ConnectionError> =>
|
|
158
|
+
layerConnection(makeConnection(config), { commandTimeout: config.commandTimeout });
|
|
159
|
+
|
|
160
|
+
export const layerConfig = (
|
|
161
|
+
url: Config.Config<string>,
|
|
162
|
+
config?: Omit<ClientConfig, "url">,
|
|
163
|
+
): Layer.Layer<Redis, ConnectionError | ConfigError.ConfigError> =>
|
|
164
|
+
Layer.unwrapEffect(Effect.map(url, (resolved) => layer({ ...config, url: resolved })));
|
|
165
|
+
|
|
166
|
+
/** Pools `size` command connections; pub/sub uses a dedicated connection. */
|
|
167
|
+
export const layerPooled = (
|
|
168
|
+
config: ClientConfig & { readonly size: number },
|
|
169
|
+
): Layer.Layer<Redis, ConnectionError> =>
|
|
170
|
+
layerConnection(
|
|
171
|
+
pooledConnection(
|
|
172
|
+
makeConnection(failFastConfig(config)),
|
|
173
|
+
config.size,
|
|
174
|
+
subscribeStream(config),
|
|
175
|
+
dedicatedStream(config),
|
|
176
|
+
),
|
|
177
|
+
{
|
|
178
|
+
commandTimeout: config.commandTimeout,
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
}
|