@better-auth/redis-storage 1.7.0-rc.2 → 1.7.0-rc.4

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));
@@ -68,12 +78,42 @@ return value
68
78
  async delete(key) {
69
79
  await client.del(prefixKey(key));
70
80
  },
81
+ /**
82
+ * Lists the keys under the configured prefix, with the prefix stripped.
83
+ *
84
+ * Keys are enumerated with `SCAN`, a best-effort walk: it may report the
85
+ * same key on more than one page, so the result is de-duplicated, and
86
+ * keys added or removed while the scan runs may or may not appear. Order
87
+ * is not guaranteed.
88
+ */
71
89
  async listKeys() {
72
- return (await client.keys(`${keyPrefix}*`)).map((key) => key.replace(keyPrefix, ""));
90
+ const keys = /* @__PURE__ */ new Set();
91
+ for await (const batch of scanBatches()) for (const key of batch) keys.add(key.slice(keyPrefix.length));
92
+ return [...keys];
73
93
  },
94
+ /**
95
+ * Deletes keys under the configured prefix.
96
+ *
97
+ * **Not atomic.** Keys are enumerated with `SCAN` and deleted page by
98
+ * page, so if Redis errors or the connection drops mid-iteration the
99
+ * returned promise rejects *after* earlier pages have already been
100
+ * deleted, leaving the store partially cleared. A rejection therefore
101
+ * means "an unknown subset of keys may already be gone", not "nothing
102
+ * changed", unlike a single blocking `DEL`, which either removes
103
+ * everything or nothing.
104
+ *
105
+ * **Best-effort while the keyspace changes.** `SCAN` is a best-effort
106
+ * enumeration: keys added or removed while the scan runs may or may not
107
+ * be returned, and a key may be returned more than once. A resolved call
108
+ * is therefore not proof the store is empty when other clients are
109
+ * writing concurrently.
110
+ *
111
+ * `clear()` is safe to call again: it is idempotent, so callers that need
112
+ * a fully empty store (e.g. revoking every session or rate-limit counter)
113
+ * should retry until it resolves. An already-empty store is a no-op.
114
+ */
74
115
  async clear() {
75
- const keys = await client.keys(`${keyPrefix}*`);
76
- await client.del(...keys);
116
+ for await (const batch of scanBatches()) await client.del(...batch);
77
117
  }
78
118
  };
79
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/redis-storage",
3
- "version": "1.7.0-rc.2",
3
+ "version": "1.7.0-rc.4",
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.7.0-rc.2"
38
+ "@better-auth/core": "^1.7.0-rc.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.9.3",
42
42
  "ioredis": "^5.11.1",
43
43
  "tsdown": "0.22.7",
44
44
  "typescript": "^6.0.3",
45
- "vitest": "^4.1.9",
46
- "@better-auth/core": "1.7.0-rc.2"
45
+ "vitest": "^4.1.10",
46
+ "@better-auth/core": "1.7.0-rc.4"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsdown",