@better-auth/redis-storage 1.6.25 → 1.6.26

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.mts CHANGED
@@ -38,7 +38,36 @@ declare function redisStorage(config: RedisStorageConfig): {
38
38
  increment(key: string, ttl: number): Promise<number>;
39
39
  set(key: string, value: string, ttl?: number | undefined): Promise<void>;
40
40
  delete(key: string): Promise<void>;
41
+ /**
42
+ * Lists the keys under the configured prefix, with the prefix stripped.
43
+ *
44
+ * Keys are enumerated with `SCAN`, a best-effort walk: it may report the
45
+ * same key on more than one page, so the result is de-duplicated, and
46
+ * keys added or removed while the scan runs may or may not appear. Order
47
+ * is not guaranteed.
48
+ */
41
49
  listKeys(): Promise<string[]>;
50
+ /**
51
+ * Deletes keys under the configured prefix.
52
+ *
53
+ * **Not atomic.** Keys are enumerated with `SCAN` and deleted page by
54
+ * page, so if Redis errors or the connection drops mid-iteration the
55
+ * returned promise rejects *after* earlier pages have already been
56
+ * deleted, leaving the store partially cleared. A rejection therefore
57
+ * means "an unknown subset of keys may already be gone", not "nothing
58
+ * changed", unlike a single blocking `DEL`, which either removes
59
+ * everything or nothing.
60
+ *
61
+ * **Best-effort while the keyspace changes.** `SCAN` is a best-effort
62
+ * enumeration: keys added or removed while the scan runs may or may not
63
+ * be returned, and a key may be returned more than once. A resolved call
64
+ * is therefore not proof the store is empty when other clients are
65
+ * writing concurrently.
66
+ *
67
+ * `clear()` is safe to call again: it is idempotent, so callers that need
68
+ * a fully empty store (e.g. revoking every session or rate-limit counter)
69
+ * should retry until it resolves. An already-empty store is a no-op.
70
+ */
42
71
  clear(): Promise<void>;
43
72
  };
44
73
  //#endregion
package/dist/index.mjs CHANGED
@@ -41,6 +41,16 @@ return value
41
41
  return `${keyPrefix}${key}`;
42
42
  };
43
43
  const isUnknownCommandError = (error) => error instanceof Error && error.message.toLowerCase().includes("unknown command");
44
+ const SCAN_COUNT = 100;
45
+ const escapedPrefix = keyPrefix.replace(/[\\*?[\]]/g, "\\$&");
46
+ async function* scanBatches() {
47
+ let cursor = "0";
48
+ do {
49
+ const [nextCursor, batch] = await client.scan(cursor, "MATCH", `${escapedPrefix}*`, "COUNT", SCAN_COUNT);
50
+ cursor = nextCursor;
51
+ if (batch.length > 0) yield batch;
52
+ } while (cursor !== "0");
53
+ }
44
54
  return {
45
55
  async get(key) {
46
56
  return client.get(prefixKey(key));
@@ -67,12 +77,42 @@ return value
67
77
  async delete(key) {
68
78
  await client.del(prefixKey(key));
69
79
  },
80
+ /**
81
+ * Lists the keys under the configured prefix, with the prefix stripped.
82
+ *
83
+ * Keys are enumerated with `SCAN`, a best-effort walk: it may report the
84
+ * same key on more than one page, so the result is de-duplicated, and
85
+ * keys added or removed while the scan runs may or may not appear. Order
86
+ * is not guaranteed.
87
+ */
70
88
  async listKeys() {
71
- return (await client.keys(`${keyPrefix}*`)).map((key) => key.replace(keyPrefix, ""));
89
+ const keys = /* @__PURE__ */ new Set();
90
+ for await (const batch of scanBatches()) for (const key of batch) keys.add(key.slice(keyPrefix.length));
91
+ return [...keys];
72
92
  },
93
+ /**
94
+ * Deletes keys under the configured prefix.
95
+ *
96
+ * **Not atomic.** Keys are enumerated with `SCAN` and deleted page by
97
+ * page, so if Redis errors or the connection drops mid-iteration the
98
+ * returned promise rejects *after* earlier pages have already been
99
+ * deleted, leaving the store partially cleared. A rejection therefore
100
+ * means "an unknown subset of keys may already be gone", not "nothing
101
+ * changed", unlike a single blocking `DEL`, which either removes
102
+ * everything or nothing.
103
+ *
104
+ * **Best-effort while the keyspace changes.** `SCAN` is a best-effort
105
+ * enumeration: keys added or removed while the scan runs may or may not
106
+ * be returned, and a key may be returned more than once. A resolved call
107
+ * is therefore not proof the store is empty when other clients are
108
+ * writing concurrently.
109
+ *
110
+ * `clear()` is safe to call again: it is idempotent, so callers that need
111
+ * a fully empty store (e.g. revoking every session or rate-limit counter)
112
+ * should retry until it resolves. An already-empty store is a no-op.
113
+ */
73
114
  async clear() {
74
- const keys = await client.keys(`${keyPrefix}*`);
75
- await client.del(...keys);
115
+ for await (const batch of scanBatches()) await client.del(...batch);
76
116
  }
77
117
  };
78
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/redis-storage",
3
- "version": "1.6.25",
3
+ "version": "1.6.26",
4
4
  "description": "Redis storage for Better Auth secondary storage",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,15 +35,15 @@
35
35
  },
36
36
  "peerDependencies": {
37
37
  "ioredis": "^5.0.0",
38
- "@better-auth/core": "^1.6.25"
38
+ "@better-auth/core": "^1.6.26"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.3.2",
42
42
  "ioredis": "^5.9.3",
43
43
  "tsdown": "0.22.7",
44
44
  "typescript": "^6.0.3",
45
- "vitest": "^4.1.5",
46
- "@better-auth/core": "1.6.25"
45
+ "vitest": "^4.1.10",
46
+ "@better-auth/core": "1.6.26"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsdown",