@unikvs/redis.bun 0.0.0-stage → 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 +2 -2
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +212 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -3
- package/src/errors.ts +4 -0
- package/src/index.ts +5 -0
- package/src/redis.ts +302 -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,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @unikvs/redis.bun
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Document](https://tai-kun.github.io/unikvs/)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
* すべてのキーの先頭に付与するプレフィックスです。
|
|
10
|
+
*
|
|
11
|
+
* キーを名前空間で分離し、`clear()` が削除する範囲を限定します。空文字を指定するとキーをそのまま使います。
|
|
12
|
+
*
|
|
13
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions-keyprefix)
|
|
14
|
+
*/
|
|
15
|
+
readonly keyPrefix?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
|
|
19
|
+
*/
|
|
20
|
+
declare class Redis implements IStorage {
|
|
21
|
+
#private;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/**
|
|
24
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
|
|
25
|
+
*/
|
|
26
|
+
constructor(url?: string, options?: RedisStorageOptions);
|
|
27
|
+
get isOpen(): boolean;
|
|
28
|
+
open(_c: IStorage.OpenContext): Promise<void>;
|
|
29
|
+
close(_c: IStorage.CloseContext): void;
|
|
30
|
+
write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void>;
|
|
31
|
+
read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>>;
|
|
32
|
+
exists(c: IStorage.ExistsContext): Promise<boolean>;
|
|
33
|
+
delete(c: IStorage.DeleteContext): Promise<void>;
|
|
34
|
+
clear(c: IStorage.ClearContext): Promise<void>;
|
|
35
|
+
getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>>;
|
|
36
|
+
getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { KeyNotFoundError, type KeyNotFoundErrorArgs, type KeyNotFoundErrorMeta, Redis, type RedisStorageOptions, UnsupportedRuntimeError };
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/redis.ts"],"mappings":";;;;;;KAMY,sBAAsB;;;;;;;;WAQvB;;;;;cAsBU,iBAAiB;;WAyBpB;;;;EAKhB,YAAmB,cAAc,UAAS;MAU/B;EAcX,KAAkB,IAAI,SAAS,cAAc;EA4B7C,MAAa,IAAI,SAAS;EAY1B,MAAmB,GAAG,SAAS,cAAc,MAAM,WAAW,eAAe;EAS7E,KAAkB,GAAG,SAAS,cAAc,QAAQ,WAAW;EAe/D,OAAoB,GAAG,SAAS,gBAAgB;EAShD,OAAoB,GAAG,SAAS,gBAAgB;EAShD,MAAmB,GAAG,SAAS,eAAe;EAuB9C,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW;EA0E9E,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
get isOpen() {
|
|
65
|
+
return _classPrivateFieldGet2(_con, this) !== null;
|
|
66
|
+
}
|
|
67
|
+
async open(_c) {
|
|
68
|
+
if (typeof Bun === "undefined") throw new UnsupportedRuntimeError$1({
|
|
69
|
+
name: this.name,
|
|
70
|
+
runtime: "Bun"
|
|
71
|
+
});
|
|
72
|
+
if (_classPrivateFieldGet2(_con, this)) this.close(_c);
|
|
73
|
+
const { RedisClient: RedisClientImpl } = await import("bun");
|
|
74
|
+
const client = new RedisClientImpl(_classPrivateFieldGet2(_url, this), _classPrivateFieldGet2(_options, this));
|
|
75
|
+
try {
|
|
76
|
+
await client.connect();
|
|
77
|
+
} catch (ex) {
|
|
78
|
+
client.close();
|
|
79
|
+
throw ex;
|
|
80
|
+
}
|
|
81
|
+
_classPrivateFieldSet2(_con, this, { client });
|
|
82
|
+
}
|
|
83
|
+
close(_c) {
|
|
84
|
+
_assertClassBrand(_Redis_brand, this, _getConnection).call(this).client.close();
|
|
85
|
+
_classPrivateFieldSet2(_con, this, null);
|
|
86
|
+
}
|
|
87
|
+
async write(c, data) {
|
|
88
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
89
|
+
c.signal.throwIfAborted();
|
|
90
|
+
await client.set(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key), data);
|
|
91
|
+
}
|
|
92
|
+
async read(c) {
|
|
93
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
94
|
+
c.signal.throwIfAborted();
|
|
95
|
+
const data = await client.getBuffer(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
|
|
96
|
+
if (data === null) throw new KeyNotFoundError$1({ key: c.key });
|
|
97
|
+
return new Uint8Array(data);
|
|
98
|
+
}
|
|
99
|
+
async exists(c) {
|
|
100
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
101
|
+
c.signal.throwIfAborted();
|
|
102
|
+
return client.exists(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
|
|
103
|
+
}
|
|
104
|
+
async delete(c) {
|
|
105
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
106
|
+
c.signal.throwIfAborted();
|
|
107
|
+
await client.del(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
|
|
108
|
+
}
|
|
109
|
+
async clear(c) {
|
|
110
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
111
|
+
c.signal.throwIfAborted();
|
|
112
|
+
const pattern = `${_classPrivateFieldGet2(_keyPrefix, this)}*`;
|
|
113
|
+
let cursor = "0";
|
|
114
|
+
do {
|
|
115
|
+
c.signal.throwIfAborted();
|
|
116
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
|
|
117
|
+
if (keys.length > 0) await client.del(...keys);
|
|
118
|
+
cursor = nextCursor;
|
|
119
|
+
} while (cursor !== "0");
|
|
120
|
+
}
|
|
121
|
+
getWritable(c) {
|
|
122
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
123
|
+
c.signal.throwIfAborted();
|
|
124
|
+
const dest = _assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key);
|
|
125
|
+
const tmp = `${dest}.${Bun.randomUUIDv7()}.tmp`;
|
|
126
|
+
let finished = false;
|
|
127
|
+
const removeAbortListener = () => {
|
|
128
|
+
c.signal.removeEventListener("abort", onAbort);
|
|
129
|
+
};
|
|
130
|
+
const discardTemporary = async () => {
|
|
131
|
+
try {
|
|
132
|
+
await client.del(tmp);
|
|
133
|
+
} catch {}
|
|
134
|
+
};
|
|
135
|
+
const finish = async () => {
|
|
136
|
+
if (finished) return;
|
|
137
|
+
finished = true;
|
|
138
|
+
removeAbortListener();
|
|
139
|
+
await discardTemporary();
|
|
140
|
+
};
|
|
141
|
+
const onAbort = () => {
|
|
142
|
+
finish();
|
|
143
|
+
};
|
|
144
|
+
c.signal.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
return new WritableStream({
|
|
146
|
+
async abort() {
|
|
147
|
+
await finish();
|
|
148
|
+
},
|
|
149
|
+
async close() {
|
|
150
|
+
c.signal.throwIfAborted();
|
|
151
|
+
try {
|
|
152
|
+
await client.rename(tmp, dest);
|
|
153
|
+
} catch (ex) {
|
|
154
|
+
await finish();
|
|
155
|
+
throw ex;
|
|
156
|
+
}
|
|
157
|
+
finished = true;
|
|
158
|
+
removeAbortListener();
|
|
159
|
+
},
|
|
160
|
+
async start() {
|
|
161
|
+
try {
|
|
162
|
+
c.signal.throwIfAborted();
|
|
163
|
+
await client.set(tmp, "");
|
|
164
|
+
} catch (ex) {
|
|
165
|
+
await finish();
|
|
166
|
+
throw ex;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
async write(chunk) {
|
|
170
|
+
c.signal.throwIfAborted();
|
|
171
|
+
try {
|
|
172
|
+
await client.append(tmp, chunk);
|
|
173
|
+
} catch (ex) {
|
|
174
|
+
await finish();
|
|
175
|
+
throw ex;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
getReadable(c) {
|
|
181
|
+
const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
|
|
182
|
+
c.signal.throwIfAborted();
|
|
183
|
+
const dest = _assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key);
|
|
184
|
+
let sent = false;
|
|
185
|
+
return new ReadableStream({ async pull(controller) {
|
|
186
|
+
if (sent) {
|
|
187
|
+
controller.close();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
sent = true;
|
|
191
|
+
c.signal.throwIfAborted();
|
|
192
|
+
const data = await client.getBuffer(dest);
|
|
193
|
+
if (data === null) throw new KeyNotFoundError$1({ key: c.key });
|
|
194
|
+
controller.enqueue(new Uint8Array(data));
|
|
195
|
+
controller.close();
|
|
196
|
+
} });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
function _getConnection() {
|
|
200
|
+
if (_classPrivateFieldGet2(_con, this) === null) throw new TypeError("Redis is not open. Call open() first.");
|
|
201
|
+
return _classPrivateFieldGet2(_con, this);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。
|
|
205
|
+
*/
|
|
206
|
+
function _toKey(key) {
|
|
207
|
+
return `${_classPrivateFieldGet2(_keyPrefix, this)}${key}`;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
export { KeyNotFoundError, Redis, UnsupportedRuntimeError };
|
|
211
|
+
|
|
212
|
+
//# 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 * すべてのキーの先頭に付与するプレフィックスです。\n *\n * キーを名前空間で分離し、`clear()` が削除する範囲を限定します。空文字を指定するとキーをそのまま使います。\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 * 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 public readonly name: 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 public get isOpen(): boolean {\n return this.#con !== null;\n }\n\n // オープン済みかを確認します。\n // 未オープン時は `TypeError` を投げます。\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 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 public close(_c: IStorage.CloseContext): void {\n this.#getConnection().client.close();\n this.#con = null;\n }\n\n /**\n * unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。\n */\n #toKey(key: string): string {\n return `${this.#keyPrefix}${key}`;\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 // Bun のクライアントは ArrayBufferView をそのままバイナリーとして送信します。\n await client.set(this.#toKey(c.key), 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 // Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。\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 // Bun のクライアントは EXISTS の整数を真偽値に変換して返します。\n return client.exists(this.#toKey(c.key));\n }\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 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 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 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"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unikvs/redis.bun",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.13",
|
|
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.13"
|
|
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
|
+
}
|
|
6
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
package/src/redis.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
* すべてのキーの先頭に付与するプレフィックスです。
|
|
10
|
+
*
|
|
11
|
+
* キーを名前空間で分離し、`clear()` が削除する範囲を限定します。空文字を指定するとキーをそのまま使います。
|
|
12
|
+
*
|
|
13
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redisstorageoptions-keyprefix)
|
|
14
|
+
*/
|
|
15
|
+
readonly keyPrefix?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ストレージの動作に必要な接続を保持する型定義です。
|
|
20
|
+
*/
|
|
21
|
+
type Connection = {
|
|
22
|
+
/**
|
|
23
|
+
* Bun の Redis クライアントです。
|
|
24
|
+
*/
|
|
25
|
+
readonly client: RedisClient;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Redis の `SCAN` 1 回あたりにヒントとして渡すキー数です。
|
|
30
|
+
* 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。
|
|
31
|
+
*/
|
|
32
|
+
const SCAN_COUNT = 1000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
|
|
36
|
+
*/
|
|
37
|
+
export default class Redis implements IStorage {
|
|
38
|
+
/**
|
|
39
|
+
* Redis クライアントのインスタンスを保持します。
|
|
40
|
+
*
|
|
41
|
+
* ストレージがオープンされるまで null です。
|
|
42
|
+
*/
|
|
43
|
+
#con: Connection | null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 接続先の URL です。
|
|
47
|
+
*
|
|
48
|
+
* 未指定の場合はクライアントが環境変数 (`REDIS_URL`、`VALKEY_URL`) から解決します。
|
|
49
|
+
*/
|
|
50
|
+
readonly #url: string | undefined;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `RedisClient` の初期化に使用する設定オブジェクトです。
|
|
54
|
+
*/
|
|
55
|
+
readonly #options: RedisOptions;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* すべてのキーの先頭に付与するプレフィックスです。
|
|
59
|
+
*/
|
|
60
|
+
readonly #keyPrefix: string;
|
|
61
|
+
|
|
62
|
+
public readonly name: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/redis-bun#redis)
|
|
66
|
+
*/
|
|
67
|
+
public constructor(url?: string, options: RedisStorageOptions = {}) {
|
|
68
|
+
const { keyPrefix = "unikvs:", ...redisOptions } = options;
|
|
69
|
+
|
|
70
|
+
this.name = "Redis";
|
|
71
|
+
this.#con = null;
|
|
72
|
+
this.#url = url;
|
|
73
|
+
this.#options = redisOptions;
|
|
74
|
+
this.#keyPrefix = keyPrefix;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public get isOpen(): boolean {
|
|
78
|
+
return this.#con !== null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// オープン済みかを確認します。
|
|
82
|
+
// 未オープン時は `TypeError` を投げます。
|
|
83
|
+
#getConnection(): Connection {
|
|
84
|
+
if (this.#con === null) {
|
|
85
|
+
throw new TypeError("Redis is not open. Call open() first.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.#con;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public async open(_c: IStorage.OpenContext): Promise<void> {
|
|
92
|
+
// Bun 以外のランタイムで誤って使われた場合に、原因の分かるエラーを返します。
|
|
93
|
+
if (typeof Bun === "undefined") {
|
|
94
|
+
throw new UnsupportedRuntimeError({
|
|
95
|
+
name: this.name,
|
|
96
|
+
runtime: "Bun",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// オープン済みの場合は古い接続を閉じ、開き直しでリークしないようにします。
|
|
101
|
+
if (this.#con) {
|
|
102
|
+
this.close(_c as unknown as IStorage.CloseContext);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { RedisClient: RedisClientImpl } = await import("bun");
|
|
106
|
+
const client = new RedisClientImpl(this.#url, this.#options);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// 最初のコマンドまで接続しないクライアントのため、open の時点で接続を確立します。
|
|
110
|
+
await client.connect();
|
|
111
|
+
} catch (ex) {
|
|
112
|
+
client.close();
|
|
113
|
+
throw ex;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.#con = { client };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public close(_c: IStorage.CloseContext): void {
|
|
120
|
+
this.#getConnection().client.close();
|
|
121
|
+
this.#con = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。
|
|
126
|
+
*/
|
|
127
|
+
#toKey(key: string): string {
|
|
128
|
+
return `${this.#keyPrefix}${key}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void> {
|
|
132
|
+
const { client } = this.#getConnection();
|
|
133
|
+
|
|
134
|
+
c.signal.throwIfAborted();
|
|
135
|
+
|
|
136
|
+
// Bun のクライアントは ArrayBufferView をそのままバイナリーとして送信します。
|
|
137
|
+
await client.set(this.#toKey(c.key), data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public async read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>> {
|
|
141
|
+
const { client } = this.#getConnection();
|
|
142
|
+
|
|
143
|
+
c.signal.throwIfAborted();
|
|
144
|
+
|
|
145
|
+
const data = await client.getBuffer(this.#toKey(c.key));
|
|
146
|
+
|
|
147
|
+
if (data === null) {
|
|
148
|
+
throw new KeyNotFoundError({ key: c.key });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。
|
|
152
|
+
return new Uint8Array(data);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public async exists(c: IStorage.ExistsContext): Promise<boolean> {
|
|
156
|
+
const { client } = this.#getConnection();
|
|
157
|
+
|
|
158
|
+
c.signal.throwIfAborted();
|
|
159
|
+
|
|
160
|
+
// Bun のクライアントは EXISTS の整数を真偽値に変換して返します。
|
|
161
|
+
return client.exists(this.#toKey(c.key));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async delete(c: IStorage.DeleteContext): Promise<void> {
|
|
165
|
+
const { client } = this.#getConnection();
|
|
166
|
+
|
|
167
|
+
c.signal.throwIfAborted();
|
|
168
|
+
|
|
169
|
+
// 存在しないキーの削除は 0 を返すだけでエラーになりません。
|
|
170
|
+
await client.del(this.#toKey(c.key));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public async clear(c: IStorage.ClearContext): Promise<void> {
|
|
174
|
+
const { client } = this.#getConnection();
|
|
175
|
+
|
|
176
|
+
c.signal.throwIfAborted();
|
|
177
|
+
|
|
178
|
+
// prefix が空文字の場合は MATCH "*" となり、選択中のデータベースの全キーが対象になります。
|
|
179
|
+
const pattern = `${this.#keyPrefix}*`;
|
|
180
|
+
|
|
181
|
+
let cursor = "0";
|
|
182
|
+
|
|
183
|
+
do {
|
|
184
|
+
c.signal.throwIfAborted();
|
|
185
|
+
|
|
186
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
|
|
187
|
+
|
|
188
|
+
if (keys.length > 0) {
|
|
189
|
+
await client.del(...keys);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
cursor = nextCursor;
|
|
193
|
+
} while (cursor !== "0");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>> {
|
|
197
|
+
const { client } = this.#getConnection();
|
|
198
|
+
|
|
199
|
+
c.signal.throwIfAborted();
|
|
200
|
+
|
|
201
|
+
const dest = this.#toKey(c.key);
|
|
202
|
+
// 一意なサフィックスにより、同一キーへの並行書き込みでも一時キーが衝突しないようにします。
|
|
203
|
+
const tmp = `${dest}.${Bun.randomUUIDv7()}.tmp`;
|
|
204
|
+
|
|
205
|
+
// 中断と失敗のどちらからでも後始末が走るため、完了処理は一度だけ行います。
|
|
206
|
+
let finished = false;
|
|
207
|
+
const removeAbortListener = (): void => {
|
|
208
|
+
c.signal.removeEventListener("abort", onAbort);
|
|
209
|
+
};
|
|
210
|
+
const discardTemporary = async (): Promise<void> => {
|
|
211
|
+
try {
|
|
212
|
+
await client.del(tmp);
|
|
213
|
+
} catch {
|
|
214
|
+
// 後始末としての削除なので、失敗しても無視します。
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const finish = async (): Promise<void> => {
|
|
218
|
+
if (finished) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
finished = true;
|
|
223
|
+
removeAbortListener();
|
|
224
|
+
await discardTemporary();
|
|
225
|
+
};
|
|
226
|
+
const onAbort = (): void => {
|
|
227
|
+
void finish();
|
|
228
|
+
};
|
|
229
|
+
c.signal.addEventListener("abort", onAbort, { once: true });
|
|
230
|
+
|
|
231
|
+
return new WritableStream<Uint8Array<ArrayBuffer>>({
|
|
232
|
+
async abort() {
|
|
233
|
+
await finish();
|
|
234
|
+
},
|
|
235
|
+
async close() {
|
|
236
|
+
c.signal.throwIfAborted();
|
|
237
|
+
try {
|
|
238
|
+
// 全チャンクの追記が完了した時点で初めて最終キーへ置き換えます (swap-on-close)。
|
|
239
|
+
await client.rename(tmp, dest);
|
|
240
|
+
} catch (ex) {
|
|
241
|
+
await finish();
|
|
242
|
+
throw ex;
|
|
243
|
+
}
|
|
244
|
+
// rename 後は一時キーが存在しないため、完了済みとして中断時の削除を行いません。
|
|
245
|
+
finished = true;
|
|
246
|
+
removeAbortListener();
|
|
247
|
+
},
|
|
248
|
+
async start() {
|
|
249
|
+
try {
|
|
250
|
+
c.signal.throwIfAborted();
|
|
251
|
+
// 0 バイトの書き込みでも close 時の RENAME が成功するよう、空の文字列で一時キーを作ります。
|
|
252
|
+
await client.set(tmp, "");
|
|
253
|
+
} catch (ex) {
|
|
254
|
+
await finish();
|
|
255
|
+
throw ex;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
async write(chunk) {
|
|
259
|
+
c.signal.throwIfAborted();
|
|
260
|
+
try {
|
|
261
|
+
await client.append(tmp, chunk);
|
|
262
|
+
} catch (ex) {
|
|
263
|
+
await finish();
|
|
264
|
+
throw ex;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>> {
|
|
271
|
+
const { client } = this.#getConnection();
|
|
272
|
+
|
|
273
|
+
c.signal.throwIfAborted();
|
|
274
|
+
|
|
275
|
+
const dest = this.#toKey(c.key);
|
|
276
|
+
let sent = false;
|
|
277
|
+
|
|
278
|
+
// Redis の GET は値を一括で返すため、取得したバイト列を単一のチャンクとして送出します。
|
|
279
|
+
return new ReadableStream<Uint8Array<ArrayBuffer>>({
|
|
280
|
+
async pull(controller) {
|
|
281
|
+
if (sent) {
|
|
282
|
+
controller.close();
|
|
283
|
+
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
sent = true;
|
|
288
|
+
c.signal.throwIfAborted();
|
|
289
|
+
|
|
290
|
+
const data = await client.getBuffer(dest);
|
|
291
|
+
|
|
292
|
+
if (data === null) {
|
|
293
|
+
throw new KeyNotFoundError({ key: c.key });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Bun は Buffer を返すため、契約どおりの Uint8Array へ正規化します。
|
|
297
|
+
controller.enqueue(new Uint8Array(data));
|
|
298
|
+
controller.close();
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|