@unikvs/redis.node 0.0.0 → 0.0.13
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 +1 -43
- package/dist/index.d.ts +134 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -8
- package/src/errors.ts +161 -0
- package/src/index.ts +19 -0
- package/src/redis.ts +392 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present, tai-kun
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,45 +1,3 @@
|
|
|
1
1
|
# @unikvs/redis.node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
|
|
6
|
-
|
|
7
|
-
This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
This package exists to:
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@unikvs/redis.node`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
15
|
-
|
|
16
|
-
## What is OIDC Trusted Publishing?
|
|
17
|
-
|
|
18
|
-
OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
|
|
19
|
-
|
|
20
|
-
## Setup Instructions
|
|
21
|
-
|
|
22
|
-
To properly configure OIDC trusted publishing for this package:
|
|
23
|
-
|
|
24
|
-
1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
28
|
-
|
|
29
|
-
## DO NOT USE THIS PACKAGE
|
|
30
|
-
|
|
31
|
-
This package is a placeholder for OIDC configuration only. It:
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
36
|
-
|
|
37
|
-
## More Information
|
|
38
|
-
|
|
39
|
-
For more details about npm's trusted publishing feature, see:
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
**Maintained for OIDC setup purposes only**
|
|
3
|
+
[Document](https://tai-kun.github.io/unikvs/)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ErrorBase, ErrorOptions, IStorage, InvalidUsageErrorBase, KeyNotFoundError, KeyNotFoundErrorArgs, KeyNotFoundErrorMeta } from "@unikvs/core";
|
|
2
|
+
import { ClusterNode, ClusterOptions, RedisOptions } from "ioredis";
|
|
3
|
+
//#region ../src/errors.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)
|
|
6
|
+
*/
|
|
7
|
+
export declare class ClusterNotSupportedError extends InvalidUsageErrorBase<undefined> {
|
|
8
|
+
/**
|
|
9
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)
|
|
10
|
+
*/
|
|
11
|
+
constructor(options?: ErrorOptions);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
15
|
+
*/
|
|
16
|
+
type InvalidCloseTimeoutErrorMeta = {
|
|
17
|
+
/**
|
|
18
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#invalid-close-timeout-actual)
|
|
19
|
+
*/
|
|
20
|
+
readonly actual: unknown;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
24
|
+
*/
|
|
25
|
+
type InvalidCloseTimeoutErrorArgs = ErrorOptions & InvalidCloseTimeoutErrorMeta;
|
|
26
|
+
/**
|
|
27
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
28
|
+
*/
|
|
29
|
+
export declare class InvalidCloseTimeoutError extends ErrorBase<InvalidCloseTimeoutErrorMeta> {
|
|
30
|
+
/**
|
|
31
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
32
|
+
*/
|
|
33
|
+
constructor(args: InvalidCloseTimeoutErrorArgs);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
37
|
+
*/
|
|
38
|
+
type ConnectTimeoutErrorMeta = {
|
|
39
|
+
/**
|
|
40
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#connect-timeout-timeoutms)
|
|
41
|
+
*/
|
|
42
|
+
readonly timeoutMs: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
46
|
+
*/
|
|
47
|
+
type ConnectTimeoutErrorArgs = ErrorOptions & ConnectTimeoutErrorMeta;
|
|
48
|
+
/**
|
|
49
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
50
|
+
*/
|
|
51
|
+
export declare class ConnectTimeoutError extends ErrorBase<ConnectTimeoutErrorMeta> {
|
|
52
|
+
/**
|
|
53
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
54
|
+
*/
|
|
55
|
+
constructor(args: ConnectTimeoutErrorArgs);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
59
|
+
*/
|
|
60
|
+
type CloseTimeoutErrorMeta = {
|
|
61
|
+
/**
|
|
62
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#close-timeout-timeoutms)
|
|
63
|
+
*/
|
|
64
|
+
readonly timeoutMs: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
68
|
+
*/
|
|
69
|
+
type CloseTimeoutErrorArgs = ErrorOptions & CloseTimeoutErrorMeta;
|
|
70
|
+
/**
|
|
71
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
72
|
+
*/
|
|
73
|
+
export declare class CloseTimeoutError extends ErrorBase<CloseTimeoutErrorMeta> {
|
|
74
|
+
/**
|
|
75
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
76
|
+
*/
|
|
77
|
+
constructor(args: CloseTimeoutErrorArgs);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region ../src/redis.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec)
|
|
83
|
+
*/
|
|
84
|
+
type RedisClusterSpec = {
|
|
85
|
+
/**
|
|
86
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec-nodes)
|
|
87
|
+
*/
|
|
88
|
+
readonly nodes: readonly ClusterNode[];
|
|
89
|
+
/**
|
|
90
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec-options)
|
|
91
|
+
*/
|
|
92
|
+
readonly options?: ClusterOptions | undefined;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions)
|
|
96
|
+
*/
|
|
97
|
+
type RedisStorageOptions = Omit<RedisOptions, "keyPrefix" | "lazyConnect"> & {
|
|
98
|
+
/**
|
|
99
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-keyprefix)
|
|
100
|
+
*/
|
|
101
|
+
readonly keyPrefix?: string | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-cluster)
|
|
104
|
+
*/
|
|
105
|
+
readonly cluster?: RedisClusterSpec | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-closetimeout)
|
|
108
|
+
*/
|
|
109
|
+
readonly closeTimeout?: number | undefined;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
|
|
113
|
+
*/
|
|
114
|
+
declare class Redis implements IStorage {
|
|
115
|
+
#private;
|
|
116
|
+
readonly name: string;
|
|
117
|
+
/**
|
|
118
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
|
|
119
|
+
*/
|
|
120
|
+
constructor(url?: string, options?: RedisStorageOptions);
|
|
121
|
+
get isOpen(): boolean;
|
|
122
|
+
open(_c: IStorage.OpenContext): Promise<void>;
|
|
123
|
+
close(_c: IStorage.CloseContext): Promise<void>;
|
|
124
|
+
write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void>;
|
|
125
|
+
read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>>;
|
|
126
|
+
exists(c: IStorage.ExistsContext): Promise<boolean>;
|
|
127
|
+
delete(c: IStorage.DeleteContext): Promise<void>;
|
|
128
|
+
clear(c: IStorage.ClearContext): Promise<void>;
|
|
129
|
+
getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>>;
|
|
130
|
+
getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { type CloseTimeoutErrorArgs, type CloseTimeoutErrorMeta, type ConnectTimeoutErrorArgs, type ConnectTimeoutErrorMeta, type InvalidCloseTimeoutErrorArgs, type InvalidCloseTimeoutErrorMeta, KeyNotFoundError, type KeyNotFoundErrorArgs, type KeyNotFoundErrorMeta, Redis, type RedisClusterSpec, type RedisStorageOptions };
|
|
134
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/redis.ts"],"mappings":";;;;;;qBAQa,iCAAiC;;;;EAQ5C,YAAmB,UAAU;;;;;KAiBnB;;;;WAID;;;;;KAMC,+BAA+B,eAAe;;;;qBAK7C,iCAAiC,UAAU;;;;EAQtD,YAAmB,MAAM;;;;;KAsBf;;;;WAID;;;;;KAMC,0BAA0B,eAAe;;;;qBAKxC,4BAA4B,UAAU;;;;EAQjD,YAAmB,MAAM;;;;;KAqBf;;;;WAID;;;;;KAMC,wBAAwB,eAAe;;;;qBAKtC,0BAA0B,UAAU;;;;EAQ/C,YAAmB,MAAM;;;;;;;KC7Hf;;;;WAID,gBAAgB;;;;WAKhB,UAAU;;;;;KAMT,sBAAsB,KAAK;;;;WAI5B;;;;WAKA,UAAU;;;;WAKV;;;;;cAYU,iBAAiB;;WAepB;;;;EAKhB,YAAmB,cAAc,UAAS;MA4B/B;EAgBX,KAAkB,IAAI,SAAS,cAAc;EA+D7C,MAAmB,IAAI,SAAS,eAAe;EAkD/C,MAAmB,GAAG,SAAS,cAAc,MAAM,WAAW,eAAe;EAQ7E,KAAkB,GAAG,SAAS,cAAc,QAAQ,WAAW;EAc/D,OAAoB,GAAG,SAAS,gBAAgB;EAQhD,OAAoB,GAAG,SAAS,gBAAgB;EAQhD,MAAmB,GAAG,SAAS,eAAe;EAU9C,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW;EA0E9E,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { ErrorBase, InvalidUsageErrorBase, KeyNotFoundError, KeyNotFoundError as KeyNotFoundError$1, setErrorMessage } from "@unikvs/core";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { Redis as Redis$1 } from "ioredis";
|
|
4
|
+
//#region ../src/errors.ts
|
|
5
|
+
var _ClusterNotSupportedError;
|
|
6
|
+
var _InvalidCloseTimeoutError;
|
|
7
|
+
var _ConnectTimeoutError;
|
|
8
|
+
var _CloseTimeoutError;
|
|
9
|
+
/**
|
|
10
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)
|
|
11
|
+
*/
|
|
12
|
+
var ClusterNotSupportedError = class extends InvalidUsageErrorBase {
|
|
13
|
+
/**
|
|
14
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)
|
|
15
|
+
*/
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super("Redis storage v1 supports standalone mode only: the `cluster` option is not supported.", options);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
_ClusterNotSupportedError = ClusterNotSupportedError;
|
|
21
|
+
_ClusterNotSupportedError.prototype.name = "RedisClusterNotSupportedError";
|
|
22
|
+
setErrorMessage(ClusterNotSupportedError, "Redis ストレージ v1 は standalone モードのみに対応します。`cluster` オプションはサポートされていません。", "ja");
|
|
23
|
+
/**
|
|
24
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
25
|
+
*/
|
|
26
|
+
var InvalidCloseTimeoutError = class extends ErrorBase {
|
|
27
|
+
/**
|
|
28
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
29
|
+
*/
|
|
30
|
+
constructor(args) {
|
|
31
|
+
const { actual, ...options } = args;
|
|
32
|
+
super({ actual }, ({ actual: messageActual }) => `\`closeTimeout\` must be a finite positive number (ms), but got ${String(messageActual)}.`, options);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
_InvalidCloseTimeoutError = InvalidCloseTimeoutError;
|
|
36
|
+
_InvalidCloseTimeoutError.prototype.name = "RedisInvalidCloseTimeoutError";
|
|
37
|
+
setErrorMessage(InvalidCloseTimeoutError, ({ actual }) => `\`closeTimeout\` は有限の正数 (ミリ秒) でなければなりません。受け取った値: ${String(actual)}`, "ja");
|
|
38
|
+
/**
|
|
39
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
40
|
+
*/
|
|
41
|
+
var ConnectTimeoutError = class extends ErrorBase {
|
|
42
|
+
/**
|
|
43
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
44
|
+
*/
|
|
45
|
+
constructor(args) {
|
|
46
|
+
const { timeoutMs, ...options } = args;
|
|
47
|
+
super({ timeoutMs }, ({ timeoutMs: messageTimeoutMs }) => `Timed out connecting to Redis after ${messageTimeoutMs}ms`, options);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
_ConnectTimeoutError = ConnectTimeoutError;
|
|
51
|
+
_ConnectTimeoutError.prototype.name = "RedisConnectTimeoutError";
|
|
52
|
+
setErrorMessage(ConnectTimeoutError, ({ timeoutMs }) => `Redis への接続が ${timeoutMs}ms でタイムアウトしました`, "ja");
|
|
53
|
+
/**
|
|
54
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
55
|
+
*/
|
|
56
|
+
var CloseTimeoutError = class extends ErrorBase {
|
|
57
|
+
/**
|
|
58
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
59
|
+
*/
|
|
60
|
+
constructor(args) {
|
|
61
|
+
const { timeoutMs, ...options } = args;
|
|
62
|
+
super({ timeoutMs }, ({ timeoutMs: messageTimeoutMs }) => `Timed out closing Redis after ${messageTimeoutMs}ms`, options);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
_CloseTimeoutError = CloseTimeoutError;
|
|
66
|
+
_CloseTimeoutError.prototype.name = "RedisCloseTimeoutError";
|
|
67
|
+
setErrorMessage(CloseTimeoutError, ({ timeoutMs }) => `Redis からの切断処理が ${timeoutMs}ms でタイムアウトしました`, "ja");
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/checkPrivateRedeclaration.js
|
|
70
|
+
function _checkPrivateRedeclaration(e, t) {
|
|
71
|
+
if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateMethodInitSpec.js
|
|
75
|
+
function _classPrivateMethodInitSpec(e, a) {
|
|
76
|
+
_checkPrivateRedeclaration(e, a), a.add(e);
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldInitSpec.js
|
|
80
|
+
function _classPrivateFieldInitSpec(e, t, a) {
|
|
81
|
+
_checkPrivateRedeclaration(e, t), t.set(e, a);
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/assertClassBrand.js
|
|
85
|
+
function _assertClassBrand(e, t, n) {
|
|
86
|
+
if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
|
|
87
|
+
throw new TypeError("Private element is not present on this object");
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldSet2.js
|
|
91
|
+
function _classPrivateFieldSet2(s, a, r) {
|
|
92
|
+
return s.set(_assertClassBrand(s, a), r), r;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldGet2.js
|
|
96
|
+
function _classPrivateFieldGet2(s, a) {
|
|
97
|
+
return s.get(_assertClassBrand(s, a));
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region ../src/redis.ts
|
|
101
|
+
const SCAN_COUNT = 1e3;
|
|
102
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
|
|
103
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
|
|
104
|
+
var _con = /* @__PURE__ */ new WeakMap();
|
|
105
|
+
var _url = /* @__PURE__ */ new WeakMap();
|
|
106
|
+
var _options = /* @__PURE__ */ new WeakMap();
|
|
107
|
+
var _closeTimeout = /* @__PURE__ */ new WeakMap();
|
|
108
|
+
var _keyPrefix = /* @__PURE__ */ new WeakMap();
|
|
109
|
+
var _Redis_brand = /* @__PURE__ */ new WeakSet();
|
|
110
|
+
/**
|
|
111
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
|
|
112
|
+
*/
|
|
113
|
+
var Redis = class {
|
|
114
|
+
/**
|
|
115
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
|
|
116
|
+
*/
|
|
117
|
+
constructor(url, options = {}) {
|
|
118
|
+
_classPrivateMethodInitSpec(this, _Redis_brand);
|
|
119
|
+
_classPrivateFieldInitSpec(this, _con, void 0);
|
|
120
|
+
_classPrivateFieldInitSpec(this, _url, void 0);
|
|
121
|
+
_classPrivateFieldInitSpec(this, _options, void 0);
|
|
122
|
+
_classPrivateFieldInitSpec(this, _closeTimeout, void 0);
|
|
123
|
+
_classPrivateFieldInitSpec(this, _keyPrefix, void 0);
|
|
124
|
+
const { cluster, keyPrefix = "unikvs:", closeTimeout = DEFAULT_CLOSE_TIMEOUT_MS, ...redisOptions } = options;
|
|
125
|
+
if (cluster !== void 0) throw new ClusterNotSupportedError();
|
|
126
|
+
if (typeof closeTimeout !== "number" || !Number.isFinite(closeTimeout) || closeTimeout <= 0) throw new InvalidCloseTimeoutError({ actual: closeTimeout });
|
|
127
|
+
this.name = "Redis";
|
|
128
|
+
_classPrivateFieldSet2(_con, this, null);
|
|
129
|
+
_classPrivateFieldSet2(_url, this, url);
|
|
130
|
+
_classPrivateFieldSet2(_options, this, redisOptions);
|
|
131
|
+
_classPrivateFieldSet2(_closeTimeout, this, closeTimeout);
|
|
132
|
+
_classPrivateFieldSet2(_keyPrefix, this, keyPrefix);
|
|
133
|
+
}
|
|
134
|
+
get isOpen() {
|
|
135
|
+
return _classPrivateFieldGet2(_con, this) !== null;
|
|
136
|
+
}
|
|
137
|
+
async open(_c) {
|
|
138
|
+
if (_classPrivateFieldGet2(_con, this)) await this.close(_c);
|
|
139
|
+
const explicitUrl = _classPrivateFieldGet2(_url, this) === void 0 || _classPrivateFieldGet2(_url, this) === "" ? void 0 : _classPrivateFieldGet2(_url, this);
|
|
140
|
+
const redisUrl = process.env["REDIS_URL"] === void 0 || process.env["REDIS_URL"] === "" ? void 0 : process.env["REDIS_URL"];
|
|
141
|
+
const valkeyUrl = process.env["VALKEY_URL"] === void 0 || process.env["VALKEY_URL"] === "" ? void 0 : process.env["VALKEY_URL"];
|
|
142
|
+
const url = explicitUrl ?? redisUrl ?? valkeyUrl;
|
|
143
|
+
const connectTimeout = _classPrivateFieldGet2(_options, this).connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
144
|
+
const options = {
|
|
145
|
+
..._classPrivateFieldGet2(_options, this),
|
|
146
|
+
lazyConnect: true,
|
|
147
|
+
connectTimeout
|
|
148
|
+
};
|
|
149
|
+
const raw = url === void 0 ? new Redis$1(options) : new Redis$1(url, options);
|
|
150
|
+
raw.on("error", () => {});
|
|
151
|
+
let timer;
|
|
152
|
+
try {
|
|
153
|
+
await Promise.race([raw.connect(), new Promise((_, reject) => {
|
|
154
|
+
timer = setTimeout(() => {
|
|
155
|
+
reject(new ConnectTimeoutError({ timeoutMs: connectTimeout }));
|
|
156
|
+
}, connectTimeout);
|
|
157
|
+
})]);
|
|
158
|
+
} catch (ex) {
|
|
159
|
+
raw.disconnect();
|
|
160
|
+
throw ex;
|
|
161
|
+
} finally {
|
|
162
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
_classPrivateFieldSet2(_con, this, { client: raw });
|
|
165
|
+
}
|
|
166
|
+
async close(_c) {
|
|
167
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
168
|
+
_classPrivateFieldSet2(_con, this, null);
|
|
169
|
+
const closeTimeout = _classPrivateFieldGet2(_closeTimeout, this);
|
|
170
|
+
let timer;
|
|
171
|
+
try {
|
|
172
|
+
await Promise.race([client.quit(), new Promise((_, reject) => {
|
|
173
|
+
timer = setTimeout(() => {
|
|
174
|
+
reject(new CloseTimeoutError({ timeoutMs: closeTimeout }));
|
|
175
|
+
}, closeTimeout);
|
|
176
|
+
})]);
|
|
177
|
+
} catch {
|
|
178
|
+
client.disconnect();
|
|
179
|
+
} finally {
|
|
180
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async write(c, data) {
|
|
184
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
185
|
+
c.signal.throwIfAborted();
|
|
186
|
+
await client.set(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key), Buffer.from(data));
|
|
187
|
+
}
|
|
188
|
+
async read(c) {
|
|
189
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
190
|
+
c.signal.throwIfAborted();
|
|
191
|
+
const data = await client.getBuffer(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
|
|
192
|
+
if (data === null) throw new KeyNotFoundError$1({ key: c.key });
|
|
193
|
+
return new Uint8Array(data);
|
|
194
|
+
}
|
|
195
|
+
async exists(c) {
|
|
196
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
197
|
+
c.signal.throwIfAborted();
|
|
198
|
+
return await client.exists(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key)) > 0;
|
|
199
|
+
}
|
|
200
|
+
async delete(c) {
|
|
201
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
202
|
+
c.signal.throwIfAborted();
|
|
203
|
+
await client.del(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
|
|
204
|
+
}
|
|
205
|
+
async clear(c) {
|
|
206
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
207
|
+
c.signal.throwIfAborted();
|
|
208
|
+
const pattern = `${_classPrivateFieldGet2(_keyPrefix, this)}*`;
|
|
209
|
+
await _assertClassBrand(_Redis_brand, this, _clearPattern).call(this, client, pattern, c.signal);
|
|
210
|
+
}
|
|
211
|
+
getWritable(c) {
|
|
212
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
213
|
+
c.signal.throwIfAborted();
|
|
214
|
+
const dest = _assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key);
|
|
215
|
+
const tmp = `${dest}.${randomUUID()}.tmp`;
|
|
216
|
+
let finished = false;
|
|
217
|
+
const removeAbortListener = () => {
|
|
218
|
+
c.signal.removeEventListener("abort", onAbort);
|
|
219
|
+
};
|
|
220
|
+
const discardTemporary = async () => {
|
|
221
|
+
try {
|
|
222
|
+
await client.del(tmp);
|
|
223
|
+
} catch {}
|
|
224
|
+
};
|
|
225
|
+
const finish = async () => {
|
|
226
|
+
if (finished) return;
|
|
227
|
+
finished = true;
|
|
228
|
+
removeAbortListener();
|
|
229
|
+
await discardTemporary();
|
|
230
|
+
};
|
|
231
|
+
const onAbort = () => {
|
|
232
|
+
finish();
|
|
233
|
+
};
|
|
234
|
+
c.signal.addEventListener("abort", onAbort, { once: true });
|
|
235
|
+
return new WritableStream({
|
|
236
|
+
async abort() {
|
|
237
|
+
await finish();
|
|
238
|
+
},
|
|
239
|
+
async close() {
|
|
240
|
+
c.signal.throwIfAborted();
|
|
241
|
+
try {
|
|
242
|
+
await client.rename(tmp, dest);
|
|
243
|
+
} catch (ex) {
|
|
244
|
+
await finish();
|
|
245
|
+
throw ex;
|
|
246
|
+
}
|
|
247
|
+
finished = true;
|
|
248
|
+
removeAbortListener();
|
|
249
|
+
},
|
|
250
|
+
async start() {
|
|
251
|
+
try {
|
|
252
|
+
c.signal.throwIfAborted();
|
|
253
|
+
await client.set(tmp, "");
|
|
254
|
+
} catch (ex) {
|
|
255
|
+
await finish();
|
|
256
|
+
throw ex;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async write(chunk) {
|
|
260
|
+
c.signal.throwIfAborted();
|
|
261
|
+
try {
|
|
262
|
+
await client.append(tmp, Buffer.from(chunk));
|
|
263
|
+
} catch (ex) {
|
|
264
|
+
await finish();
|
|
265
|
+
throw ex;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
getReadable(c) {
|
|
271
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
272
|
+
c.signal.throwIfAborted();
|
|
273
|
+
const dest = _assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key);
|
|
274
|
+
let sent = false;
|
|
275
|
+
return new ReadableStream({ async pull(controller) {
|
|
276
|
+
if (sent) {
|
|
277
|
+
controller.close();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
sent = true;
|
|
281
|
+
c.signal.throwIfAborted();
|
|
282
|
+
const data = await client.getBuffer(dest);
|
|
283
|
+
if (data === null) throw new KeyNotFoundError$1({ key: c.key });
|
|
284
|
+
controller.enqueue(new Uint8Array(data));
|
|
285
|
+
controller.close();
|
|
286
|
+
} });
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function _getConnection() {
|
|
290
|
+
const con = _classPrivateFieldGet2(_con, this);
|
|
291
|
+
if (con === null) throw new TypeError("Redis is not open. Call open() first.");
|
|
292
|
+
return con;
|
|
293
|
+
}
|
|
294
|
+
function _toKey(key) {
|
|
295
|
+
return `${_classPrivateFieldGet2(_keyPrefix, this)}${key}`;
|
|
296
|
+
}
|
|
297
|
+
async function _clearPattern(client, pattern, signal) {
|
|
298
|
+
let cursor = "0";
|
|
299
|
+
do {
|
|
300
|
+
signal.throwIfAborted();
|
|
301
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
|
|
302
|
+
if (keys.length > 0) await client.del(...keys);
|
|
303
|
+
cursor = nextCursor;
|
|
304
|
+
} while (cursor !== "0");
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
export { CloseTimeoutError, ClusterNotSupportedError, ConnectTimeoutError, InvalidCloseTimeoutError, KeyNotFoundError, Redis };
|
|
308
|
+
|
|
309
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["Ioredis","KeyNotFoundError"],"sources":["../src/errors.ts","../src/redis.ts"],"sourcesContent":["import { type ErrorOptions, ErrorBase, setErrorMessage, InvalidUsageErrorBase } from \"@unikvs/core\";\n\nexport type { KeyNotFoundErrorArgs, KeyNotFoundErrorMeta } from \"@unikvs/core\";\nexport { KeyNotFoundError } from \"@unikvs/core\";\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)\n */\nexport class ClusterNotSupportedError extends InvalidUsageErrorBase<undefined> {\n static {\n this.prototype.name = \"RedisClusterNotSupportedError\";\n }\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)\n */\n public constructor(options?: ErrorOptions) {\n super(\n \"Redis storage v1 supports standalone mode only: the `cluster` option is not supported.\",\n options,\n );\n }\n}\n\nsetErrorMessage(\n ClusterNotSupportedError,\n \"Redis ストレージ v1 は standalone モードのみに対応します。`cluster` オプションはサポートされていません。\",\n \"ja\",\n);\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)\n */\nexport type InvalidCloseTimeoutErrorMeta = {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#invalid-close-timeout-actual)\n */\n readonly actual: unknown;\n};\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)\n */\nexport type InvalidCloseTimeoutErrorArgs = ErrorOptions & InvalidCloseTimeoutErrorMeta;\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)\n */\nexport class InvalidCloseTimeoutError extends ErrorBase<InvalidCloseTimeoutErrorMeta> {\n static {\n this.prototype.name = \"RedisInvalidCloseTimeoutError\";\n }\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)\n */\n public constructor(args: InvalidCloseTimeoutErrorArgs) {\n const { actual, ...options } = args;\n const meta: InvalidCloseTimeoutErrorMeta = { actual };\n super(\n meta,\n ({ actual: messageActual }) =>\n `\\`closeTimeout\\` must be a finite positive number (ms), but got ${String(messageActual)}.`,\n options,\n );\n }\n}\n\nsetErrorMessage(\n InvalidCloseTimeoutError,\n ({ actual }) =>\n `\\`closeTimeout\\` は有限の正数 (ミリ秒) でなければなりません。受け取った値: ${String(actual)}`,\n \"ja\",\n);\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)\n */\nexport type ConnectTimeoutErrorMeta = {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#connect-timeout-timeoutms)\n */\n readonly timeoutMs: number;\n};\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)\n */\nexport type ConnectTimeoutErrorArgs = ErrorOptions & ConnectTimeoutErrorMeta;\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)\n */\nexport class ConnectTimeoutError extends ErrorBase<ConnectTimeoutErrorMeta> {\n static {\n this.prototype.name = \"RedisConnectTimeoutError\";\n }\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)\n */\n public constructor(args: ConnectTimeoutErrorArgs) {\n const { timeoutMs, ...options } = args;\n const meta: ConnectTimeoutErrorMeta = { timeoutMs };\n super(\n meta,\n ({ timeoutMs: messageTimeoutMs }) =>\n `Timed out connecting to Redis after ${messageTimeoutMs}ms`,\n options,\n );\n }\n}\n\nsetErrorMessage(\n ConnectTimeoutError,\n ({ timeoutMs }) => `Redis への接続が ${timeoutMs}ms でタイムアウトしました`,\n \"ja\",\n);\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)\n */\nexport type CloseTimeoutErrorMeta = {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#close-timeout-timeoutms)\n */\n readonly timeoutMs: number;\n};\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)\n */\nexport type CloseTimeoutErrorArgs = ErrorOptions & CloseTimeoutErrorMeta;\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)\n */\nexport class CloseTimeoutError extends ErrorBase<CloseTimeoutErrorMeta> {\n static {\n this.prototype.name = \"RedisCloseTimeoutError\";\n }\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)\n */\n public constructor(args: CloseTimeoutErrorArgs) {\n const { timeoutMs, ...options } = args;\n const meta: CloseTimeoutErrorMeta = { timeoutMs };\n super(\n meta,\n ({ timeoutMs: messageTimeoutMs }) => `Timed out closing Redis after ${messageTimeoutMs}ms`,\n options,\n );\n }\n}\n\nsetErrorMessage(\n CloseTimeoutError,\n ({ timeoutMs }) => `Redis からの切断処理が ${timeoutMs}ms でタイムアウトしました`,\n \"ja\",\n);\n","import { randomUUID } from \"node:crypto\";\n\nimport { type IStorage, KeyNotFoundError } from \"@unikvs/core\";\nimport {\n type ClusterNode,\n type RedisOptions,\n type ClusterOptions,\n Redis as Ioredis,\n} from \"ioredis\";\n\nimport {\n CloseTimeoutError,\n ConnectTimeoutError,\n ClusterNotSupportedError,\n InvalidCloseTimeoutError,\n} from \"./errors.js\";\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec)\n */\nexport type RedisClusterSpec = {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec-nodes)\n */\n readonly nodes: readonly ClusterNode[];\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec-options)\n */\n readonly options?: ClusterOptions | undefined;\n};\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions)\n */\nexport type RedisStorageOptions = Omit<RedisOptions, \"keyPrefix\" | \"lazyConnect\"> & {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-keyprefix)\n */\n readonly keyPrefix?: string | undefined;\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-cluster)\n */\n readonly cluster?: RedisClusterSpec | undefined;\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-closetimeout)\n */\n readonly closeTimeout?: number | undefined;\n};\n\nconst SCAN_COUNT = 1000;\nconst DEFAULT_CONNECT_TIMEOUT_MS = 5000;\nconst DEFAULT_CLOSE_TIMEOUT_MS = 5000;\n\ntype Connection = { readonly client: Ioredis };\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)\n */\nexport default class Redis implements IStorage {\n #con: Connection | null;\n\n readonly #url: string | undefined;\n\n // `closeTimeout` は本パッケージ固有の拡張です。\n // `this.#options` には含めず、別フィールドで保管します。\n // `this.#options` は `new Ioredis` にそのまま spread できる透過バッグです。\n readonly #options: Omit<RedisStorageOptions, \"keyPrefix\" | \"cluster\" | \"closeTimeout\">;\n\n // `ioredis` の `disconnectTimeout` (ソケット切断待機) とは別物です。\n readonly #closeTimeout: number;\n\n readonly #keyPrefix: string;\n\n public readonly name: string;\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)\n */\n public constructor(url?: string, options: RedisStorageOptions = {}) {\n const {\n cluster,\n keyPrefix = \"unikvs:\",\n closeTimeout = DEFAULT_CLOSE_TIMEOUT_MS,\n ...redisOptions\n } = options;\n\n // v1 は standalone のみに対応します。\n // `cluster` を指定したときは、`closeTimeout` 検証より先に拒否します。\n if (cluster !== undefined) {\n throw new ClusterNotSupportedError();\n }\n\n // `undefined` は既定値 5000ms になるため対象外とします。\n // 明示値が有限の正数でない場合のみ拒否します。\n if (typeof closeTimeout !== \"number\" || !Number.isFinite(closeTimeout) || closeTimeout <= 0) {\n throw new InvalidCloseTimeoutError({ actual: closeTimeout });\n }\n\n this.name = \"Redis\";\n this.#con = null;\n this.#url = url;\n this.#options = redisOptions;\n this.#closeTimeout = closeTimeout;\n this.#keyPrefix = keyPrefix;\n }\n\n public get isOpen(): boolean {\n return this.#con !== null;\n }\n\n // 開いている接続を返します。\n // 未オープン時は `TypeError` を投げます。\n #getConnection(): Connection {\n const con = this.#con;\n\n if (con === null) {\n throw new TypeError(\"Redis is not open. Call open() first.\");\n }\n\n return con;\n }\n\n public async open(_c: IStorage.OpenContext): Promise<void> {\n // 並行 `open` 同士および `open` 接続待ち中の `close` の同時呼び出しは禁止 (事前条件) です。\n // 呼び出し側で直列化してください。\n // 並行 `close` 同士は一方が `TypeError` reject する形で直列化されます。\n if (this.#con) {\n await this.close(_c as unknown as IStorage.CloseContext);\n }\n\n // 空文字の `url`・環境変数は未指定として扱います (本パッケージ仕様)。\n // `url` は `redis://` / `rediss://` 形式での指定を要求します。\n // 実行時の形式検証は行いません (bare 形式は非サポート・動作未保証)。\n const explicitUrl = this.#url === undefined || this.#url === \"\" ? undefined : this.#url;\n const redisUrl =\n process.env[\"REDIS_URL\"] === undefined || process.env[\"REDIS_URL\"] === \"\"\n ? undefined\n : process.env[\"REDIS_URL\"];\n const valkeyUrl =\n process.env[\"VALKEY_URL\"] === undefined || process.env[\"VALKEY_URL\"] === \"\"\n ? undefined\n : process.env[\"VALKEY_URL\"];\n const url = explicitUrl ?? redisUrl ?? valkeyUrl;\n const connectTimeout = this.#options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;\n\n // `ioredis` v6 のコンストラクタ型は `replyMapping` に明示 `undefined` を許しません。\n // `exactOptionalPropertyTypes` 下では spread のまま渡せないため型表明を付けます。\n // 実行時の値は変わらず、`replyMapping` を含む透過は保たれます。\n type ConnectOptions = RedisOptions & { replyMapping?: \"legacy\" };\n\n const options = {\n ...this.#options,\n lazyConnect: true,\n connectTimeout,\n } as ConnectOptions;\n const raw = url === undefined ? new Ioredis(options) : new Ioredis(url, options);\n\n // `ioredis` は接続失敗・再試行時に `error` イベントを emit します。\n // unhandled `error` によるクラッシュを抑止するため no-op リスナを付けます。\n // 可観測性は v1 では提供しません。\n raw.on(\"error\", () => {});\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n try {\n await Promise.race([\n raw.connect(),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new ConnectTimeoutError({ timeoutMs: connectTimeout }));\n }, connectTimeout);\n }),\n ]);\n } catch (ex) {\n raw.disconnect();\n throw ex;\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n }\n\n this.#con = { client: raw };\n }\n\n public async close(_c: IStorage.CloseContext): Promise<void> {\n // 未 `open`・二重 `close` は `TypeError` で reject されます。\n // 必ず `await` し、結果を浮かせないでください。\n const { client } = this.#getConnection();\n\n // 先行 null 化により、並行 `close` 同士は二重 `quit` になりません。\n this.#con = null;\n\n const closeTimeout = this.#closeTimeout;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n try {\n await Promise.race([\n client.quit(),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new CloseTimeoutError({ timeoutMs: closeTimeout }));\n }, closeTimeout);\n }),\n ]);\n } catch {\n // `quit()` の失敗全般 (タイムアウト含む) は `disconnect()` で掃除して正常復帰します。\n client.disconnect();\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n }\n }\n\n #toKey(key: string): string {\n return `${this.#keyPrefix}${key}`;\n }\n\n async #clearPattern(client: Ioredis, pattern: string, signal: AbortSignal): Promise<void> {\n let cursor = \"0\";\n\n do {\n signal.throwIfAborted();\n\n const [nextCursor, keys] = await client.scan(cursor, \"MATCH\", pattern, \"COUNT\", SCAN_COUNT);\n\n if (keys.length > 0) {\n await client.del(...keys);\n }\n\n cursor = nextCursor;\n } while (cursor !== \"0\");\n }\n\n public async write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n await client.set(this.#toKey(c.key), Buffer.from(data));\n }\n\n public async read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n const data = await client.getBuffer(this.#toKey(c.key));\n\n if (data === null) {\n throw new KeyNotFoundError({ key: c.key });\n }\n\n return new Uint8Array(data);\n }\n\n public async exists(c: IStorage.ExistsContext): Promise<boolean> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n return (await client.exists(this.#toKey(c.key))) > 0;\n }\n\n public async delete(c: IStorage.DeleteContext): Promise<void> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n await client.del(this.#toKey(c.key));\n }\n\n public async clear(c: IStorage.ClearContext): Promise<void> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n // prefix が空文字の場合は MATCH \"*\" となり、選択中のデータベースの全キーが対象になります。\n const pattern = `${this.#keyPrefix}*`;\n await this.#clearPattern(client, pattern, c.signal);\n }\n\n public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n const dest = this.#toKey(c.key);\n // 一意なサフィックスにより、同一キーへの並行書き込みでも一時キーが衝突しません。\n const tmp = `${dest}.${randomUUID()}.tmp`;\n\n // 中断と失敗のどちらからでも後始末が走るため、完了処理は一度だけ行います。\n let finished = false;\n const removeAbortListener = (): void => {\n c.signal.removeEventListener(\"abort\", onAbort);\n };\n const discardTemporary = async (): Promise<void> => {\n try {\n await client.del(tmp);\n } catch {\n // 後始末としての削除のため、失敗しても無視します。\n }\n };\n const finish = async (): Promise<void> => {\n if (finished) {\n return;\n }\n\n finished = true;\n removeAbortListener();\n await discardTemporary();\n };\n const onAbort = (): void => {\n void finish();\n };\n c.signal.addEventListener(\"abort\", onAbort, { once: true });\n\n return new WritableStream<Uint8Array<ArrayBuffer>>({\n async abort() {\n await finish();\n },\n async close() {\n c.signal.throwIfAborted();\n try {\n // 全チャンクの追記が完了した時点で初めて最終キーへ置き換えます (swap-on-close)。\n await client.rename(tmp, dest);\n } catch (ex) {\n await finish();\n throw ex;\n }\n // rename 後は一時キーが存在しないため、完了済みとして中断時の削除を行いません。\n finished = true;\n removeAbortListener();\n },\n async start() {\n try {\n c.signal.throwIfAborted();\n // 0 バイトの書き込みでも close 時の RENAME が成功するよう、空の文字列で一時キーを作ります。\n await client.set(tmp, \"\");\n } catch (ex) {\n await finish();\n throw ex;\n }\n },\n async write(chunk) {\n c.signal.throwIfAborted();\n try {\n await client.append(tmp, Buffer.from(chunk));\n } catch (ex) {\n await finish();\n throw ex;\n }\n },\n });\n }\n\n public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n const dest = this.#toKey(c.key);\n let sent = false;\n\n // Redis の GET は値を一括で返すため、取得したバイト列を単一のチャンクとして送出します。\n return new ReadableStream<Uint8Array<ArrayBuffer>>({\n async pull(controller) {\n if (sent) {\n controller.close();\n\n return;\n }\n\n sent = true;\n c.signal.throwIfAborted();\n\n const data = await client.getBuffer(dest);\n\n if (data === null) {\n throw new KeyNotFoundError({ key: c.key });\n }\n\n controller.enqueue(new Uint8Array(data));\n controller.close();\n },\n });\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unikvs/redis.node",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
}
|
|
3
|
+
"version": "0.0.13",
|
|
4
|
+
"description": "Storage plugin that stores byte arrays in Redis on Node.js.",
|
|
5
|
+
"homepage": "https://tai-kun.github.io/unikvs/",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/tai-kun/unikvs",
|
|
9
|
+
"directory": "packages/redis.node"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"package.json",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@unikvs/core": "0.0.13",
|
|
28
|
+
"ioredis": "^6.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@unikvs/testing": "0.0.0",
|
|
32
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
33
|
+
"valibot": "1.5.0",
|
|
34
|
+
"vite-plus": "1.3.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@logtape/logtape": "^2.0.0"
|
|
38
|
+
},
|
|
39
|
+
"readme": "README.md",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "vp -C .config pack"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type ErrorOptions, ErrorBase, setErrorMessage, InvalidUsageErrorBase } from "@unikvs/core";
|
|
2
|
+
|
|
3
|
+
export type { KeyNotFoundErrorArgs, KeyNotFoundErrorMeta } from "@unikvs/core";
|
|
4
|
+
export { KeyNotFoundError } from "@unikvs/core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)
|
|
8
|
+
*/
|
|
9
|
+
export class ClusterNotSupportedError extends InvalidUsageErrorBase<undefined> {
|
|
10
|
+
static {
|
|
11
|
+
this.prototype.name = "RedisClusterNotSupportedError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-cluster-not-supported)
|
|
16
|
+
*/
|
|
17
|
+
public constructor(options?: ErrorOptions) {
|
|
18
|
+
super(
|
|
19
|
+
"Redis storage v1 supports standalone mode only: the `cluster` option is not supported.",
|
|
20
|
+
options,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setErrorMessage(
|
|
26
|
+
ClusterNotSupportedError,
|
|
27
|
+
"Redis ストレージ v1 は standalone モードのみに対応します。`cluster` オプションはサポートされていません。",
|
|
28
|
+
"ja",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
33
|
+
*/
|
|
34
|
+
export type InvalidCloseTimeoutErrorMeta = {
|
|
35
|
+
/**
|
|
36
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#invalid-close-timeout-actual)
|
|
37
|
+
*/
|
|
38
|
+
readonly actual: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
43
|
+
*/
|
|
44
|
+
export type InvalidCloseTimeoutErrorArgs = ErrorOptions & InvalidCloseTimeoutErrorMeta;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
48
|
+
*/
|
|
49
|
+
export class InvalidCloseTimeoutError extends ErrorBase<InvalidCloseTimeoutErrorMeta> {
|
|
50
|
+
static {
|
|
51
|
+
this.prototype.name = "RedisInvalidCloseTimeoutError";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-invalid-close-timeout)
|
|
56
|
+
*/
|
|
57
|
+
public constructor(args: InvalidCloseTimeoutErrorArgs) {
|
|
58
|
+
const { actual, ...options } = args;
|
|
59
|
+
const meta: InvalidCloseTimeoutErrorMeta = { actual };
|
|
60
|
+
super(
|
|
61
|
+
meta,
|
|
62
|
+
({ actual: messageActual }) =>
|
|
63
|
+
`\`closeTimeout\` must be a finite positive number (ms), but got ${String(messageActual)}.`,
|
|
64
|
+
options,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setErrorMessage(
|
|
70
|
+
InvalidCloseTimeoutError,
|
|
71
|
+
({ actual }) =>
|
|
72
|
+
`\`closeTimeout\` は有限の正数 (ミリ秒) でなければなりません。受け取った値: ${String(actual)}`,
|
|
73
|
+
"ja",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
78
|
+
*/
|
|
79
|
+
export type ConnectTimeoutErrorMeta = {
|
|
80
|
+
/**
|
|
81
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#connect-timeout-timeoutms)
|
|
82
|
+
*/
|
|
83
|
+
readonly timeoutMs: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
88
|
+
*/
|
|
89
|
+
export type ConnectTimeoutErrorArgs = ErrorOptions & ConnectTimeoutErrorMeta;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
93
|
+
*/
|
|
94
|
+
export class ConnectTimeoutError extends ErrorBase<ConnectTimeoutErrorMeta> {
|
|
95
|
+
static {
|
|
96
|
+
this.prototype.name = "RedisConnectTimeoutError";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-connect-timeout)
|
|
101
|
+
*/
|
|
102
|
+
public constructor(args: ConnectTimeoutErrorArgs) {
|
|
103
|
+
const { timeoutMs, ...options } = args;
|
|
104
|
+
const meta: ConnectTimeoutErrorMeta = { timeoutMs };
|
|
105
|
+
super(
|
|
106
|
+
meta,
|
|
107
|
+
({ timeoutMs: messageTimeoutMs }) =>
|
|
108
|
+
`Timed out connecting to Redis after ${messageTimeoutMs}ms`,
|
|
109
|
+
options,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setErrorMessage(
|
|
115
|
+
ConnectTimeoutError,
|
|
116
|
+
({ timeoutMs }) => `Redis への接続が ${timeoutMs}ms でタイムアウトしました`,
|
|
117
|
+
"ja",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
122
|
+
*/
|
|
123
|
+
export type CloseTimeoutErrorMeta = {
|
|
124
|
+
/**
|
|
125
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#close-timeout-timeoutms)
|
|
126
|
+
*/
|
|
127
|
+
readonly timeoutMs: number;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
132
|
+
*/
|
|
133
|
+
export type CloseTimeoutErrorArgs = ErrorOptions & CloseTimeoutErrorMeta;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
137
|
+
*/
|
|
138
|
+
export class CloseTimeoutError extends ErrorBase<CloseTimeoutErrorMeta> {
|
|
139
|
+
static {
|
|
140
|
+
this.prototype.name = "RedisCloseTimeoutError";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#error-close-timeout)
|
|
145
|
+
*/
|
|
146
|
+
public constructor(args: CloseTimeoutErrorArgs) {
|
|
147
|
+
const { timeoutMs, ...options } = args;
|
|
148
|
+
const meta: CloseTimeoutErrorMeta = { timeoutMs };
|
|
149
|
+
super(
|
|
150
|
+
meta,
|
|
151
|
+
({ timeoutMs: messageTimeoutMs }) => `Timed out closing Redis after ${messageTimeoutMs}ms`,
|
|
152
|
+
options,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setErrorMessage(
|
|
158
|
+
CloseTimeoutError,
|
|
159
|
+
({ timeoutMs }) => `Redis からの切断処理が ${timeoutMs}ms でタイムアウトしました`,
|
|
160
|
+
"ja",
|
|
161
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
KeyNotFoundErrorArgs,
|
|
3
|
+
KeyNotFoundErrorMeta,
|
|
4
|
+
CloseTimeoutErrorArgs,
|
|
5
|
+
CloseTimeoutErrorMeta,
|
|
6
|
+
ConnectTimeoutErrorArgs,
|
|
7
|
+
ConnectTimeoutErrorMeta,
|
|
8
|
+
InvalidCloseTimeoutErrorArgs,
|
|
9
|
+
InvalidCloseTimeoutErrorMeta,
|
|
10
|
+
} from "./errors.js";
|
|
11
|
+
export {
|
|
12
|
+
KeyNotFoundError,
|
|
13
|
+
CloseTimeoutError,
|
|
14
|
+
ConnectTimeoutError,
|
|
15
|
+
ClusterNotSupportedError,
|
|
16
|
+
InvalidCloseTimeoutError,
|
|
17
|
+
} from "./errors.js";
|
|
18
|
+
export type * from "./redis.js";
|
|
19
|
+
export { default as Redis } from "./redis.js";
|
package/src/redis.ts
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { type IStorage, KeyNotFoundError } from "@unikvs/core";
|
|
4
|
+
import {
|
|
5
|
+
type ClusterNode,
|
|
6
|
+
type RedisOptions,
|
|
7
|
+
type ClusterOptions,
|
|
8
|
+
Redis as Ioredis,
|
|
9
|
+
} from "ioredis";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CloseTimeoutError,
|
|
13
|
+
ConnectTimeoutError,
|
|
14
|
+
ClusterNotSupportedError,
|
|
15
|
+
InvalidCloseTimeoutError,
|
|
16
|
+
} from "./errors.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec)
|
|
20
|
+
*/
|
|
21
|
+
export type RedisClusterSpec = {
|
|
22
|
+
/**
|
|
23
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec-nodes)
|
|
24
|
+
*/
|
|
25
|
+
readonly nodes: readonly ClusterNode[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisclusterspec-options)
|
|
29
|
+
*/
|
|
30
|
+
readonly options?: ClusterOptions | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions)
|
|
35
|
+
*/
|
|
36
|
+
export type RedisStorageOptions = Omit<RedisOptions, "keyPrefix" | "lazyConnect"> & {
|
|
37
|
+
/**
|
|
38
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-keyprefix)
|
|
39
|
+
*/
|
|
40
|
+
readonly keyPrefix?: string | undefined;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-cluster)
|
|
44
|
+
*/
|
|
45
|
+
readonly cluster?: RedisClusterSpec | undefined;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redisstorageoptions-closetimeout)
|
|
49
|
+
*/
|
|
50
|
+
readonly closeTimeout?: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const SCAN_COUNT = 1000;
|
|
54
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
55
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
|
|
56
|
+
|
|
57
|
+
type Connection = { readonly client: Ioredis };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
|
|
61
|
+
*/
|
|
62
|
+
export default class Redis implements IStorage {
|
|
63
|
+
#con: Connection | null;
|
|
64
|
+
|
|
65
|
+
readonly #url: string | undefined;
|
|
66
|
+
|
|
67
|
+
// `closeTimeout` は本パッケージ固有の拡張です。
|
|
68
|
+
// `this.#options` には含めず、別フィールドで保管します。
|
|
69
|
+
// `this.#options` は `new Ioredis` にそのまま spread できる透過バッグです。
|
|
70
|
+
readonly #options: Omit<RedisStorageOptions, "keyPrefix" | "cluster" | "closeTimeout">;
|
|
71
|
+
|
|
72
|
+
// `ioredis` の `disconnectTimeout` (ソケット切断待機) とは別物です。
|
|
73
|
+
readonly #closeTimeout: number;
|
|
74
|
+
|
|
75
|
+
readonly #keyPrefix: string;
|
|
76
|
+
|
|
77
|
+
public readonly name: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
|
|
81
|
+
*/
|
|
82
|
+
public constructor(url?: string, options: RedisStorageOptions = {}) {
|
|
83
|
+
const {
|
|
84
|
+
cluster,
|
|
85
|
+
keyPrefix = "unikvs:",
|
|
86
|
+
closeTimeout = DEFAULT_CLOSE_TIMEOUT_MS,
|
|
87
|
+
...redisOptions
|
|
88
|
+
} = options;
|
|
89
|
+
|
|
90
|
+
// v1 は standalone のみに対応します。
|
|
91
|
+
// `cluster` を指定したときは、`closeTimeout` 検証より先に拒否します。
|
|
92
|
+
if (cluster !== undefined) {
|
|
93
|
+
throw new ClusterNotSupportedError();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// `undefined` は既定値 5000ms になるため対象外とします。
|
|
97
|
+
// 明示値が有限の正数でない場合のみ拒否します。
|
|
98
|
+
if (typeof closeTimeout !== "number" || !Number.isFinite(closeTimeout) || closeTimeout <= 0) {
|
|
99
|
+
throw new InvalidCloseTimeoutError({ actual: closeTimeout });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.name = "Redis";
|
|
103
|
+
this.#con = null;
|
|
104
|
+
this.#url = url;
|
|
105
|
+
this.#options = redisOptions;
|
|
106
|
+
this.#closeTimeout = closeTimeout;
|
|
107
|
+
this.#keyPrefix = keyPrefix;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public get isOpen(): boolean {
|
|
111
|
+
return this.#con !== null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 開いている接続を返します。
|
|
115
|
+
// 未オープン時は `TypeError` を投げます。
|
|
116
|
+
#getConnection(): Connection {
|
|
117
|
+
const con = this.#con;
|
|
118
|
+
|
|
119
|
+
if (con === null) {
|
|
120
|
+
throw new TypeError("Redis is not open. Call open() first.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return con;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public async open(_c: IStorage.OpenContext): Promise<void> {
|
|
127
|
+
// 並行 `open` 同士および `open` 接続待ち中の `close` の同時呼び出しは禁止 (事前条件) です。
|
|
128
|
+
// 呼び出し側で直列化してください。
|
|
129
|
+
// 並行 `close` 同士は一方が `TypeError` reject する形で直列化されます。
|
|
130
|
+
if (this.#con) {
|
|
131
|
+
await this.close(_c as unknown as IStorage.CloseContext);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 空文字の `url`・環境変数は未指定として扱います (本パッケージ仕様)。
|
|
135
|
+
// `url` は `redis://` / `rediss://` 形式での指定を要求します。
|
|
136
|
+
// 実行時の形式検証は行いません (bare 形式は非サポート・動作未保証)。
|
|
137
|
+
const explicitUrl = this.#url === undefined || this.#url === "" ? undefined : this.#url;
|
|
138
|
+
const redisUrl =
|
|
139
|
+
process.env["REDIS_URL"] === undefined || process.env["REDIS_URL"] === ""
|
|
140
|
+
? undefined
|
|
141
|
+
: process.env["REDIS_URL"];
|
|
142
|
+
const valkeyUrl =
|
|
143
|
+
process.env["VALKEY_URL"] === undefined || process.env["VALKEY_URL"] === ""
|
|
144
|
+
? undefined
|
|
145
|
+
: process.env["VALKEY_URL"];
|
|
146
|
+
const url = explicitUrl ?? redisUrl ?? valkeyUrl;
|
|
147
|
+
const connectTimeout = this.#options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
148
|
+
|
|
149
|
+
// `ioredis` v6 のコンストラクタ型は `replyMapping` に明示 `undefined` を許しません。
|
|
150
|
+
// `exactOptionalPropertyTypes` 下では spread のまま渡せないため型表明を付けます。
|
|
151
|
+
// 実行時の値は変わらず、`replyMapping` を含む透過は保たれます。
|
|
152
|
+
type ConnectOptions = RedisOptions & { replyMapping?: "legacy" };
|
|
153
|
+
|
|
154
|
+
const options = {
|
|
155
|
+
...this.#options,
|
|
156
|
+
lazyConnect: true,
|
|
157
|
+
connectTimeout,
|
|
158
|
+
} as ConnectOptions;
|
|
159
|
+
const raw = url === undefined ? new Ioredis(options) : new Ioredis(url, options);
|
|
160
|
+
|
|
161
|
+
// `ioredis` は接続失敗・再試行時に `error` イベントを emit します。
|
|
162
|
+
// unhandled `error` によるクラッシュを抑止するため no-op リスナを付けます。
|
|
163
|
+
// 可観測性は v1 では提供しません。
|
|
164
|
+
raw.on("error", () => {});
|
|
165
|
+
|
|
166
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await Promise.race([
|
|
170
|
+
raw.connect(),
|
|
171
|
+
new Promise<never>((_, reject) => {
|
|
172
|
+
timer = setTimeout(() => {
|
|
173
|
+
reject(new ConnectTimeoutError({ timeoutMs: connectTimeout }));
|
|
174
|
+
}, connectTimeout);
|
|
175
|
+
}),
|
|
176
|
+
]);
|
|
177
|
+
} catch (ex) {
|
|
178
|
+
raw.disconnect();
|
|
179
|
+
throw ex;
|
|
180
|
+
} finally {
|
|
181
|
+
if (timer !== undefined) {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.#con = { client: raw };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public async close(_c: IStorage.CloseContext): Promise<void> {
|
|
190
|
+
// 未 `open`・二重 `close` は `TypeError` で reject されます。
|
|
191
|
+
// 必ず `await` し、結果を浮かせないでください。
|
|
192
|
+
const { client } = this.#getConnection();
|
|
193
|
+
|
|
194
|
+
// 先行 null 化により、並行 `close` 同士は二重 `quit` になりません。
|
|
195
|
+
this.#con = null;
|
|
196
|
+
|
|
197
|
+
const closeTimeout = this.#closeTimeout;
|
|
198
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await Promise.race([
|
|
202
|
+
client.quit(),
|
|
203
|
+
new Promise<never>((_, reject) => {
|
|
204
|
+
timer = setTimeout(() => {
|
|
205
|
+
reject(new CloseTimeoutError({ timeoutMs: closeTimeout }));
|
|
206
|
+
}, closeTimeout);
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
} catch {
|
|
210
|
+
// `quit()` の失敗全般 (タイムアウト含む) は `disconnect()` で掃除して正常復帰します。
|
|
211
|
+
client.disconnect();
|
|
212
|
+
} finally {
|
|
213
|
+
if (timer !== undefined) {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#toKey(key: string): string {
|
|
220
|
+
return `${this.#keyPrefix}${key}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async #clearPattern(client: Ioredis, pattern: string, signal: AbortSignal): Promise<void> {
|
|
224
|
+
let cursor = "0";
|
|
225
|
+
|
|
226
|
+
do {
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
|
|
229
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
|
|
230
|
+
|
|
231
|
+
if (keys.length > 0) {
|
|
232
|
+
await client.del(...keys);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
cursor = nextCursor;
|
|
236
|
+
} while (cursor !== "0");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public async write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void> {
|
|
240
|
+
const { client } = this.#getConnection();
|
|
241
|
+
|
|
242
|
+
c.signal.throwIfAborted();
|
|
243
|
+
|
|
244
|
+
await client.set(this.#toKey(c.key), Buffer.from(data));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public async read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>> {
|
|
248
|
+
const { client } = this.#getConnection();
|
|
249
|
+
|
|
250
|
+
c.signal.throwIfAborted();
|
|
251
|
+
|
|
252
|
+
const data = await client.getBuffer(this.#toKey(c.key));
|
|
253
|
+
|
|
254
|
+
if (data === null) {
|
|
255
|
+
throw new KeyNotFoundError({ key: c.key });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return new Uint8Array(data);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async exists(c: IStorage.ExistsContext): Promise<boolean> {
|
|
262
|
+
const { client } = this.#getConnection();
|
|
263
|
+
|
|
264
|
+
c.signal.throwIfAborted();
|
|
265
|
+
|
|
266
|
+
return (await client.exists(this.#toKey(c.key))) > 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public async delete(c: IStorage.DeleteContext): Promise<void> {
|
|
270
|
+
const { client } = this.#getConnection();
|
|
271
|
+
|
|
272
|
+
c.signal.throwIfAborted();
|
|
273
|
+
|
|
274
|
+
await client.del(this.#toKey(c.key));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public async clear(c: IStorage.ClearContext): Promise<void> {
|
|
278
|
+
const { client } = this.#getConnection();
|
|
279
|
+
|
|
280
|
+
c.signal.throwIfAborted();
|
|
281
|
+
|
|
282
|
+
// prefix が空文字の場合は MATCH "*" となり、選択中のデータベースの全キーが対象になります。
|
|
283
|
+
const pattern = `${this.#keyPrefix}*`;
|
|
284
|
+
await this.#clearPattern(client, pattern, c.signal);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>> {
|
|
288
|
+
const { client } = this.#getConnection();
|
|
289
|
+
|
|
290
|
+
c.signal.throwIfAborted();
|
|
291
|
+
|
|
292
|
+
const dest = this.#toKey(c.key);
|
|
293
|
+
// 一意なサフィックスにより、同一キーへの並行書き込みでも一時キーが衝突しません。
|
|
294
|
+
const tmp = `${dest}.${randomUUID()}.tmp`;
|
|
295
|
+
|
|
296
|
+
// 中断と失敗のどちらからでも後始末が走るため、完了処理は一度だけ行います。
|
|
297
|
+
let finished = false;
|
|
298
|
+
const removeAbortListener = (): void => {
|
|
299
|
+
c.signal.removeEventListener("abort", onAbort);
|
|
300
|
+
};
|
|
301
|
+
const discardTemporary = async (): Promise<void> => {
|
|
302
|
+
try {
|
|
303
|
+
await client.del(tmp);
|
|
304
|
+
} catch {
|
|
305
|
+
// 後始末としての削除のため、失敗しても無視します。
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const finish = async (): Promise<void> => {
|
|
309
|
+
if (finished) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
finished = true;
|
|
314
|
+
removeAbortListener();
|
|
315
|
+
await discardTemporary();
|
|
316
|
+
};
|
|
317
|
+
const onAbort = (): void => {
|
|
318
|
+
void finish();
|
|
319
|
+
};
|
|
320
|
+
c.signal.addEventListener("abort", onAbort, { once: true });
|
|
321
|
+
|
|
322
|
+
return new WritableStream<Uint8Array<ArrayBuffer>>({
|
|
323
|
+
async abort() {
|
|
324
|
+
await finish();
|
|
325
|
+
},
|
|
326
|
+
async close() {
|
|
327
|
+
c.signal.throwIfAborted();
|
|
328
|
+
try {
|
|
329
|
+
// 全チャンクの追記が完了した時点で初めて最終キーへ置き換えます (swap-on-close)。
|
|
330
|
+
await client.rename(tmp, dest);
|
|
331
|
+
} catch (ex) {
|
|
332
|
+
await finish();
|
|
333
|
+
throw ex;
|
|
334
|
+
}
|
|
335
|
+
// rename 後は一時キーが存在しないため、完了済みとして中断時の削除を行いません。
|
|
336
|
+
finished = true;
|
|
337
|
+
removeAbortListener();
|
|
338
|
+
},
|
|
339
|
+
async start() {
|
|
340
|
+
try {
|
|
341
|
+
c.signal.throwIfAborted();
|
|
342
|
+
// 0 バイトの書き込みでも close 時の RENAME が成功するよう、空の文字列で一時キーを作ります。
|
|
343
|
+
await client.set(tmp, "");
|
|
344
|
+
} catch (ex) {
|
|
345
|
+
await finish();
|
|
346
|
+
throw ex;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
async write(chunk) {
|
|
350
|
+
c.signal.throwIfAborted();
|
|
351
|
+
try {
|
|
352
|
+
await client.append(tmp, Buffer.from(chunk));
|
|
353
|
+
} catch (ex) {
|
|
354
|
+
await finish();
|
|
355
|
+
throw ex;
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>> {
|
|
362
|
+
const { client } = this.#getConnection();
|
|
363
|
+
|
|
364
|
+
c.signal.throwIfAborted();
|
|
365
|
+
|
|
366
|
+
const dest = this.#toKey(c.key);
|
|
367
|
+
let sent = false;
|
|
368
|
+
|
|
369
|
+
// Redis の GET は値を一括で返すため、取得したバイト列を単一のチャンクとして送出します。
|
|
370
|
+
return new ReadableStream<Uint8Array<ArrayBuffer>>({
|
|
371
|
+
async pull(controller) {
|
|
372
|
+
if (sent) {
|
|
373
|
+
controller.close();
|
|
374
|
+
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
sent = true;
|
|
379
|
+
c.signal.throwIfAborted();
|
|
380
|
+
|
|
381
|
+
const data = await client.getBuffer(dest);
|
|
382
|
+
|
|
383
|
+
if (data === null) {
|
|
384
|
+
throw new KeyNotFoundError({ key: c.key });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
controller.enqueue(new Uint8Array(data));
|
|
388
|
+
controller.close();
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|