strata-storage 2.8.5 → 3.0.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.
@@ -73,6 +73,49 @@ 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
+ ## Key prefix and the 3.0.0 migration
86
+
87
+ `localStorage` and `sessionStorage` keys are written under **`strata:`** as of 3.0.0
88
+ (`DEFAULT_WEB_KEY_PREFIX`, exported). Existing data migrates itself: a miss at `strata:<key>` falls back to the bare `<key>`, and if the value is one of ours it is moved under the prefix. Per key, on read — never a bulk sweep, never a value that is not our envelope, never overwriting an existing prefixed value.
89
+
90
+ 🔴 **Set `keyPrefix: false` when anything outside this library reads a physical key directly** — a pre-paint theme script, a logger reading its own level. Migration keeps the data reachable through this library; it cannot fix a hard-coded reader.
91
+
92
+ ```typescript
93
+ export const storage = defineStorage({ keyPrefix: false }); // pre-3.0 keys
94
+ export const storage = defineStorage({ migrateLegacyKeys: false }); // don't adopt 2.x entries
95
+ ```
96
+
97
+ `cookies` (already `strata_`), `indexedDB`, `cache`, `memory` and `url` are unchanged. `namespace` is a separate mechanism: the physical key is `<keyPrefix><namespace>:<key>`.
98
+
99
+ ## Shared storage areas — how this library identifies its own keys
100
+
101
+ `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.**
102
+
103
+ What follows from that:
104
+
105
+ - `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.
106
+ - The TTL sweep and `clear()` never read, parse, delete or log about another application's data.
107
+ - 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.
108
+ - Debug it with `setLogLevel('debug')` (exported), which names each skipped key and why.
109
+
110
+ ```typescript
111
+ import { setLogLevel, isStorageEnvelope } from 'strata-storage';
112
+
113
+ setLogLevel('debug'); // see what is being skipped
114
+ isStorageEnvelope(JSON.parse(raw)); // is this value one of ours?
115
+ ```
116
+
117
+ 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.
118
+
76
119
  ## Storage Options (per operation)
77
120
 
78
121
  ```typescript
package/CHANGELOG.md CHANGED
@@ -5,6 +5,157 @@ 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
+ ## [3.0.0] - 2026-09-01
9
+
10
+ **Breaking: the web adapters now prefix their keys with `strata:`.** One line restores the old behaviour,
11
+ and existing data migrates itself on read — but read the migration notes before upgrading, because a
12
+ consumer that reads a physical key from outside this library must opt out.
13
+
14
+ ### Breaking
15
+
16
+ - **`localStorage` and `sessionStorage` keys are now written under `strata:`.** `DEFAULT_WEB_KEY_PREFIX`
17
+ is exported. Before this release the default prefix was the empty string, so this library's keys sat
18
+ unprefixed among every other script's on the origin. 2.9.0 made that *safe* — a key is ours only if its
19
+ value is a `StorageValue` envelope — and this makes it *tidy*, so our keys are identifiable by name too.
20
+ - `cookies` are unchanged: they already defaulted to `strata_`, and moving them would break existing
21
+ cookies for no gain.
22
+ - `indexedDB` and `cache` own a named store, `memory` owns its own Map, and the URL adapter already
23
+ prefixes its params. None of them can collide with another script's keys, so none of them change.
24
+ - `namespace` is a separate mechanism and is unaffected. The physical key is
25
+ `<keyPrefix><namespace>:<key>`.
26
+
27
+ ### Added
28
+
29
+ - **`keyPrefix`** — set `false` (or `''`) for the pre-3.0 behaviour, or a string for your own prefix. A
30
+ per-adapter `adapters.localStorage.prefix` still wins over it.
31
+ - **`migrateLegacyKeys`** (default `true`) — adopt pre-3.0 unprefixed entries on read.
32
+
33
+ ### Migration
34
+
35
+ **Most consumers need to do nothing.** On a miss at `strata:<key>`, the adapter looks for the bare `<key>`
36
+ and, if the value is one of ours, moves it under the prefix and returns it. Your data stays reachable.
37
+
38
+ Three properties of that migration, each verified in a browser:
39
+
40
+ - **It is per key, on read — never a bulk sweep.** A sweep would adopt every unprefixed envelope on the
41
+ origin, including keys belonging to an instance that opted out or to a sibling app still on 2.x.
42
+ - **It never adopts a value that is not ours** — the 2.9.0 envelope check is what makes it safe at all.
43
+ A planted `_cltk` is left exactly where it is.
44
+ - **It never overwrites.** A value already at the prefixed key wins and the legacy entry is left alone.
45
+
46
+ 🔴 **Take `keyPrefix: false` if anything outside this library reads a physical key directly** — a
47
+ pre-paint theme script that runs before any module loads, or a logger reading its own level. Those
48
+ readers know the exact key name, and a prefix changes it underneath them. Migration keeps the *data*
49
+ reachable through this library; it cannot fix a hard-coded reader.
50
+
51
+ ```typescript
52
+ // Frozen physical key names, or an external reader — keep 2.x behaviour:
53
+ export const storage = defineStorage({ keyPrefix: false });
54
+ ```
55
+
56
+ 🔴 **Two applications sharing one origin, one still on 2.x:** set `migrateLegacyKeys: false` on the
57
+ upgraded one, so it does not move keys the other still reads. Better, give each app a `namespace` —
58
+ they were already colliding before this release.
59
+
60
+ ### Fixed
61
+
62
+ - **A live timer no longer keeps a Node process alive.** `setInterval` holds the event loop open, so a
63
+ short-lived script — a build step, a CLI, an SSR warmup — that created an instance never exited unless
64
+ it also called `close()`. Measured: a script doing one `set`/`get` hung until killed at 120s, and now
65
+ exits immediately. All three timers (adapter TTL, cross-adapter cleanup, auto-backup) are `unref`'d;
66
+ it is a no-op in browsers.
67
+
68
+ ## [2.9.0] - 2026-09-01
69
+
70
+ Closes the entire consumer-reported issue queue — six entries from five projects (LabFlow, ClearHire,
71
+ HabitForge, LifeWell, Trizlink). **No API changes and no data migration**: every key stays exactly where
72
+ it is.
73
+
74
+ ### Fixed
75
+
76
+ - **A web adapter no longer treats another application's keys as its own** (ISSUE-01, ISSUE-09).
77
+ `localStorage`, `sessionStorage` and cookies are shared with every other script on the origin, and the
78
+ default key prefix is empty — so `startsWith(prefix)` matched *every* key there. An adapter now
79
+ identifies its own data by **shape**: a key counts as ours only if its stored value deserializes into a
80
+ `StorageValue` envelope. Consequences, all measured in a browser against 2.8.5 and again after:
81
+ - The permanent error stream is gone. `[strata-storage] Failed to get key _cltk from sessionStorage:
82
+ SyntaxError…` (Microsoft Clarity's session key) and the same message for a consumer's own
83
+ `logger-level` no longer occur. **A foreign value is not an error** — it is evidence the key belongs
84
+ to somebody else, which is the ordinary case in a shared area, so it is skipped and reported at
85
+ `debug`. `logger.error` is now reserved for a key carrying *our* envelope that still fails, and for a
86
+ genuine storage-access fault (private mode, a blocked origin).
87
+ - `keys()` no longer returns keys this library never wrote.
88
+ - 🔴 **`cleanupExpired()` no longer deletes another application's data.** A foreign key holding valid
89
+ JSON with a past `expires` was removed by the TTL sweep. Earlier notes in the issue queue recorded
90
+ this path as latent; driving the repro in a real browser showed it firing, so it was live.
91
+ - 🔴 **`clear()` no longer wipes the origin.** With an empty prefix the unfiltered branch deleted every
92
+ key present, including other applications'. It now removes only entries we wrote.
93
+ - **An unscoped `subscribe()` no longer throws** (ISSUE-07). `Strata.subscribe` fanned out across every
94
+ registered adapter, and `indexedDB`, `cookies` and `cache` — all registered by default — throw
95
+ `NotSupportedError: Operation 'subscribe' is not supported by indexedDB adapter`. The documented
96
+ "omit options to hear every adapter" form therefore killed application boot on any default instance.
97
+ Non-observable backends are skipped; an observer that hears fewer backends is the right outcome when
98
+ some cannot speak. Naming a non-observable backend explicitly now warns instead of failing silently.
99
+ - **Per-adapter configuration actually reaches the adapter** (ISSUE-08).
100
+ `defineStorage({ adapters: { localStorage: { prefix: 'app:' } } })` was a silent no-op — the isolation
101
+ advice the README gave and the fleet skill taught, doing nothing. The adapter honoured
102
+ `initialize({ prefix })` all along; the config path to it did not cover the **synchronous** API, which
103
+ is deliberately usable before initialization completes. Adapters now apply configuration synchronously
104
+ at registration, so `setSync`/`getSync` issued before `initialize()` resolves use the configured prefix
105
+ instead of writing to the bare key.
106
+ - **`defaultStorages` now protects the synchronous path too** (ISSUE-08, related finding). It reads as an
107
+ ordered fallback list and behaved as one only for the async API: with `localStorage` unavailable,
108
+ `defineStorage({ defaultStorages: ['localStorage','memory'] }).setSync(...)` still selected
109
+ `localStorage` and threw rather than falling through to `memory`.
110
+ - **`clear({ expiredOnly: true })` was a guaranteed no-op** on `localStorage`, `sessionStorage` and
111
+ cookies. It filtered the output of `keysSync()`, which excludes expired entries — so it inspected
112
+ exactly the set that could never match.
113
+ - **`cleanupExpired()` reported `0` while doing the work.** On these adapters the reaping happened as an
114
+ undocumented side effect of enumeration; the count is now real.
115
+ - **`size()` on `LocalStorageAdapter` read `window.localStorage` directly** instead of the adapter's own
116
+ storage area, so every inherited method was wrong for `sessionStorage` until the subclass
117
+ re-implemented it.
118
+ - **Cross-tab `storage` events could never match for `sessionStorage`.** The listener compared against
119
+ `window.localStorage` by name rather than the adapter's own area.
120
+ - **`setLogLevel` / `getLogLevel` are now exported.** The logger documented `setLogLevel('debug')` as a
121
+ supported control, and it was unreachable from the package entry point. This matters as of this
122
+ release: skipped foreign keys are reported at `debug`, so raising the level is how a consumer answers
123
+ "why is my key missing from `keys()`?".
124
+ - **The repo's own version claims contradicted each other** (ISSUE-06). `CLAUDE.md` asserted npm `latest`
125
+ was `2.8.2` (a bad release) with `2.8.3` "awaiting publish", while `docs/MANUAL-TASKS.md` recorded that
126
+ publish as done and `package.json` was two releases further on. The version prose is now a pointer to a
127
+ single home, and **the build fails** when the README's at-a-glance version row disagrees with
128
+ `package.json` — the row is what npm renders, and it had already shipped stale once.
129
+
130
+ ### Changed
131
+
132
+ - **`adapters: { <name>: false }` now opts an adapter out of *registration*, not just initialization.**
133
+ Registering an adapter the instance never uses still costs a TTL sweep over a storage area it does not
134
+ own. 🔴 `defaultStorages` is **not** this switch — it is the preference order for choosing the *default*
135
+ adapter, while multi-adapter operations deliberately span everything registered. That distinction was
136
+ undocumented, and reading `defaultStorages` as a registration allow-list is what produced ISSUE-09.
137
+ - **`SessionStorageAdapter` shrank from 303 lines to 64.** It re-implemented ~230 lines that differed from
138
+ its parent only by naming `window.sessionStorage`, while the parent already routes every read and write
139
+ through `getStorage()`. That duplication is why one defect became two issue numbers: the copy carried
140
+ its own error call, so fixing the `localStorage` path left the `sessionStorage` path untouched.
141
+
142
+ ### Added
143
+
144
+ - `isStorageEnvelope(value)` is exported — the predicate deciding whether a stored value was written by
145
+ this library, useful when auditing a shared storage area.
146
+ - Two build gates, run by `yarn build`: the ownership predicate is asserted over 18 cases (including the
147
+ real-world foreign values above), and the README's version row is checked against `package.json`. Both
148
+ were watched failing against two differently-shaped planted defects each before being trusted.
149
+
150
+ ### Notes for upgraders
151
+
152
+ Nothing moves and no migration is required. Behaviour that changes: `keys()` and `clear()` now see only
153
+ this library's own entries, which is the fix rather than a regression. If you relied on an empty-prefix
154
+ instance enumerating or clearing foreign keys, target those keys directly instead.
155
+
156
+ A future **3.0.0** will give the web adapters a real default key prefix, built on this release's shape
157
+ check, with migrate-on-read and an opt-out for consumers whose physical key names are frozen.
158
+
8
159
  ## [2.8.5] - 2026-07-25
9
160
 
10
161
  ### Fixed
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.5` |
35
+ | **Version** | `3.0.0` |
36
36
  | **License** | MIT |
37
37
  | **Node** | `>=24.13.0` |
38
38
  | **Platforms** | Web · iOS · Android (via Capacitor) |
@@ -305,8 +305,10 @@ 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
+ | `keyPrefix` | `string \| false` | `'strata:'` | Key prefix for `localStorage`/`sessionStorage`. `false` restores pre-3.0 unprefixed keys take it when anything outside this library reads a physical key directly. |
310
+ | `migrateLegacyKeys` | `boolean` | `true` | Adopt pre-3.0 unprefixed entries on read: never a bulk sweep, never a non-envelope, never overwriting an existing prefixed value. |
311
+ | `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
312
  | `encryption` | `{ enabled, password }` | disabled | AES-GCM encryption on every write. Async path only. |
311
313
  | `compression` | `{ enabled, threshold }` | disabled | Compress values above `threshold` bytes. Async path only. |
312
314
  | `sync` | `{ enabled }` | disabled | Cross-tab change notifications. |
@@ -465,12 +467,18 @@ More: [Troubleshooting](https://stratastorage-docs.aoneahsan.com/reference/troub
465
467
  client-side.
466
468
  - **Node support is minimal.** Only the `memory` adapter is available outside a browser, so values do not
467
469
  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).
470
+ - **Web adapters share their storage area, and identify their own data by shape.** `localStorage`,
471
+ `sessionStorage` and cookies are shared with every other script on the origin. An adapter treats a key
472
+ as its own only when the stored value is a `StorageValue` envelope, so `keys()`, the TTL sweep and
473
+ `clear()` never touch another application's data, and a value we cannot read is skipped at `debug`
474
+ rather than reported as an error. **The consequence to know about:** a key written to the same area by
475
+ something other than this library is invisible to `keys()` by design. Call `setLogLevel('debug')` to see
476
+ what is being skipped and why.
477
+ - **Keys are prefixed `strata:` since 3.0.0, and that is a breaking change.** Existing data migrates
478
+ itself on read, so most consumers do nothing. 🔴 **But if anything outside this library reads a physical
479
+ key directly — a pre-paint theme script, a logger reading its own level — set
480
+ `defineStorage({ keyPrefix: false })`.** Migration keeps the data reachable through this library; it
481
+ cannot fix a hard-coded reader. See [Changelog](https://github.com/aoneahsan/strata-storage/blob/main/CHANGELOG.md).
474
482
  - **Firebase adapter names are not in the `StorageType` union.** `'firestore'` and `'realtime'` are runtime
475
483
  names, so strict TypeScript may need a cast on the options object.
476
484
 
@@ -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
  }