@unikvs/redis.bun 0.0.0 → 0.0.14

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 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.bun
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
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.bun`
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/)
@@ -0,0 +1,122 @@
1
+ import { IStorage, KeyNotFoundError, KeyNotFoundErrorArgs, KeyNotFoundErrorMeta, UnsupportedRuntimeError } from "@unikvs/core";
2
+ import { RedisOptions } from "bun";
3
+ //#region ../src/redis.d.ts
4
+ /**
5
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions)
6
+ */
7
+ type RedisStorageOptions = RedisOptions & {
8
+ /**
9
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions-keyprefix)
10
+ */
11
+ readonly keyPrefix?: string;
12
+ };
13
+ /**
14
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
15
+ */
16
+ declare class Redis implements IStorage {
17
+ #private;
18
+ /**
19
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
20
+ */
21
+ readonly name: string;
22
+ /**
23
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
24
+ */
25
+ constructor(url?: string, options?: RedisStorageOptions);
26
+ /**
27
+ * ストレージがオープン済みかどうかです。
28
+ *
29
+ * @returns オープン済みなら真です。
30
+ */
31
+ get isOpen(): boolean;
32
+ /**
33
+ * ストレージをオープンします。
34
+ *
35
+ * Bun の Redis クライアントを接続します。
36
+ *
37
+ * @param _c オープンコンテキストです。
38
+ *
39
+ * @returns オープンが完了したら解決します。
40
+ */
41
+ open(_c: IStorage.OpenContext): Promise<void>;
42
+ /**
43
+ * ストレージをクローズします。
44
+ *
45
+ * クライアントを閉じて参照を外します。
46
+ *
47
+ * @param _c クローズコンテキストです。
48
+ */
49
+ close(_c: IStorage.CloseContext): void;
50
+ /**
51
+ * バイト列を書き込みます。
52
+ *
53
+ * 指定したキーで値を上書きします。
54
+ *
55
+ * @param c 書き込みコンテキストです。
56
+ * @param data 保存するバイト列です。
57
+ *
58
+ * @returns 書き込みが完了したら解決します。
59
+ */
60
+ write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void>;
61
+ /**
62
+ * バイト列を読み取ります。
63
+ *
64
+ * 指定したキーの値を取得します。
65
+ *
66
+ * @param c 読み取りコンテキストです。
67
+ *
68
+ * @returns 読み取ったバイト列です。
69
+ */
70
+ read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>>;
71
+ /**
72
+ * キーの存在を確認します。
73
+ *
74
+ * @param c 存在確認コンテキストです。
75
+ *
76
+ * @returns 存在すれば真です。
77
+ */
78
+ exists(c: IStorage.ExistsContext): Promise<boolean>;
79
+ /**
80
+ * キーを削除します。
81
+ *
82
+ * 存在しないキーも成功扱いにします。
83
+ *
84
+ * @param c 削除コンテキストです。
85
+ *
86
+ * @returns 削除が完了したら解決します。
87
+ */
88
+ delete(c: IStorage.DeleteContext): Promise<void>;
89
+ /**
90
+ * 接頭辞に一致するキーをすべて削除します。
91
+ *
92
+ * `SCAN` で列挙しながら削除します。
93
+ *
94
+ * @param c クリアコンテキストです。
95
+ *
96
+ * @returns 削除が完了したら解決します。
97
+ */
98
+ clear(c: IStorage.ClearContext): Promise<void>;
99
+ /**
100
+ * 書き込み用ストリームを取得します。
101
+ *
102
+ * 一時キーに追記し、`close` 時に最終キーへ置き換えます。
103
+ *
104
+ * @param c 書き込みストリーム取得コンテキストです。
105
+ *
106
+ * @returns 書き込み用ストリームです。
107
+ */
108
+ getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>>;
109
+ /**
110
+ * 読み取り用ストリームを取得します。
111
+ *
112
+ * 取得した値を単一チャンクとして送出します。
113
+ *
114
+ * @param c 読み取りストリーム取得コンテキストです。
115
+ *
116
+ * @returns 読み取り用ストリームです。
117
+ */
118
+ getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>>;
119
+ }
120
+ //#endregion
121
+ export { KeyNotFoundError, type KeyNotFoundErrorArgs, type KeyNotFoundErrorMeta, Redis, type RedisStorageOptions, UnsupportedRuntimeError };
122
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/redis.ts"],"mappings":";;;;;;KAMY,sBAAsB;;;;WAIvB;;;;;cAsBU,iBAAiB;;;;;WAIpB;;;;EA6BhB,YAAmB,cAAc,UAAS;;;;;;MAe/B;;;;;;;;;;EAaX,KAAkB,IAAI,SAAS,cAAc;;;;;;;;EAmC7C,MAAa,IAAI,SAAS;;;;;;;;;;;EAe1B,MAAmB,GAAG,SAAS,cAAc,MAAM,WAAW,eAAe;;;;;;;;;;EAkB7E,KAAkB,GAAG,SAAS,cAAc,QAAQ,WAAW;;;;;;;;EAsB/D,OAAoB,GAAG,SAAS,gBAAgB;;;;;;;;;;EAkBhD,OAAoB,GAAG,SAAS,gBAAgB;;;;;;;;;;EAkBhD,MAAmB,GAAG,SAAS,eAAe;;;;;;;;;;EAgC9C,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW;;;;;;;;;;EAmF9E,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW"}
package/dist/index.js ADDED
@@ -0,0 +1,306 @@
1
+ import { KeyNotFoundError, KeyNotFoundError as KeyNotFoundError$1, UnsupportedRuntimeError, UnsupportedRuntimeError as UnsupportedRuntimeError$1 } from "@unikvs/core";
2
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/checkPrivateRedeclaration.js
3
+ function _checkPrivateRedeclaration(e, t) {
4
+ if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
5
+ }
6
+ //#endregion
7
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateMethodInitSpec.js
8
+ function _classPrivateMethodInitSpec(e, a) {
9
+ _checkPrivateRedeclaration(e, a), a.add(e);
10
+ }
11
+ //#endregion
12
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldInitSpec.js
13
+ function _classPrivateFieldInitSpec(e, t, a) {
14
+ _checkPrivateRedeclaration(e, t), t.set(e, a);
15
+ }
16
+ //#endregion
17
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/assertClassBrand.js
18
+ function _assertClassBrand(e, t, n) {
19
+ if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
20
+ throw new TypeError("Private element is not present on this object");
21
+ }
22
+ //#endregion
23
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldSet2.js
24
+ function _classPrivateFieldSet2(s, a, r) {
25
+ return s.set(_assertClassBrand(s, a), r), r;
26
+ }
27
+ //#endregion
28
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldGet2.js
29
+ function _classPrivateFieldGet2(s, a) {
30
+ return s.get(_assertClassBrand(s, a));
31
+ }
32
+ //#endregion
33
+ //#region ../src/redis.ts
34
+ /**
35
+ * Redis の `SCAN` 1 回あたりにヒントとして渡すキー数です。
36
+ * 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。
37
+ */
38
+ const SCAN_COUNT = 1e3;
39
+ var _con = /* @__PURE__ */ new WeakMap();
40
+ var _url = /* @__PURE__ */ new WeakMap();
41
+ var _options = /* @__PURE__ */ new WeakMap();
42
+ var _keyPrefix = /* @__PURE__ */ new WeakMap();
43
+ var _Redis_brand = /* @__PURE__ */ new WeakSet();
44
+ /**
45
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
46
+ */
47
+ var Redis = class {
48
+ /**
49
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
50
+ */
51
+ constructor(url, options = {}) {
52
+ _classPrivateMethodInitSpec(this, _Redis_brand);
53
+ _classPrivateFieldInitSpec(this, _con, void 0);
54
+ _classPrivateFieldInitSpec(this, _url, void 0);
55
+ _classPrivateFieldInitSpec(this, _options, void 0);
56
+ _classPrivateFieldInitSpec(this, _keyPrefix, void 0);
57
+ const { keyPrefix = "unikvs:", ...redisOptions } = options;
58
+ this.name = "Redis";
59
+ _classPrivateFieldSet2(_con, this, null);
60
+ _classPrivateFieldSet2(_url, this, url);
61
+ _classPrivateFieldSet2(_options, this, redisOptions);
62
+ _classPrivateFieldSet2(_keyPrefix, this, keyPrefix);
63
+ }
64
+ /**
65
+ * ストレージがオープン済みかどうかです。
66
+ *
67
+ * @returns オープン済みなら真です。
68
+ */
69
+ get isOpen() {
70
+ return _classPrivateFieldGet2(_con, this) !== null;
71
+ }
72
+ /**
73
+ * ストレージをオープンします。
74
+ *
75
+ * Bun の Redis クライアントを接続します。
76
+ *
77
+ * @param _c オープンコンテキストです。
78
+ *
79
+ * @returns オープンが完了したら解決します。
80
+ */
81
+ async open(_c) {
82
+ if (typeof Bun === "undefined") throw new UnsupportedRuntimeError$1({
83
+ name: this.name,
84
+ runtime: "Bun"
85
+ });
86
+ if (_classPrivateFieldGet2(_con, this)) this.close(_c);
87
+ const { RedisClient: RedisClientImpl } = await import("bun");
88
+ const client = new RedisClientImpl(_classPrivateFieldGet2(_url, this), _classPrivateFieldGet2(_options, this));
89
+ try {
90
+ await client.connect();
91
+ } catch (ex) {
92
+ client.close();
93
+ throw ex;
94
+ }
95
+ _classPrivateFieldSet2(_con, this, { client });
96
+ }
97
+ /**
98
+ * ストレージをクローズします。
99
+ *
100
+ * クライアントを閉じて参照を外します。
101
+ *
102
+ * @param _c クローズコンテキストです。
103
+ */
104
+ close(_c) {
105
+ _assertClassBrand(_Redis_brand, this, _getConnection).call(this).client.close();
106
+ _classPrivateFieldSet2(_con, this, null);
107
+ }
108
+ /**
109
+ * バイト列を書き込みます。
110
+ *
111
+ * 指定したキーで値を上書きします。
112
+ *
113
+ * @param c 書き込みコンテキストです。
114
+ * @param data 保存するバイト列です。
115
+ *
116
+ * @returns 書き込みが完了したら解決します。
117
+ */
118
+ async write(c, data) {
119
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
120
+ c.signal.throwIfAborted();
121
+ await client.set(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key), data);
122
+ }
123
+ /**
124
+ * バイト列を読み取ります。
125
+ *
126
+ * 指定したキーの値を取得します。
127
+ *
128
+ * @param c 読み取りコンテキストです。
129
+ *
130
+ * @returns 読み取ったバイト列です。
131
+ */
132
+ async read(c) {
133
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
134
+ c.signal.throwIfAborted();
135
+ const data = await client.getBuffer(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
136
+ if (data === null) throw new KeyNotFoundError$1({ key: c.key });
137
+ return new Uint8Array(data);
138
+ }
139
+ /**
140
+ * キーの存在を確認します。
141
+ *
142
+ * @param c 存在確認コンテキストです。
143
+ *
144
+ * @returns 存在すれば真です。
145
+ */
146
+ async exists(c) {
147
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
148
+ c.signal.throwIfAborted();
149
+ return client.exists(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
150
+ }
151
+ /**
152
+ * キーを削除します。
153
+ *
154
+ * 存在しないキーも成功扱いにします。
155
+ *
156
+ * @param c 削除コンテキストです。
157
+ *
158
+ * @returns 削除が完了したら解決します。
159
+ */
160
+ async delete(c) {
161
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
162
+ c.signal.throwIfAborted();
163
+ await client.del(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
164
+ }
165
+ /**
166
+ * 接頭辞に一致するキーをすべて削除します。
167
+ *
168
+ * `SCAN` で列挙しながら削除します。
169
+ *
170
+ * @param c クリアコンテキストです。
171
+ *
172
+ * @returns 削除が完了したら解決します。
173
+ */
174
+ async clear(c) {
175
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
176
+ c.signal.throwIfAborted();
177
+ const pattern = `${_classPrivateFieldGet2(_keyPrefix, this)}*`;
178
+ let cursor = "0";
179
+ do {
180
+ c.signal.throwIfAborted();
181
+ const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
182
+ if (keys.length > 0) await client.del(...keys);
183
+ cursor = nextCursor;
184
+ } while (cursor !== "0");
185
+ }
186
+ /**
187
+ * 書き込み用ストリームを取得します。
188
+ *
189
+ * 一時キーに追記し、`close` 時に最終キーへ置き換えます。
190
+ *
191
+ * @param c 書き込みストリーム取得コンテキストです。
192
+ *
193
+ * @returns 書き込み用ストリームです。
194
+ */
195
+ getWritable(c) {
196
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
197
+ c.signal.throwIfAborted();
198
+ const dest = _assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key);
199
+ const tmp = `${dest}.${Bun.randomUUIDv7()}.tmp`;
200
+ let finished = false;
201
+ const removeAbortListener = () => {
202
+ c.signal.removeEventListener("abort", onAbort);
203
+ };
204
+ const discardTemporary = async () => {
205
+ try {
206
+ await client.del(tmp);
207
+ } catch {}
208
+ };
209
+ const finish = async () => {
210
+ if (finished) return;
211
+ finished = true;
212
+ removeAbortListener();
213
+ await discardTemporary();
214
+ };
215
+ const onAbort = () => {
216
+ finish();
217
+ };
218
+ c.signal.addEventListener("abort", onAbort, { once: true });
219
+ return new WritableStream({
220
+ async abort() {
221
+ await finish();
222
+ },
223
+ async close() {
224
+ c.signal.throwIfAborted();
225
+ try {
226
+ await client.rename(tmp, dest);
227
+ } catch (ex) {
228
+ await finish();
229
+ throw ex;
230
+ }
231
+ finished = true;
232
+ removeAbortListener();
233
+ },
234
+ async start() {
235
+ try {
236
+ c.signal.throwIfAborted();
237
+ await client.set(tmp, "");
238
+ } catch (ex) {
239
+ await finish();
240
+ throw ex;
241
+ }
242
+ },
243
+ async write(chunk) {
244
+ c.signal.throwIfAborted();
245
+ try {
246
+ await client.append(tmp, chunk);
247
+ } catch (ex) {
248
+ await finish();
249
+ throw ex;
250
+ }
251
+ }
252
+ });
253
+ }
254
+ /**
255
+ * 読み取り用ストリームを取得します。
256
+ *
257
+ * 取得した値を単一チャンクとして送出します。
258
+ *
259
+ * @param c 読み取りストリーム取得コンテキストです。
260
+ *
261
+ * @returns 読み取り用ストリームです。
262
+ */
263
+ getReadable(c) {
264
+ const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
265
+ c.signal.throwIfAborted();
266
+ const dest = _assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key);
267
+ let sent = false;
268
+ return new ReadableStream({ async pull(controller) {
269
+ if (sent) {
270
+ controller.close();
271
+ return;
272
+ }
273
+ sent = true;
274
+ c.signal.throwIfAborted();
275
+ const data = await client.getBuffer(dest);
276
+ if (data === null) throw new KeyNotFoundError$1({ key: c.key });
277
+ controller.enqueue(new Uint8Array(data));
278
+ controller.close();
279
+ } });
280
+ }
281
+ };
282
+ /**
283
+ * オープン済みかを確認します。
284
+ *
285
+ * 未オープン時は例外を投げます。
286
+ *
287
+ * @returns オープン済みの接続です。
288
+ */
289
+ function _getConnection() {
290
+ if (_classPrivateFieldGet2(_con, this) === null) throw new TypeError("Redis is not open. Call open() first.");
291
+ return _classPrivateFieldGet2(_con, this);
292
+ }
293
+ /**
294
+ * unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。
295
+ *
296
+ * @param key 変換する論理キーです。
297
+ *
298
+ * @returns Redis 上の実キーです。
299
+ */
300
+ function _toKey(key) {
301
+ return `${_classPrivateFieldGet2(_keyPrefix, this)}${key}`;
302
+ }
303
+ //#endregion
304
+ export { KeyNotFoundError, Redis, UnsupportedRuntimeError };
305
+
306
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["UnsupportedRuntimeError","KeyNotFoundError"],"sources":["../src/redis.ts"],"sourcesContent":["import { type IStorage, KeyNotFoundError, UnsupportedRuntimeError } from \"@unikvs/core\";\nimport type { RedisClient, RedisOptions } from \"bun\";\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions)\n */\nexport type RedisStorageOptions = RedisOptions & {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions-keyprefix)\n */\n readonly keyPrefix?: string;\n};\n\n/**\n * ストレージの動作に必要な接続を保持する型定義です。\n */\ntype Connection = {\n /**\n * Bun の Redis クライアントです。\n */\n readonly client: RedisClient;\n};\n\n/**\n * Redis の `SCAN` 1 回あたりにヒントとして渡すキー数です。\n * 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。\n */\nconst SCAN_COUNT = 1000;\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)\n */\nexport default class Redis implements IStorage {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)\n */\n public readonly name: string;\n\n /**\n * Redis クライアントのインスタンスを保持します。\n *\n * ストレージがオープンされるまで null です。\n */\n #con: Connection | null;\n\n /**\n * 接続先の URL です。\n *\n * 未指定の場合はクライアントが環境変数 (`REDIS_URL`、`VALKEY_URL`) から解決します。\n */\n readonly #url: string | undefined;\n\n /**\n * `RedisClient` の初期化に使用する設定オブジェクトです。\n */\n readonly #options: RedisOptions;\n\n /**\n * すべてのキーの先頭に付与するプレフィックスです。\n */\n readonly #keyPrefix: string;\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)\n */\n public constructor(url?: string, options: RedisStorageOptions = {}) {\n const { keyPrefix = \"unikvs:\", ...redisOptions } = options;\n\n this.name = \"Redis\";\n this.#con = null;\n this.#url = url;\n this.#options = redisOptions;\n this.#keyPrefix = keyPrefix;\n }\n\n /**\n * ストレージがオープン済みかどうかです。\n *\n * @returns オープン済みなら真です。\n */\n public get isOpen(): boolean {\n return this.#con !== null;\n }\n\n /**\n * ストレージをオープンします。\n *\n * Bun の Redis クライアントを接続します。\n *\n * @param _c オープンコンテキストです。\n *\n * @returns オープンが完了したら解決します。\n */\n public async open(_c: IStorage.OpenContext): Promise<void> {\n // Bun 以外のランタイムで誤って使われた場合に、原因の分かるエラーを返します。\n if (typeof Bun === \"undefined\") {\n throw new UnsupportedRuntimeError({\n name: this.name,\n runtime: \"Bun\",\n });\n }\n\n // オープン済みの場合は古い接続を閉じ、開き直しでリークしないようにします。\n if (this.#con) {\n this.close(_c as unknown as IStorage.CloseContext);\n }\n\n const { RedisClient: RedisClientImpl } = await import(\"bun\");\n const client = new RedisClientImpl(this.#url, this.#options);\n\n try {\n // 最初のコマンドまで接続しないクライアントのため、open の時点で接続を確立します。\n await client.connect();\n } catch (ex) {\n client.close();\n throw ex;\n }\n\n this.#con = { client };\n }\n\n /**\n * ストレージをクローズします。\n *\n * クライアントを閉じて参照を外します。\n *\n * @param _c クローズコンテキストです。\n */\n public close(_c: IStorage.CloseContext): void {\n this.#getConnection().client.close();\n this.#con = null;\n }\n\n /**\n * バイト列を書き込みます。\n *\n * 指定したキーで値を上書きします。\n *\n * @param c 書き込みコンテキストです。\n * @param data 保存するバイト列です。\n *\n * @returns 書き込みが完了したら解決します。\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 // Bun のクライアントは ArrayBufferView をそのままバイナリーとして送信します。\n await client.set(this.#toKey(c.key), data);\n }\n\n /**\n * バイト列を読み取ります。\n *\n * 指定したキーの値を取得します。\n *\n * @param c 読み取りコンテキストです。\n *\n * @returns 読み取ったバイト列です。\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 // Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。\n return new Uint8Array(data);\n }\n\n /**\n * キーの存在を確認します。\n *\n * @param c 存在確認コンテキストです。\n *\n * @returns 存在すれば真です。\n */\n public async exists(c: IStorage.ExistsContext): Promise<boolean> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n // Bun のクライアントは EXISTS の整数を真偽値に変換して返します。\n return client.exists(this.#toKey(c.key));\n }\n\n /**\n * キーを削除します。\n *\n * 存在しないキーも成功扱いにします。\n *\n * @param c 削除コンテキストです。\n *\n * @returns 削除が完了したら解決します。\n */\n public async delete(c: IStorage.DeleteContext): Promise<void> {\n const { client } = this.#getConnection();\n\n c.signal.throwIfAborted();\n\n // 存在しないキーの削除は 0 を返すだけでエラーになりません。\n await client.del(this.#toKey(c.key));\n }\n\n /**\n * 接頭辞に一致するキーをすべて削除します。\n *\n * `SCAN` で列挙しながら削除します。\n *\n * @param c クリアコンテキストです。\n *\n * @returns 削除が完了したら解決します。\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\n let cursor = \"0\";\n\n do {\n c.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 /**\n * 書き込み用ストリームを取得します。\n *\n * 一時キーに追記し、`close` 時に最終キーへ置き換えます。\n *\n * @param c 書き込みストリーム取得コンテキストです。\n *\n * @returns 書き込み用ストリームです。\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}.${Bun.randomUUIDv7()}.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, chunk);\n } catch (ex) {\n await finish();\n throw ex;\n }\n },\n });\n }\n\n /**\n * 読み取り用ストリームを取得します。\n *\n * 取得した値を単一チャンクとして送出します。\n *\n * @param c 読み取りストリーム取得コンテキストです。\n *\n * @returns 読み取り用ストリームです。\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 // Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。\n controller.enqueue(new Uint8Array(data));\n controller.close();\n },\n });\n }\n\n /**\n * オープン済みかを確認します。\n *\n * 未オープン時は例外を投げます。\n *\n * @returns オープン済みの接続です。\n */\n #getConnection(): Connection {\n if (this.#con === null) {\n throw new TypeError(\"Redis is not open. Call open() first.\");\n }\n\n return this.#con;\n }\n\n /**\n * unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。\n *\n * @param key 変換する論理キーです。\n *\n * @returns Redis 上の実キーです。\n */\n #toKey(key: string): string {\n return `${this.#keyPrefix}${key}`;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,44 @@
1
1
  {
2
2
  "name": "@unikvs/redis.bun",
3
- "version": "0.0.0",
4
- "description": "OIDC trusted publishing setup package for @unikvs/redis.bun",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
3
+ "version": "0.0.14",
4
+ "description": "Storage plugin that stores byte arrays in Redis on Bun.",
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.bun"
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.14"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "1.4.3",
31
+ "@unikvs/testing": "0.0.0",
32
+ "@vitest/coverage-istanbul": "5.0.5",
33
+ "@vitest/coverage-v8": "5.0.5",
34
+ "valibot": "1.5.0",
35
+ "vite-plus": "1.3.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@logtape/logtape": "^2.0.0"
39
+ },
40
+ "readme": "README.md",
41
+ "scripts": {
42
+ "build": "vp -C .config pack"
43
+ }
44
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type { KeyNotFoundErrorArgs, KeyNotFoundErrorMeta } from "@unikvs/core";
2
+ export { KeyNotFoundError } from "@unikvs/core";
3
+ export type { UnsupportedRuntimeErrorArgs, UnsupportedRuntimeErrorMeta } from "@unikvs/core";
4
+ export { UnsupportedRuntimeError } from "@unikvs/core";
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type { KeyNotFoundErrorArgs, KeyNotFoundErrorMeta } from "./errors.js";
2
+ export { KeyNotFoundError, UnsupportedRuntimeError } from "./errors.js";
3
+
4
+ export type * from "./redis.js";
5
+ export { default as Redis } from "./redis.js";
package/src/redis.ts ADDED
@@ -0,0 +1,393 @@
1
+ import { type IStorage, KeyNotFoundError, UnsupportedRuntimeError } from "@unikvs/core";
2
+ import type { RedisClient, RedisOptions } from "bun";
3
+
4
+ /**
5
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions)
6
+ */
7
+ export type RedisStorageOptions = RedisOptions & {
8
+ /**
9
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions-keyprefix)
10
+ */
11
+ readonly keyPrefix?: string;
12
+ };
13
+
14
+ /**
15
+ * ストレージの動作に必要な接続を保持する型定義です。
16
+ */
17
+ type Connection = {
18
+ /**
19
+ * Bun の Redis クライアントです。
20
+ */
21
+ readonly client: RedisClient;
22
+ };
23
+
24
+ /**
25
+ * Redis の `SCAN` 1 回あたりにヒントとして渡すキー数です。
26
+ * 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。
27
+ */
28
+ const SCAN_COUNT = 1000;
29
+
30
+ /**
31
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
32
+ */
33
+ export default class Redis implements IStorage {
34
+ /**
35
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
36
+ */
37
+ public readonly name: string;
38
+
39
+ /**
40
+ * Redis クライアントのインスタンスを保持します。
41
+ *
42
+ * ストレージがオープンされるまで null です。
43
+ */
44
+ #con: Connection | null;
45
+
46
+ /**
47
+ * 接続先の URL です。
48
+ *
49
+ * 未指定の場合はクライアントが環境変数 (`REDIS_URL`、`VALKEY_URL`) から解決します。
50
+ */
51
+ readonly #url: string | undefined;
52
+
53
+ /**
54
+ * `RedisClient` の初期化に使用する設定オブジェクトです。
55
+ */
56
+ readonly #options: RedisOptions;
57
+
58
+ /**
59
+ * すべてのキーの先頭に付与するプレフィックスです。
60
+ */
61
+ readonly #keyPrefix: string;
62
+
63
+ /**
64
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
65
+ */
66
+ public constructor(url?: string, options: RedisStorageOptions = {}) {
67
+ const { keyPrefix = "unikvs:", ...redisOptions } = options;
68
+
69
+ this.name = "Redis";
70
+ this.#con = null;
71
+ this.#url = url;
72
+ this.#options = redisOptions;
73
+ this.#keyPrefix = keyPrefix;
74
+ }
75
+
76
+ /**
77
+ * ストレージがオープン済みかどうかです。
78
+ *
79
+ * @returns オープン済みなら真です。
80
+ */
81
+ public get isOpen(): boolean {
82
+ return this.#con !== null;
83
+ }
84
+
85
+ /**
86
+ * ストレージをオープンします。
87
+ *
88
+ * Bun の Redis クライアントを接続します。
89
+ *
90
+ * @param _c オープンコンテキストです。
91
+ *
92
+ * @returns オープンが完了したら解決します。
93
+ */
94
+ public async open(_c: IStorage.OpenContext): Promise<void> {
95
+ // Bun 以外のランタイムで誤って使われた場合に、原因の分かるエラーを返します。
96
+ if (typeof Bun === "undefined") {
97
+ throw new UnsupportedRuntimeError({
98
+ name: this.name,
99
+ runtime: "Bun",
100
+ });
101
+ }
102
+
103
+ // オープン済みの場合は古い接続を閉じ、開き直しでリークしないようにします。
104
+ if (this.#con) {
105
+ this.close(_c as unknown as IStorage.CloseContext);
106
+ }
107
+
108
+ const { RedisClient: RedisClientImpl } = await import("bun");
109
+ const client = new RedisClientImpl(this.#url, this.#options);
110
+
111
+ try {
112
+ // 最初のコマンドまで接続しないクライアントのため、open の時点で接続を確立します。
113
+ await client.connect();
114
+ } catch (ex) {
115
+ client.close();
116
+ throw ex;
117
+ }
118
+
119
+ this.#con = { client };
120
+ }
121
+
122
+ /**
123
+ * ストレージをクローズします。
124
+ *
125
+ * クライアントを閉じて参照を外します。
126
+ *
127
+ * @param _c クローズコンテキストです。
128
+ */
129
+ public close(_c: IStorage.CloseContext): void {
130
+ this.#getConnection().client.close();
131
+ this.#con = null;
132
+ }
133
+
134
+ /**
135
+ * バイト列を書き込みます。
136
+ *
137
+ * 指定したキーで値を上書きします。
138
+ *
139
+ * @param c 書き込みコンテキストです。
140
+ * @param data 保存するバイト列です。
141
+ *
142
+ * @returns 書き込みが完了したら解決します。
143
+ */
144
+ public async write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void> {
145
+ const { client } = this.#getConnection();
146
+
147
+ c.signal.throwIfAborted();
148
+
149
+ // Bun のクライアントは ArrayBufferView をそのままバイナリーとして送信します。
150
+ await client.set(this.#toKey(c.key), data);
151
+ }
152
+
153
+ /**
154
+ * バイト列を読み取ります。
155
+ *
156
+ * 指定したキーの値を取得します。
157
+ *
158
+ * @param c 読み取りコンテキストです。
159
+ *
160
+ * @returns 読み取ったバイト列です。
161
+ */
162
+ public async read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>> {
163
+ const { client } = this.#getConnection();
164
+
165
+ c.signal.throwIfAborted();
166
+
167
+ const data = await client.getBuffer(this.#toKey(c.key));
168
+
169
+ if (data === null) {
170
+ throw new KeyNotFoundError({ key: c.key });
171
+ }
172
+
173
+ // Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。
174
+ return new Uint8Array(data);
175
+ }
176
+
177
+ /**
178
+ * キーの存在を確認します。
179
+ *
180
+ * @param c 存在確認コンテキストです。
181
+ *
182
+ * @returns 存在すれば真です。
183
+ */
184
+ public async exists(c: IStorage.ExistsContext): Promise<boolean> {
185
+ const { client } = this.#getConnection();
186
+
187
+ c.signal.throwIfAborted();
188
+
189
+ // Bun のクライアントは EXISTS の整数を真偽値に変換して返します。
190
+ return client.exists(this.#toKey(c.key));
191
+ }
192
+
193
+ /**
194
+ * キーを削除します。
195
+ *
196
+ * 存在しないキーも成功扱いにします。
197
+ *
198
+ * @param c 削除コンテキストです。
199
+ *
200
+ * @returns 削除が完了したら解決します。
201
+ */
202
+ public async delete(c: IStorage.DeleteContext): Promise<void> {
203
+ const { client } = this.#getConnection();
204
+
205
+ c.signal.throwIfAborted();
206
+
207
+ // 存在しないキーの削除は 0 を返すだけでエラーになりません。
208
+ await client.del(this.#toKey(c.key));
209
+ }
210
+
211
+ /**
212
+ * 接頭辞に一致するキーをすべて削除します。
213
+ *
214
+ * `SCAN` で列挙しながら削除します。
215
+ *
216
+ * @param c クリアコンテキストです。
217
+ *
218
+ * @returns 削除が完了したら解決します。
219
+ */
220
+ public async clear(c: IStorage.ClearContext): Promise<void> {
221
+ const { client } = this.#getConnection();
222
+
223
+ c.signal.throwIfAborted();
224
+
225
+ // prefix が空文字の場合は MATCH "*" となり、選択中のデータベースの全キーが対象になります。
226
+ const pattern = `${this.#keyPrefix}*`;
227
+
228
+ let cursor = "0";
229
+
230
+ do {
231
+ c.signal.throwIfAborted();
232
+
233
+ const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
234
+
235
+ if (keys.length > 0) {
236
+ await client.del(...keys);
237
+ }
238
+
239
+ cursor = nextCursor;
240
+ } while (cursor !== "0");
241
+ }
242
+
243
+ /**
244
+ * 書き込み用ストリームを取得します。
245
+ *
246
+ * 一時キーに追記し、`close` 時に最終キーへ置き換えます。
247
+ *
248
+ * @param c 書き込みストリーム取得コンテキストです。
249
+ *
250
+ * @returns 書き込み用ストリームです。
251
+ */
252
+ public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>> {
253
+ const { client } = this.#getConnection();
254
+
255
+ c.signal.throwIfAborted();
256
+
257
+ const dest = this.#toKey(c.key);
258
+ // 一意なサフィックスにより、同一キーへの並行書き込みでも一時キーが衝突しないようにします。
259
+ const tmp = `${dest}.${Bun.randomUUIDv7()}.tmp`;
260
+
261
+ // 中断と失敗のどちらからでも後始末が走るため、完了処理は一度だけ行います。
262
+ let finished = false;
263
+ const removeAbortListener = (): void => {
264
+ c.signal.removeEventListener("abort", onAbort);
265
+ };
266
+ const discardTemporary = async (): Promise<void> => {
267
+ try {
268
+ await client.del(tmp);
269
+ } catch {
270
+ // 後始末としての削除なので、失敗しても無視します。
271
+ }
272
+ };
273
+ const finish = async (): Promise<void> => {
274
+ if (finished) {
275
+ return;
276
+ }
277
+
278
+ finished = true;
279
+ removeAbortListener();
280
+ await discardTemporary();
281
+ };
282
+ const onAbort = (): void => {
283
+ void finish();
284
+ };
285
+ c.signal.addEventListener("abort", onAbort, { once: true });
286
+
287
+ return new WritableStream<Uint8Array<ArrayBuffer>>({
288
+ async abort() {
289
+ await finish();
290
+ },
291
+ async close() {
292
+ c.signal.throwIfAborted();
293
+ try {
294
+ // 全チャンクの追記が完了した時点で初めて最終キーへ置き換えます (swap-on-close)。
295
+ await client.rename(tmp, dest);
296
+ } catch (ex) {
297
+ await finish();
298
+ throw ex;
299
+ }
300
+ // rename 後は一時キーが存在しないため、完了済みとして中断時の削除を行いません。
301
+ finished = true;
302
+ removeAbortListener();
303
+ },
304
+ async start() {
305
+ try {
306
+ c.signal.throwIfAborted();
307
+ // 0 バイトの書き込みでも close 時の RENAME が成功するよう、空の文字列で一時キーを作ります。
308
+ await client.set(tmp, "");
309
+ } catch (ex) {
310
+ await finish();
311
+ throw ex;
312
+ }
313
+ },
314
+ async write(chunk) {
315
+ c.signal.throwIfAborted();
316
+ try {
317
+ await client.append(tmp, chunk);
318
+ } catch (ex) {
319
+ await finish();
320
+ throw ex;
321
+ }
322
+ },
323
+ });
324
+ }
325
+
326
+ /**
327
+ * 読み取り用ストリームを取得します。
328
+ *
329
+ * 取得した値を単一チャンクとして送出します。
330
+ *
331
+ * @param c 読み取りストリーム取得コンテキストです。
332
+ *
333
+ * @returns 読み取り用ストリームです。
334
+ */
335
+ public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>> {
336
+ const { client } = this.#getConnection();
337
+
338
+ c.signal.throwIfAborted();
339
+
340
+ const dest = this.#toKey(c.key);
341
+ let sent = false;
342
+
343
+ // Redis の GET は値を一括で返すため、取得したバイト列を単一のチャンクとして送出します。
344
+ return new ReadableStream<Uint8Array<ArrayBuffer>>({
345
+ async pull(controller) {
346
+ if (sent) {
347
+ controller.close();
348
+
349
+ return;
350
+ }
351
+
352
+ sent = true;
353
+ c.signal.throwIfAborted();
354
+
355
+ const data = await client.getBuffer(dest);
356
+
357
+ if (data === null) {
358
+ throw new KeyNotFoundError({ key: c.key });
359
+ }
360
+
361
+ // Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。
362
+ controller.enqueue(new Uint8Array(data));
363
+ controller.close();
364
+ },
365
+ });
366
+ }
367
+
368
+ /**
369
+ * オープン済みかを確認します。
370
+ *
371
+ * 未オープン時は例外を投げます。
372
+ *
373
+ * @returns オープン済みの接続です。
374
+ */
375
+ #getConnection(): Connection {
376
+ if (this.#con === null) {
377
+ throw new TypeError("Redis is not open. Call open() first.");
378
+ }
379
+
380
+ return this.#con;
381
+ }
382
+
383
+ /**
384
+ * unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。
385
+ *
386
+ * @param key 変換する論理キーです。
387
+ *
388
+ * @returns Redis 上の実キーです。
389
+ */
390
+ #toKey(key: string): string {
391
+ return `${this.#keyPrefix}${key}`;
392
+ }
393
+ }