strata-storage 2.8.4 β†’ 2.9.0

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.
@@ -1,6 +1,6 @@
1
1
  # AI Integration Guide - strata-storage
2
2
 
3
- Quick reference for AI development agents (Claude Code, Cursor, Copilot, etc.) to integrate `strata-storage` into web and mobile projects. Current version: **2.8.3**.
3
+ Quick reference for AI development agents (Claude Code, Cursor, Copilot, etc.) to integrate `strata-storage` into web and mobile projects. Current version: **2.8.5**.
4
4
 
5
5
  ## Installation
6
6
 
@@ -73,6 +73,35 @@ export const storage = defineStorage({
73
73
 
74
74
  `defineStorage()` pre-registers memory, localStorage, sessionStorage, IndexedDB, cookies, and the Cache API. Use `new Strata(config)` + `registerWebAdapters(instance)` only when you need full control.
75
75
 
76
+ πŸ”΄ **`defaultStorages` is the preference order for the DEFAULT adapter β€” not a registration list.** Operations with no explicit `storage` (`keys`, `clear`, `size`, `subscribe`) deliberately span every *registered* adapter. To leave one out entirely, say so:
77
+
78
+ ```typescript
79
+ export const storage = defineStorage({
80
+ defaultStorages: ['localStorage'],
81
+ adapters: { sessionStorage: false, indexedDB: false, cookies: false, cache: false },
82
+ });
83
+ ```
84
+
85
+ ## Shared storage areas β€” how this library identifies its own keys (2.9.0)
86
+
87
+ `localStorage`, `sessionStorage` and cookies are shared with every other script on the origin, and the default key prefix is empty β€” so a name test cannot tell our keys from theirs. **A key counts as ours only when its stored value is a `StorageValue` envelope.**
88
+
89
+ What follows from that:
90
+
91
+ - `keys()` returns only keys this library wrote. A key put in the same area by anything else is invisible to it **by design** β€” that is the fix, not a bug.
92
+ - The TTL sweep and `clear()` never read, parse, delete or log about another application's data.
93
+ - A value we cannot read is **not an error**. It is skipped and reported at `debug`, because it is evidence the key belongs to somebody else. `logger.error` is reserved for a key carrying our envelope that still fails, and for a real storage-access fault.
94
+ - Debug it with `setLogLevel('debug')` (exported), which names each skipped key and why.
95
+
96
+ ```typescript
97
+ import { setLogLevel, isStorageEnvelope } from 'strata-storage';
98
+
99
+ setLogLevel('debug'); // see what is being skipped
100
+ isStorageEnvelope(JSON.parse(raw)); // is this value one of ours?
101
+ ```
102
+
103
+ Before 2.9.0 an empty prefix made the adapter claim every key on the origin: it error-logged about third-party values on every sweep, returned them from `keys()`, and could delete one whose JSON happened to carry a past `expires`. Prefixes and namespaces are still worth setting for a clean keyspace β€” they are no longer what keeps this library off other people's keys.
104
+
76
105
  ## Storage Options (per operation)
77
106
 
78
107
  ```typescript
package/CHANGELOG.md CHANGED
@@ -5,6 +5,105 @@ All notable changes to Strata Storage will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.9.0] - 2026-09-01
9
+
10
+ Closes the entire consumer-reported issue queue β€” six entries from five projects (LabFlow, ClearHire,
11
+ HabitForge, LifeWell, Trizlink). **No API changes and no data migration**: every key stays exactly where
12
+ it is.
13
+
14
+ ### Fixed
15
+
16
+ - **A web adapter no longer treats another application's keys as its own** (ISSUE-01, ISSUE-09).
17
+ `localStorage`, `sessionStorage` and cookies are shared with every other script on the origin, and the
18
+ default key prefix is empty β€” so `startsWith(prefix)` matched *every* key there. An adapter now
19
+ identifies its own data by **shape**: a key counts as ours only if its stored value deserializes into a
20
+ `StorageValue` envelope. Consequences, all measured in a browser against 2.8.5 and again after:
21
+ - The permanent error stream is gone. `[strata-storage] Failed to get key _cltk from sessionStorage:
22
+ SyntaxError…` (Microsoft Clarity's session key) and the same message for a consumer's own
23
+ `logger-level` no longer occur. **A foreign value is not an error** β€” it is evidence the key belongs
24
+ to somebody else, which is the ordinary case in a shared area, so it is skipped and reported at
25
+ `debug`. `logger.error` is now reserved for a key carrying *our* envelope that still fails, and for a
26
+ genuine storage-access fault (private mode, a blocked origin).
27
+ - `keys()` no longer returns keys this library never wrote.
28
+ - πŸ”΄ **`cleanupExpired()` no longer deletes another application's data.** A foreign key holding valid
29
+ JSON with a past `expires` was removed by the TTL sweep. Earlier notes in the issue queue recorded
30
+ this path as latent; driving the repro in a real browser showed it firing, so it was live.
31
+ - πŸ”΄ **`clear()` no longer wipes the origin.** With an empty prefix the unfiltered branch deleted every
32
+ key present, including other applications'. It now removes only entries we wrote.
33
+ - **An unscoped `subscribe()` no longer throws** (ISSUE-07). `Strata.subscribe` fanned out across every
34
+ registered adapter, and `indexedDB`, `cookies` and `cache` β€” all registered by default β€” throw
35
+ `NotSupportedError: Operation 'subscribe' is not supported by indexedDB adapter`. The documented
36
+ "omit options to hear every adapter" form therefore killed application boot on any default instance.
37
+ Non-observable backends are skipped; an observer that hears fewer backends is the right outcome when
38
+ some cannot speak. Naming a non-observable backend explicitly now warns instead of failing silently.
39
+ - **Per-adapter configuration actually reaches the adapter** (ISSUE-08).
40
+ `defineStorage({ adapters: { localStorage: { prefix: 'app:' } } })` was a silent no-op β€” the isolation
41
+ advice the README gave and the fleet skill taught, doing nothing. The adapter honoured
42
+ `initialize({ prefix })` all along; the config path to it did not cover the **synchronous** API, which
43
+ is deliberately usable before initialization completes. Adapters now apply configuration synchronously
44
+ at registration, so `setSync`/`getSync` issued before `initialize()` resolves use the configured prefix
45
+ instead of writing to the bare key.
46
+ - **`defaultStorages` now protects the synchronous path too** (ISSUE-08, related finding). It reads as an
47
+ ordered fallback list and behaved as one only for the async API: with `localStorage` unavailable,
48
+ `defineStorage({ defaultStorages: ['localStorage','memory'] }).setSync(...)` still selected
49
+ `localStorage` and threw rather than falling through to `memory`.
50
+ - **`clear({ expiredOnly: true })` was a guaranteed no-op** on `localStorage`, `sessionStorage` and
51
+ cookies. It filtered the output of `keysSync()`, which excludes expired entries β€” so it inspected
52
+ exactly the set that could never match.
53
+ - **`cleanupExpired()` reported `0` while doing the work.** On these adapters the reaping happened as an
54
+ undocumented side effect of enumeration; the count is now real.
55
+ - **`size()` on `LocalStorageAdapter` read `window.localStorage` directly** instead of the adapter's own
56
+ storage area, so every inherited method was wrong for `sessionStorage` until the subclass
57
+ re-implemented it.
58
+ - **Cross-tab `storage` events could never match for `sessionStorage`.** The listener compared against
59
+ `window.localStorage` by name rather than the adapter's own area.
60
+ - **`setLogLevel` / `getLogLevel` are now exported.** The logger documented `setLogLevel('debug')` as a
61
+ supported control, and it was unreachable from the package entry point. This matters as of this
62
+ release: skipped foreign keys are reported at `debug`, so raising the level is how a consumer answers
63
+ "why is my key missing from `keys()`?".
64
+ - **The repo's own version claims contradicted each other** (ISSUE-06). `CLAUDE.md` asserted npm `latest`
65
+ was `2.8.2` (a bad release) with `2.8.3` "awaiting publish", while `docs/MANUAL-TASKS.md` recorded that
66
+ publish as done and `package.json` was two releases further on. The version prose is now a pointer to a
67
+ single home, and **the build fails** when the README's at-a-glance version row disagrees with
68
+ `package.json` β€” the row is what npm renders, and it had already shipped stale once.
69
+
70
+ ### Changed
71
+
72
+ - **`adapters: { <name>: false }` now opts an adapter out of *registration*, not just initialization.**
73
+ Registering an adapter the instance never uses still costs a TTL sweep over a storage area it does not
74
+ own. πŸ”΄ `defaultStorages` is **not** this switch β€” it is the preference order for choosing the *default*
75
+ adapter, while multi-adapter operations deliberately span everything registered. That distinction was
76
+ undocumented, and reading `defaultStorages` as a registration allow-list is what produced ISSUE-09.
77
+ - **`SessionStorageAdapter` shrank from 303 lines to 64.** It re-implemented ~230 lines that differed from
78
+ its parent only by naming `window.sessionStorage`, while the parent already routes every read and write
79
+ through `getStorage()`. That duplication is why one defect became two issue numbers: the copy carried
80
+ its own error call, so fixing the `localStorage` path left the `sessionStorage` path untouched.
81
+
82
+ ### Added
83
+
84
+ - `isStorageEnvelope(value)` is exported β€” the predicate deciding whether a stored value was written by
85
+ this library, useful when auditing a shared storage area.
86
+ - Two build gates, run by `yarn build`: the ownership predicate is asserted over 18 cases (including the
87
+ real-world foreign values above), and the README's version row is checked against `package.json`. Both
88
+ were watched failing against two differently-shaped planted defects each before being trusted.
89
+
90
+ ### Notes for upgraders
91
+
92
+ Nothing moves and no migration is required. Behaviour that changes: `keys()` and `clear()` now see only
93
+ this library's own entries, which is the fix rather than a regression. If you relied on an empty-prefix
94
+ instance enumerating or clearing foreign keys, target those keys directly instead.
95
+
96
+ A future **3.0.0** will give the web adapters a real default key prefix, built on this release's shape
97
+ check, with migrate-on-read and an opt-out for consumers whose physical key names are frozen.
98
+
99
+ ## [2.8.5] - 2026-07-25
100
+
101
+ ### Fixed
102
+
103
+ - **The README stated the previous version.** The at-a-glance `Version` row is a static duplicate of
104
+ `package.json.version`, so it drifted the moment the version was bumped β€” it shipped stale in eight of the
105
+ fleet's packages at once. The row, and any native version string, now move with the release.
106
+
8
107
  ## [2.8.4] - 2026-07-25
9
108
 
10
109
  Documentation, metadata and packaging pass β€” **no runtime code changes**, no API changes. Brings the
package/README.md CHANGED
@@ -32,7 +32,7 @@ integrity checksums and mirrored backups are opt-in per call or per instance.
32
32
 
33
33
  | | |
34
34
  |---|---|
35
- | **Version** | `2.8.4` |
35
+ | **Version** | `2.9.0` |
36
36
  | **License** | MIT |
37
37
  | **Node** | `>=24.13.0` |
38
38
  | **Platforms** | Web Β· iOS Β· Android (via Capacitor) |
@@ -305,8 +305,8 @@ Passed to `defineStorage(config)` or `new Strata(config)`:
305
305
 
306
306
  | Option | Type | Default | What it does |
307
307
  |---|---|---|---|
308
- | `defaultStorages` | `StorageType[]` | `['localStorage', 'indexedDB', 'sessionStorage', 'memory']` | Preference order for picking the default adapter. |
309
- | `adapters` | `object` | `{}` | Per-adapter settings, or `false` to skip one. `localStorage`/`sessionStorage` take `{ prefix }`; `indexedDB` takes `{ dbName, version }`; `cookies` takes `{ secure, sameSite }`; `cache` takes `{ cacheName }`. |
308
+ | `defaultStorages` | `StorageType[]` | `['localStorage', 'indexedDB', 'sessionStorage', 'memory']` | Preference order for picking the default adapter, and the fallback order when one is unusable. πŸ”΄ **Not a registration list** β€” operations with no explicit `storage` (`keys`, `clear`, `size`, `subscribe`) span every *registered* adapter. To leave one out entirely use `adapters: { <name>: false }`. |
309
+ | `adapters` | `object` | `{}` | Per-adapter settings, or `false` to leave that adapter unregistered entirely. `localStorage`/`sessionStorage` take `{ prefix }`; `indexedDB` takes `{ dbName, version }`; `cookies` takes `{ secure, sameSite }`; `cache` takes `{ cacheName }`. Settings apply immediately, so they hold for `setSync`/`getSync` issued before `initialize()` resolves. |
310
310
  | `encryption` | `{ enabled, password }` | disabled | AES-GCM encryption on every write. Async path only. |
311
311
  | `compression` | `{ enabled, threshold }` | disabled | Compress values above `threshold` bytes. Async path only. |
312
312
  | `sync` | `{ enabled }` | disabled | Cross-tab change notifications. |
@@ -465,12 +465,17 @@ More: [Troubleshooting](https://stratastorage-docs.aoneahsan.com/reference/troub
465
465
  client-side.
466
466
  - **Node support is minimal.** Only the `memory` adapter is available outside a browser, so values do not
467
467
  persist across processes.
468
- - **Known open defect β€” web adapters default to an empty key prefix.** With no prefix, `keys()` on
469
- `localStorage`/`sessionStorage` returns every key on the origin, including keys written by other code, and
470
- the TTL sweep reads them. In the worst case a foreign key whose value happens to be JSON with an expired
471
- `expires` field can be removed. Until this is fixed, set an explicit prefix:
472
- `defineStorage({ adapters: { localStorage: { prefix: 'myapp:' } } })`. Tracked as `ISSUE-01` in
473
- [docs/REPORTED-ISSUES.md](https://github.com/aoneahsan/strata-storage/blob/main/docs/REPORTED-ISSUES.md).
468
+ - **Web adapters share their storage area, and identify their own data by shape.** `localStorage`,
469
+ `sessionStorage` and cookies are shared with every other script on the origin, and the default key
470
+ prefix is empty β€” so a name test alone cannot tell our keys from theirs. Since `2.9.0` an adapter
471
+ treats a key as its own only when the stored value is a `StorageValue` envelope, so `keys()`, the TTL
472
+ sweep and `clear()` never touch another application's data, and a value we cannot read is skipped at
473
+ `debug` rather than reported as an error. **The consequence to know about:** a key written to the same
474
+ area by something other than this library is invisible to `keys()` by design. Call `setLogLevel('debug')`
475
+ to see what is being skipped and why. Setting a prefix or a namespace is still worth doing for a clean
476
+ keyspace β€” `defineStorage({ namespace: 'myapp' })`, or
477
+ `defineStorage({ adapters: { localStorage: { prefix: 'myapp:' } } })` β€” but it is no longer what keeps
478
+ the library off other people's keys.
474
479
  - **Firebase adapter names are not in the `StorageType` union.** `'firestore'` and `'realtime'` are runtime
475
480
  names, so strict TypeScript may need a cast on the options object.
476
481
 
@@ -494,9 +499,7 @@ More: [Troubleshooting](https://stratastorage-docs.aoneahsan.com/reference/troub
494
499
  <a id="changelog"></a>
495
500
  ## πŸ”„ Changelog&nbsp;[#](#changelog)
496
501
 
497
- Latest release: **`2.8.4`** β€” documentation, metadata and packaging only, with no runtime changes. Ships
498
- `CHANGELOG.md` inside the tarball for the first time, adds the `funding` link, and stops the build writing a
499
- second manifest into `dist/`.
502
+ Latest release: **`2.8.5`** β€” documentation only: the at-a-glance table above reported the previous version, because it is a static duplicate of `package.json`. Full history in the changelog.
500
503
 
501
504
  Full history: [CHANGELOG.md](https://github.com/aoneahsan/strata-storage/blob/main/CHANGELOG.md).
502
505
 
@@ -21,6 +21,10 @@ export declare class CacheAdapter extends BaseAdapter {
21
21
  /**
22
22
  * Initialize the adapter
23
23
  */
24
+ configure(config?: {
25
+ cacheName?: string;
26
+ baseUrl?: string;
27
+ }): void;
24
28
  initialize(config?: {
25
29
  cacheName?: string;
26
30
  baseUrl?: string;
@@ -39,11 +39,14 @@ export class CacheAdapter extends BaseAdapter {
39
39
  /**
40
40
  * Initialize the adapter
41
41
  */
42
- async initialize(config) {
42
+ configure(config) {
43
43
  if (config?.cacheName)
44
44
  this.cacheName = config.cacheName;
45
45
  if (config?.baseUrl)
46
46
  this.baseUrl = config.baseUrl;
47
+ }
48
+ async initialize(config) {
49
+ this.configure(config);
47
50
  await this.openCache();
48
51
  this.startTTLCleanup();
49
52
  }
@@ -43,7 +43,14 @@ export declare class CookieAdapter extends BaseAdapter {
43
43
  /**
44
44
  * Initialize the adapter
45
45
  */
46
- initialize(config?: CookieOptions): Promise<void>;
46
+ configure(config?: CookieOptions & {
47
+ prefix?: string;
48
+ }): void;
49
+ initialize(config?: CookieOptions & {
50
+ prefix?: string;
51
+ }): Promise<void>;
52
+ /** Whether the cookie jar is usable right now, without awaiting anything. */
53
+ isAvailableSync(): boolean;
47
54
  /**
48
55
  * Get a value from cookies
49
56
  */
@@ -84,6 +91,18 @@ export declare class CookieAdapter extends BaseAdapter {
84
91
  * Get all keys (synchronous)
85
92
  */
86
93
  keysSync(pattern?: string | RegExp): string[];
94
+ /**
95
+ * Every cookie on this origin that this adapter actually owns, envelope
96
+ * already parsed. The cookie jar is shared with the server and with every
97
+ * script on the origin, so name alone cannot identify our data β€” shape can.
98
+ */
99
+ protected ownCookies(includeExpired?: boolean): Array<{
100
+ key: string;
101
+ cookieKey: string;
102
+ value: StorageValue;
103
+ }>;
104
+ /** Reclaim expired cookies, reading each once and reporting a real count. */
105
+ cleanupExpired(): Promise<number>;
87
106
  /**
88
107
  * Check if key exists (synchronous)
89
108
  */
@@ -71,12 +71,34 @@ export class CookieAdapter extends BaseAdapter {
71
71
  /**
72
72
  * Initialize the adapter
73
73
  */
74
- async initialize(config) {
75
- if (config) {
76
- this.cookieOptions = { ...this.cookieOptions, ...config };
74
+ configure(config) {
75
+ if (!config)
76
+ return;
77
+ const { prefix, ...cookieOptions } = config;
78
+ if (prefix !== undefined) {
79
+ this.prefix = prefix;
77
80
  }
81
+ this.cookieOptions = { ...this.cookieOptions, ...cookieOptions };
82
+ }
83
+ async initialize(config) {
84
+ this.configure(config);
78
85
  this.startTTLCleanup();
79
86
  }
87
+ /** Whether the cookie jar is usable right now, without awaiting anything. */
88
+ isAvailableSync() {
89
+ try {
90
+ if (typeof document === 'undefined')
91
+ return false;
92
+ const testKey = `${this.prefix}__test__`;
93
+ document.cookie = `${testKey}=test; path=/`;
94
+ const ok = document.cookie.includes(testKey);
95
+ this.deleteCookie(testKey);
96
+ return ok;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
80
102
  /**
81
103
  * Get a value from cookies
82
104
  */
@@ -91,20 +113,26 @@ export class CookieAdapter extends BaseAdapter {
91
113
  const value = this.getCookie(cookieKey);
92
114
  if (!value)
93
115
  return null;
116
+ let decoded;
94
117
  try {
95
- const decoded = decodeURIComponent(value);
96
- const parsed = deserialize(decoded);
97
- // Check TTL
98
- if (this.isExpired(parsed)) {
99
- this.removeSync(key);
100
- return null;
101
- }
102
- return parsed;
118
+ decoded = decodeURIComponent(value);
103
119
  }
104
- catch (error) {
105
- logger.error(`Failed to parse cookie ${key}:`, error);
120
+ catch {
121
+ // Not percent-encoded β€” so not written by this adapter.
122
+ logger.debug(`${this.name}: skipping cookie "${key}" β€” not written by this adapter.`);
123
+ return null;
124
+ }
125
+ // The cookie jar is shared with the server and every script on the origin.
126
+ // A cookie that is not our envelope belongs to somebody else; skip it
127
+ // silently rather than reporting a parse failure about their data.
128
+ const parsed = this.parseOwnValue(decoded, key);
129
+ if (!parsed)
130
+ return null;
131
+ if (this.isExpired(parsed)) {
132
+ this.removeSync(key);
106
133
  return null;
107
134
  }
135
+ return parsed;
108
136
  }
109
137
  /**
110
138
  * Set a value in cookies
@@ -185,34 +213,30 @@ export class CookieAdapter extends BaseAdapter {
185
213
  */
186
214
  clearSync(options) {
187
215
  if (!options || (!options.pattern && !options.tags && !options.expiredOnly)) {
188
- // Clear all cookies with our prefix
189
- const cookies = this.getAllCookies();
190
- for (const [cookieKey] of cookies) {
191
- if (cookieKey.startsWith(this.prefix)) {
192
- this.deleteCookie(cookieKey);
193
- }
216
+ // Delete only the cookies WE wrote. A name-only sweep deletes the
217
+ // server's session cookie too when the prefix is empty.
218
+ for (const { cookieKey } of this.ownCookies(true)) {
219
+ this.deleteCookie(cookieKey);
194
220
  }
195
221
  this.emitChange('*', undefined, undefined, 'local');
196
222
  return;
197
223
  }
198
- // Synchronous filtered clear (mirrors BaseAdapter.clear logic)
199
- for (const key of this.keysSync()) {
224
+ // Synchronous filtered clear (mirrors BaseAdapter.clear logic). Driven from
225
+ // owned entries INCLUDING expired ones, since `expiredOnly` filters on
226
+ // exactly what `keysSync()` leaves out.
227
+ for (const { key, value } of this.ownCookies(true)) {
200
228
  let shouldDelete = true;
201
229
  const pattern = options.pattern || options.prefix;
202
230
  if (pattern) {
203
231
  shouldDelete = this.filterKeys([key], pattern).length > 0;
204
232
  }
205
233
  if (shouldDelete && options.tags) {
206
- const value = this.getSync(key);
207
- if (!value?.tags || !options.tags.some((tag) => value.tags?.includes(tag))) {
234
+ if (!value.tags || !options.tags.some((tag) => value.tags?.includes(tag))) {
208
235
  shouldDelete = false;
209
236
  }
210
237
  }
211
- if (shouldDelete && options.expiredOnly) {
212
- const value = this.getSync(key);
213
- if (!value || !this.isExpired(value)) {
214
- shouldDelete = false;
215
- }
238
+ if (shouldDelete && options.expiredOnly && !this.isExpired(value)) {
239
+ shouldDelete = false;
216
240
  }
217
241
  if (shouldDelete) {
218
242
  this.removeSync(key);
@@ -229,19 +253,45 @@ export class CookieAdapter extends BaseAdapter {
229
253
  * Get all keys (synchronous)
230
254
  */
231
255
  keysSync(pattern) {
232
- const cookies = this.getAllCookies();
233
- const keys = [];
234
- for (const [cookieKey] of cookies) {
235
- if (cookieKey.startsWith(this.prefix)) {
236
- const key = cookieKey.substring(this.prefix.length);
237
- // Check if not expired
238
- const value = this.getSync(key);
239
- if (value) {
240
- keys.push(key);
241
- }
256
+ return this.filterKeys(this.ownCookies().map((entry) => entry.key), pattern);
257
+ }
258
+ /**
259
+ * Every cookie on this origin that this adapter actually owns, envelope
260
+ * already parsed. The cookie jar is shared with the server and with every
261
+ * script on the origin, so name alone cannot identify our data β€” shape can.
262
+ */
263
+ ownCookies(includeExpired = false) {
264
+ const owned = [];
265
+ for (const [cookieKey, raw] of this.getAllCookies()) {
266
+ if (!cookieKey.startsWith(this.prefix))
267
+ continue;
268
+ const key = cookieKey.substring(this.prefix.length);
269
+ let decoded;
270
+ try {
271
+ decoded = decodeURIComponent(raw);
272
+ }
273
+ catch {
274
+ continue;
275
+ }
276
+ const value = this.parseOwnValue(decoded, key);
277
+ if (!value)
278
+ continue;
279
+ if (!includeExpired && this.isExpired(value))
280
+ continue;
281
+ owned.push({ key, cookieKey, value });
282
+ }
283
+ return owned;
284
+ }
285
+ /** Reclaim expired cookies, reading each once and reporting a real count. */
286
+ async cleanupExpired() {
287
+ let removed = 0;
288
+ for (const { key, value } of this.ownCookies(true)) {
289
+ if (this.isExpired(value)) {
290
+ this.removeSync(key);
291
+ removed++;
242
292
  }
243
293
  }
244
- return this.filterKeys(keys, pattern);
294
+ return removed;
245
295
  }
246
296
  /**
247
297
  * Check if key exists (synchronous)
@@ -22,6 +22,11 @@ export declare class IndexedDBAdapter extends BaseAdapter {
22
22
  /**
23
23
  * Initialize the adapter
24
24
  */
25
+ configure(config?: {
26
+ dbName?: string;
27
+ storeName?: string;
28
+ version?: number;
29
+ }): void;
25
30
  initialize(config?: {
26
31
  dbName?: string;
27
32
  storeName?: string;
@@ -41,13 +41,16 @@ export class IndexedDBAdapter extends BaseAdapter {
41
41
  /**
42
42
  * Initialize the adapter
43
43
  */
44
- async initialize(config) {
44
+ configure(config) {
45
45
  if (config?.dbName)
46
46
  this.dbName = config.dbName;
47
47
  if (config?.storeName)
48
48
  this.storeName = config.storeName;
49
49
  if (config?.version)
50
50
  this.version = config.version;
51
+ }
52
+ async initialize(config) {
53
+ this.configure(config);
51
54
  await this.openDatabase();
52
55
  this.startTTLCleanup();
53
56
  }
@@ -17,12 +17,26 @@ export declare class LocalStorageAdapter extends BaseAdapter {
17
17
  * Check if localStorage is available
18
18
  */
19
19
  isAvailable(): Promise<boolean>;
20
+ /**
21
+ * Apply configuration synchronously. See `BaseAdapter.configure` for why the
22
+ * prefix cannot wait for the async `initialize()`.
23
+ */
24
+ configure(config?: {
25
+ prefix?: string;
26
+ }): void;
20
27
  /**
21
28
  * Initialize the adapter
22
29
  */
23
30
  initialize(config?: {
24
31
  prefix?: string;
25
32
  }): Promise<void>;
33
+ /**
34
+ * Whether this storage area is usable RIGHT NOW, without awaiting anything.
35
+ * The synchronous API needs this: `defaultStorages` reads as an ordered
36
+ * fallback list, and without a sync probe `setSync` selects an unusable
37
+ * backend and throws instead of falling through to the next one.
38
+ */
39
+ isAvailableSync(): boolean;
26
40
  /**
27
41
  * Get the backing Storage object.
28
42
  * Subclasses (e.g. SessionStorageAdapter) override this to target a
@@ -70,6 +84,31 @@ export declare class LocalStorageAdapter extends BaseAdapter {
70
84
  * Get all keys (synchronous)
71
85
  */
72
86
  keysSync(pattern?: string | RegExp): string[];
87
+ /**
88
+ * Every key in this storage area that this adapter actually owns, with the
89
+ * envelope already parsed (one read per key, not two).
90
+ *
91
+ * πŸ”΄ Name is not enough. With the default empty prefix `startsWith(prefix)` is
92
+ * true for EVERY key on the origin, so this method β€” not the prefix β€” is what
93
+ * keeps `keys()`, the TTL sweep and `clear()` off other scripts' data. An
94
+ * expired entry is skipped here exactly as before.
95
+ */
96
+ protected ownKeys(includeExpired?: boolean): Array<{
97
+ key: string;
98
+ fullKey: string;
99
+ value: StorageValue;
100
+ }>;
101
+ /**
102
+ * Reclaim expired entries, returning how many were removed.
103
+ *
104
+ * Overrides the base per-key sweep for two reasons. It reads each key once
105
+ * instead of twice, and β€” the load-bearing one β€” the base sweep is built on
106
+ * `keys()`, which does not surface expired entries here, so it could only ever
107
+ * report 0. Before this override the reaping happened as an undocumented side
108
+ * effect of `getSync()` deleting what it found expired during enumeration,
109
+ * while the returned count stayed 0.
110
+ */
111
+ cleanupExpired(): Promise<number>;
73
112
  /**
74
113
  * Check if key exists (synchronous)
75
114
  */