@unikvs/redis.node 0.0.13 → 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/dist/index.d.ts CHANGED
@@ -113,20 +113,108 @@ type RedisStorageOptions = Omit<RedisOptions, "keyPrefix" | "lazyConnect"> & {
113
113
  */
114
114
  declare class Redis implements IStorage {
115
115
  #private;
116
+ /**
117
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
118
+ */
116
119
  readonly name: string;
117
120
  /**
118
121
  * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
119
122
  */
120
123
  constructor(url?: string, options?: RedisStorageOptions);
124
+ /**
125
+ * ストレージがオープン済みかどうかです。
126
+ *
127
+ * @returns オープン済みなら真です。
128
+ */
121
129
  get isOpen(): boolean;
130
+ /**
131
+ * ストレージをオープンします。
132
+ *
133
+ * `ioredis` クライアントを作成して接続します。
134
+ *
135
+ * @param _c オープンコンテキストです。
136
+ *
137
+ * @returns オープンが完了したら解決します。
138
+ */
122
139
  open(_c: IStorage.OpenContext): Promise<void>;
140
+ /**
141
+ * ストレージをクローズします。
142
+ *
143
+ * `quit` で丁寧に閉じ、失敗時は `disconnect` で掃除します。
144
+ *
145
+ * @param _c クローズコンテキストです。
146
+ *
147
+ * @returns クローズが完了したら解決します。
148
+ */
123
149
  close(_c: IStorage.CloseContext): Promise<void>;
150
+ /**
151
+ * バイト列を書き込みます。
152
+ *
153
+ * 指定したキーで値を上書きします。
154
+ *
155
+ * @param c 書き込みコンテキストです。
156
+ * @param data 保存するバイト列です。
157
+ *
158
+ * @returns 書き込みが完了したら解決します。
159
+ */
124
160
  write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void>;
161
+ /**
162
+ * バイト列を読み取ります。
163
+ *
164
+ * 指定したキーの値を取得します。
165
+ *
166
+ * @param c 読み取りコンテキストです。
167
+ *
168
+ * @returns 読み取ったバイト列です。
169
+ */
125
170
  read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>>;
171
+ /**
172
+ * キーの存在を確認します。
173
+ *
174
+ * @param c 存在確認コンテキストです。
175
+ *
176
+ * @returns 存在すれば真です。
177
+ */
126
178
  exists(c: IStorage.ExistsContext): Promise<boolean>;
179
+ /**
180
+ * キーを削除します。
181
+ *
182
+ * 存在しないキーも成功扱いにします。
183
+ *
184
+ * @param c 削除コンテキストです。
185
+ *
186
+ * @returns 削除が完了したら解決します。
187
+ */
127
188
  delete(c: IStorage.DeleteContext): Promise<void>;
189
+ /**
190
+ * 接頭辞に一致するキーをすべて削除します。
191
+ *
192
+ * `SCAN` で列挙しながら削除します。
193
+ *
194
+ * @param c クリアコンテキストです。
195
+ *
196
+ * @returns 削除が完了したら解決します。
197
+ */
128
198
  clear(c: IStorage.ClearContext): Promise<void>;
199
+ /**
200
+ * 書き込み用ストリームを取得します。
201
+ *
202
+ * 一時キーに追記し、`close` 時に最終キーへ置き換えます。
203
+ *
204
+ * @param c 書き込みストリーム取得コンテキストです。
205
+ *
206
+ * @returns 書き込み用ストリームです。
207
+ */
129
208
  getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>>;
209
+ /**
210
+ * 読み取り用ストリームを取得します。
211
+ *
212
+ * 取得した値を単一チャンクとして送出します。
213
+ *
214
+ * @param c 読み取りストリーム取得コンテキストです。
215
+ *
216
+ * @returns 読み取り用ストリームです。
217
+ */
130
218
  getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>>;
131
219
  }
132
220
  //#endregion
@@ -1 +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"}
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;;;;;cAiCU,iBAAiB;;;;;WAIpB;;;;EAuChB,YAAmB,cAAc,UAAS;;;;;;MAiC/B;;;;;;;;;;EAaX,KAAkB,IAAI,SAAS,cAAc;;;;;;;;;;EAwE7C,MAAmB,IAAI,SAAS,eAAe;;;;;;;;;;;EAwC/C,MAAmB,GAAG,SAAS,cAAc,MAAM,WAAW,eAAe;;;;;;;;;;EAiB7E,KAAkB,GAAG,SAAS,cAAc,QAAQ,WAAW;;;;;;;;EAqB/D,OAAoB,GAAG,SAAS,gBAAgB;;;;;;;;;;EAiBhD,OAAoB,GAAG,SAAS,gBAAgB;;;;;;;;;;EAiBhD,MAAmB,GAAG,SAAS,eAAe;;;;;;;;;;EAmB9C,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW;;;;;;;;;;EAmF9E,YAAmB,GAAG,SAAS,qBAAqB,eAAe,WAAW"}
package/dist/index.js CHANGED
@@ -98,8 +98,19 @@ function _classPrivateFieldGet2(s, a) {
98
98
  }
99
99
  //#endregion
100
100
  //#region ../src/redis.ts
101
+ /**
102
+ * `SCAN` 1 回あたりにヒントとして渡すキー数です。
103
+ *
104
+ * 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。
105
+ */
101
106
  const SCAN_COUNT = 1e3;
107
+ /**
108
+ * 接続確立の既定タイムアウト (ミリ秒) です。
109
+ */
102
110
  const DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
111
+ /**
112
+ * 切断処理の既定タイムアウト (ミリ秒) です。
113
+ */
103
114
  const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
104
115
  var _con = /* @__PURE__ */ new WeakMap();
105
116
  var _url = /* @__PURE__ */ new WeakMap();
@@ -131,9 +142,23 @@ var Redis = class {
131
142
  _classPrivateFieldSet2(_closeTimeout, this, closeTimeout);
132
143
  _classPrivateFieldSet2(_keyPrefix, this, keyPrefix);
133
144
  }
145
+ /**
146
+ * ストレージがオープン済みかどうかです。
147
+ *
148
+ * @returns オープン済みなら真です。
149
+ */
134
150
  get isOpen() {
135
151
  return _classPrivateFieldGet2(_con, this) !== null;
136
152
  }
153
+ /**
154
+ * ストレージをオープンします。
155
+ *
156
+ * `ioredis` クライアントを作成して接続します。
157
+ *
158
+ * @param _c オープンコンテキストです。
159
+ *
160
+ * @returns オープンが完了したら解決します。
161
+ */
137
162
  async open(_c) {
138
163
  if (_classPrivateFieldGet2(_con, this)) await this.close(_c);
139
164
  const explicitUrl = _classPrivateFieldGet2(_url, this) === void 0 || _classPrivateFieldGet2(_url, this) === "" ? void 0 : _classPrivateFieldGet2(_url, this);
@@ -163,6 +188,15 @@ var Redis = class {
163
188
  }
164
189
  _classPrivateFieldSet2(_con, this, { client: raw });
165
190
  }
191
+ /**
192
+ * ストレージをクローズします。
193
+ *
194
+ * `quit` で丁寧に閉じ、失敗時は `disconnect` で掃除します。
195
+ *
196
+ * @param _c クローズコンテキストです。
197
+ *
198
+ * @returns クローズが完了したら解決します。
199
+ */
166
200
  async close(_c) {
167
201
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
168
202
  _classPrivateFieldSet2(_con, this, null);
@@ -180,11 +214,30 @@ var Redis = class {
180
214
  if (timer !== void 0) clearTimeout(timer);
181
215
  }
182
216
  }
217
+ /**
218
+ * バイト列を書き込みます。
219
+ *
220
+ * 指定したキーで値を上書きします。
221
+ *
222
+ * @param c 書き込みコンテキストです。
223
+ * @param data 保存するバイト列です。
224
+ *
225
+ * @returns 書き込みが完了したら解決します。
226
+ */
183
227
  async write(c, data) {
184
228
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
185
229
  c.signal.throwIfAborted();
186
230
  await client.set(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key), Buffer.from(data));
187
231
  }
232
+ /**
233
+ * バイト列を読み取ります。
234
+ *
235
+ * 指定したキーの値を取得します。
236
+ *
237
+ * @param c 読み取りコンテキストです。
238
+ *
239
+ * @returns 読み取ったバイト列です。
240
+ */
188
241
  async read(c) {
189
242
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
190
243
  c.signal.throwIfAborted();
@@ -192,22 +245,56 @@ var Redis = class {
192
245
  if (data === null) throw new KeyNotFoundError$1({ key: c.key });
193
246
  return new Uint8Array(data);
194
247
  }
248
+ /**
249
+ * キーの存在を確認します。
250
+ *
251
+ * @param c 存在確認コンテキストです。
252
+ *
253
+ * @returns 存在すれば真です。
254
+ */
195
255
  async exists(c) {
196
256
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
197
257
  c.signal.throwIfAborted();
198
258
  return await client.exists(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key)) > 0;
199
259
  }
260
+ /**
261
+ * キーを削除します。
262
+ *
263
+ * 存在しないキーも成功扱いにします。
264
+ *
265
+ * @param c 削除コンテキストです。
266
+ *
267
+ * @returns 削除が完了したら解決します。
268
+ */
200
269
  async delete(c) {
201
270
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
202
271
  c.signal.throwIfAborted();
203
272
  await client.del(_assertClassBrand(_Redis_brand, this, _toKey).call(this, c.key));
204
273
  }
274
+ /**
275
+ * 接頭辞に一致するキーをすべて削除します。
276
+ *
277
+ * `SCAN` で列挙しながら削除します。
278
+ *
279
+ * @param c クリアコンテキストです。
280
+ *
281
+ * @returns 削除が完了したら解決します。
282
+ */
205
283
  async clear(c) {
206
284
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
207
285
  c.signal.throwIfAborted();
208
286
  const pattern = `${_classPrivateFieldGet2(_keyPrefix, this)}*`;
209
287
  await _assertClassBrand(_Redis_brand, this, _clearPattern).call(this, client, pattern, c.signal);
210
288
  }
289
+ /**
290
+ * 書き込み用ストリームを取得します。
291
+ *
292
+ * 一時キーに追記し、`close` 時に最終キーへ置き換えます。
293
+ *
294
+ * @param c 書き込みストリーム取得コンテキストです。
295
+ *
296
+ * @returns 書き込み用ストリームです。
297
+ */
211
298
  getWritable(c) {
212
299
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
213
300
  c.signal.throwIfAborted();
@@ -267,6 +354,15 @@ var Redis = class {
267
354
  }
268
355
  });
269
356
  }
357
+ /**
358
+ * 読み取り用ストリームを取得します。
359
+ *
360
+ * 取得した値を単一チャンクとして送出します。
361
+ *
362
+ * @param c 読み取りストリーム取得コンテキストです。
363
+ *
364
+ * @returns 読み取り用ストリームです。
365
+ */
270
366
  getReadable(c) {
271
367
  const { client } = _assertClassBrand(_Redis_brand, this, _getConnection).call(this);
272
368
  c.signal.throwIfAborted();
@@ -286,14 +382,39 @@ var Redis = class {
286
382
  } });
287
383
  }
288
384
  };
385
+ /**
386
+ * 開いている接続を返します。
387
+ *
388
+ * 未オープン時は例外を投げます。
389
+ *
390
+ * @returns オープン済みの接続です。
391
+ */
289
392
  function _getConnection() {
290
393
  const con = _classPrivateFieldGet2(_con, this);
291
394
  if (con === null) throw new TypeError("Redis is not open. Call open() first.");
292
395
  return con;
293
396
  }
397
+ /**
398
+ * unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。
399
+ *
400
+ * @param key 変換する論理キーです。
401
+ *
402
+ * @returns Redis 上の実キーです。
403
+ */
294
404
  function _toKey(key) {
295
405
  return `${_classPrivateFieldGet2(_keyPrefix, this)}${key}`;
296
406
  }
407
+ /**
408
+ * 接頭辞に一致するキーを走査して削除します。
409
+ *
410
+ * `SCAN` で頁送りしながら `DEL` します。
411
+ *
412
+ * @param client 操作に使うクライアントです。
413
+ * @param pattern 削除対象の一致パターンです。
414
+ * @param signal 中断を知らせるシグナルです。
415
+ *
416
+ * @returns 削除が完了したら解決します。
417
+ */
297
418
  async function _clearPattern(client, pattern, signal) {
298
419
  let cursor = "0";
299
420
  do {
package/dist/index.js.map CHANGED
@@ -1 +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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"}
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\n/**\n * `SCAN` 1 回あたりにヒントとして渡すキー数です。\n *\n * 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。\n */\nconst SCAN_COUNT = 1000;\n\n/**\n * 接続確立の既定タイムアウト (ミリ秒) です。\n */\nconst DEFAULT_CONNECT_TIMEOUT_MS = 5000;\n\n/**\n * 切断処理の既定タイムアウト (ミリ秒) です。\n */\nconst DEFAULT_CLOSE_TIMEOUT_MS = 5000;\n\n/**\n * ストレージの動作に必要な接続を保持する型定義です。\n */\ntype Connection = {\n /**\n * 操作に使うクライアントです。\n */\n readonly client: Ioredis;\n};\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)\n */\nexport default class Redis implements IStorage {\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#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 * `closeTimeout` は本パッケージ固有の拡張です。\n *\n * `this.#options` には含めず、別フィールドで保管します。\n * `this.#options` は `new Ioredis` にそのまま spread できる透過バッグです。\n */\n readonly #options: Omit<RedisStorageOptions, \"keyPrefix\" | \"cluster\" | \"closeTimeout\">;\n\n /**\n * `close` 時の切断待ち時間 (ミリ秒) です。\n *\n * `ioredis` の `disconnectTimeout` (ソケット切断待機) とは別物です。\n */\n readonly #closeTimeout: number;\n\n /**\n * すべてのキーの先頭に付与するプレフィックスです。\n */\n readonly #keyPrefix: 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 /**\n * ストレージがオープン済みかどうかです。\n *\n * @returns オープン済みなら真です。\n */\n public get isOpen(): boolean {\n return this.#con !== null;\n }\n\n /**\n * ストレージをオープンします。\n *\n * `ioredis` クライアントを作成して接続します。\n *\n * @param _c オープンコンテキストです。\n *\n * @returns オープンが完了したら解決します。\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 /**\n * ストレージをクローズします。\n *\n * `quit` で丁寧に閉じ、失敗時は `disconnect` で掃除します。\n *\n * @param _c クローズコンテキストです。\n *\n * @returns クローズが完了したら解決します。\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 /**\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 await client.set(this.#toKey(c.key), Buffer.from(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 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 return (await client.exists(this.#toKey(c.key))) > 0;\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 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 await this.#clearPattern(client, pattern, c.signal);\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}.${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 /**\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 controller.enqueue(new Uint8Array(data));\n controller.close();\n },\n });\n }\n\n /**\n * 開いている接続を返します。\n *\n * 未オープン時は例外を投げます。\n *\n * @returns オープン済みの接続です。\n */\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 /**\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 /**\n * 接頭辞に一致するキーを走査して削除します。\n *\n * `SCAN` で頁送りしながら `DEL` します。\n *\n * @param client 操作に使うクライアントです。\n * @param pattern 削除対象の一致パターンです。\n * @param signal 中断を知らせるシグナルです。\n *\n * @returns 削除が完了したら解決します。\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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unikvs/redis.node",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Storage plugin that stores byte arrays in Redis on Node.js.",
5
5
  "homepage": "https://tai-kun.github.io/unikvs/",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@unikvs/core": "0.0.13",
27
+ "@unikvs/core": "0.0.14",
28
28
  "ioredis": "^6.0.0"
29
29
  },
30
30
  "devDependencies": {
package/src/redis.ts CHANGED
@@ -50,32 +50,76 @@ export type RedisStorageOptions = Omit<RedisOptions, "keyPrefix" | "lazyConnect"
50
50
  readonly closeTimeout?: number | undefined;
51
51
  };
52
52
 
53
+ /**
54
+ * `SCAN` 1 回あたりにヒントとして渡すキー数です。
55
+ *
56
+ * 削除の往復回数と 1 回の `DEL` に渡す引数数を抑えるために使用します。
57
+ */
53
58
  const SCAN_COUNT = 1000;
59
+
60
+ /**
61
+ * 接続確立の既定タイムアウト (ミリ秒) です。
62
+ */
54
63
  const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
64
+
65
+ /**
66
+ * 切断処理の既定タイムアウト (ミリ秒) です。
67
+ */
55
68
  const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
56
69
 
57
- type Connection = { readonly client: Ioredis };
70
+ /**
71
+ * ストレージの動作に必要な接続を保持する型定義です。
72
+ */
73
+ type Connection = {
74
+ /**
75
+ * 操作に使うクライアントです。
76
+ */
77
+ readonly client: Ioredis;
78
+ };
58
79
 
59
80
  /**
60
81
  * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
61
82
  */
62
83
  export default class Redis implements IStorage {
84
+ /**
85
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
86
+ */
87
+ public readonly name: string;
88
+
89
+ /**
90
+ * Redis への接続を保持します。
91
+ *
92
+ * ストレージがオープンされるまで null です。
93
+ */
63
94
  #con: Connection | null;
64
95
 
96
+ /**
97
+ * 接続先の URL です。
98
+ *
99
+ * 未指定の場合はクライアントが環境変数 (`REDIS_URL`、`VALKEY_URL`) から解決します。
100
+ */
65
101
  readonly #url: string | undefined;
66
102
 
67
- // `closeTimeout` は本パッケージ固有の拡張です。
68
- // `this.#options` には含めず、別フィールドで保管します。
69
- // `this.#options` は `new Ioredis` にそのまま spread できる透過バッグです。
103
+ /**
104
+ * `closeTimeout` は本パッケージ固有の拡張です。
105
+ *
106
+ * `this.#options` には含めず、別フィールドで保管します。
107
+ * `this.#options` は `new Ioredis` にそのまま spread できる透過バッグです。
108
+ */
70
109
  readonly #options: Omit<RedisStorageOptions, "keyPrefix" | "cluster" | "closeTimeout">;
71
110
 
72
- // `ioredis` の `disconnectTimeout` (ソケット切断待機) とは別物です。
111
+ /**
112
+ * `close` 時の切断待ち時間 (ミリ秒) です。
113
+ *
114
+ * `ioredis` の `disconnectTimeout` (ソケット切断待機) とは別物です。
115
+ */
73
116
  readonly #closeTimeout: number;
74
117
 
118
+ /**
119
+ * すべてのキーの先頭に付与するプレフィックスです。
120
+ */
75
121
  readonly #keyPrefix: string;
76
122
 
77
- public readonly name: string;
78
-
79
123
  /**
80
124
  * [API Reference](https://tai-kun.github.io/unikvs/packages/redis-node#redis)
81
125
  */
@@ -107,22 +151,24 @@ export default class Redis implements IStorage {
107
151
  this.#keyPrefix = keyPrefix;
108
152
  }
109
153
 
154
+ /**
155
+ * ストレージがオープン済みかどうかです。
156
+ *
157
+ * @returns オープン済みなら真です。
158
+ */
110
159
  public get isOpen(): boolean {
111
160
  return this.#con !== null;
112
161
  }
113
162
 
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
-
163
+ /**
164
+ * ストレージをオープンします。
165
+ *
166
+ * `ioredis` クライアントを作成して接続します。
167
+ *
168
+ * @param _c オープンコンテキストです。
169
+ *
170
+ * @returns オープンが完了したら解決します。
171
+ */
126
172
  public async open(_c: IStorage.OpenContext): Promise<void> {
127
173
  // 並行 `open` 同士および `open` 接続待ち中の `close` の同時呼び出しは禁止 (事前条件) です。
128
174
  // 呼び出し側で直列化してください。
@@ -186,6 +232,15 @@ export default class Redis implements IStorage {
186
232
  this.#con = { client: raw };
187
233
  }
188
234
 
235
+ /**
236
+ * ストレージをクローズします。
237
+ *
238
+ * `quit` で丁寧に閉じ、失敗時は `disconnect` で掃除します。
239
+ *
240
+ * @param _c クローズコンテキストです。
241
+ *
242
+ * @returns クローズが完了したら解決します。
243
+ */
189
244
  public async close(_c: IStorage.CloseContext): Promise<void> {
190
245
  // 未 `open`・二重 `close` は `TypeError` で reject されます。
191
246
  // 必ず `await` し、結果を浮かせないでください。
@@ -216,26 +271,16 @@ export default class Redis implements IStorage {
216
271
  }
217
272
  }
218
273
 
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
-
274
+ /**
275
+ * バイト列を書き込みます。
276
+ *
277
+ * 指定したキーで値を上書きします。
278
+ *
279
+ * @param c 書き込みコンテキストです。
280
+ * @param data 保存するバイト列です。
281
+ *
282
+ * @returns 書き込みが完了したら解決します。
283
+ */
239
284
  public async write(c: IStorage.WriteContext, data: Uint8Array<ArrayBuffer>): Promise<void> {
240
285
  const { client } = this.#getConnection();
241
286
 
@@ -244,6 +289,15 @@ export default class Redis implements IStorage {
244
289
  await client.set(this.#toKey(c.key), Buffer.from(data));
245
290
  }
246
291
 
292
+ /**
293
+ * バイト列を読み取ります。
294
+ *
295
+ * 指定したキーの値を取得します。
296
+ *
297
+ * @param c 読み取りコンテキストです。
298
+ *
299
+ * @returns 読み取ったバイト列です。
300
+ */
247
301
  public async read(c: IStorage.ReadContext): Promise<Uint8Array<ArrayBuffer>> {
248
302
  const { client } = this.#getConnection();
249
303
 
@@ -258,6 +312,13 @@ export default class Redis implements IStorage {
258
312
  return new Uint8Array(data);
259
313
  }
260
314
 
315
+ /**
316
+ * キーの存在を確認します。
317
+ *
318
+ * @param c 存在確認コンテキストです。
319
+ *
320
+ * @returns 存在すれば真です。
321
+ */
261
322
  public async exists(c: IStorage.ExistsContext): Promise<boolean> {
262
323
  const { client } = this.#getConnection();
263
324
 
@@ -266,6 +327,15 @@ export default class Redis implements IStorage {
266
327
  return (await client.exists(this.#toKey(c.key))) > 0;
267
328
  }
268
329
 
330
+ /**
331
+ * キーを削除します。
332
+ *
333
+ * 存在しないキーも成功扱いにします。
334
+ *
335
+ * @param c 削除コンテキストです。
336
+ *
337
+ * @returns 削除が完了したら解決します。
338
+ */
269
339
  public async delete(c: IStorage.DeleteContext): Promise<void> {
270
340
  const { client } = this.#getConnection();
271
341
 
@@ -274,6 +344,15 @@ export default class Redis implements IStorage {
274
344
  await client.del(this.#toKey(c.key));
275
345
  }
276
346
 
347
+ /**
348
+ * 接頭辞に一致するキーをすべて削除します。
349
+ *
350
+ * `SCAN` で列挙しながら削除します。
351
+ *
352
+ * @param c クリアコンテキストです。
353
+ *
354
+ * @returns 削除が完了したら解決します。
355
+ */
277
356
  public async clear(c: IStorage.ClearContext): Promise<void> {
278
357
  const { client } = this.#getConnection();
279
358
 
@@ -284,6 +363,15 @@ export default class Redis implements IStorage {
284
363
  await this.#clearPattern(client, pattern, c.signal);
285
364
  }
286
365
 
366
+ /**
367
+ * 書き込み用ストリームを取得します。
368
+ *
369
+ * 一時キーに追記し、`close` 時に最終キーへ置き換えます。
370
+ *
371
+ * @param c 書き込みストリーム取得コンテキストです。
372
+ *
373
+ * @returns 書き込み用ストリームです。
374
+ */
287
375
  public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array<ArrayBuffer>> {
288
376
  const { client } = this.#getConnection();
289
377
 
@@ -358,6 +446,15 @@ export default class Redis implements IStorage {
358
446
  });
359
447
  }
360
448
 
449
+ /**
450
+ * 読み取り用ストリームを取得します。
451
+ *
452
+ * 取得した値を単一チャンクとして送出します。
453
+ *
454
+ * @param c 読み取りストリーム取得コンテキストです。
455
+ *
456
+ * @returns 読み取り用ストリームです。
457
+ */
361
458
  public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array<ArrayBuffer>> {
362
459
  const { client } = this.#getConnection();
363
460
 
@@ -389,4 +486,59 @@ export default class Redis implements IStorage {
389
486
  },
390
487
  });
391
488
  }
489
+
490
+ /**
491
+ * 開いている接続を返します。
492
+ *
493
+ * 未オープン時は例外を投げます。
494
+ *
495
+ * @returns オープン済みの接続です。
496
+ */
497
+ #getConnection(): Connection {
498
+ const con = this.#con;
499
+
500
+ if (con === null) {
501
+ throw new TypeError("Redis is not open. Call open() first.");
502
+ }
503
+
504
+ return con;
505
+ }
506
+
507
+ /**
508
+ * unikvs のキーにプレフィックスを付与し、Redis のキーへ変換します。
509
+ *
510
+ * @param key 変換する論理キーです。
511
+ *
512
+ * @returns Redis 上の実キーです。
513
+ */
514
+ #toKey(key: string): string {
515
+ return `${this.#keyPrefix}${key}`;
516
+ }
517
+
518
+ /**
519
+ * 接頭辞に一致するキーを走査して削除します。
520
+ *
521
+ * `SCAN` で頁送りしながら `DEL` します。
522
+ *
523
+ * @param client 操作に使うクライアントです。
524
+ * @param pattern 削除対象の一致パターンです。
525
+ * @param signal 中断を知らせるシグナルです。
526
+ *
527
+ * @returns 削除が完了したら解決します。
528
+ */
529
+ async #clearPattern(client: Ioredis, pattern: string, signal: AbortSignal): Promise<void> {
530
+ let cursor = "0";
531
+
532
+ do {
533
+ signal.throwIfAborted();
534
+
535
+ const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", SCAN_COUNT);
536
+
537
+ if (keys.length > 0) {
538
+ await client.del(...keys);
539
+ }
540
+
541
+ cursor = nextCursor;
542
+ } while (cursor !== "0");
543
+ }
392
544
  }