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.
- package/AI-INTEGRATION-GUIDE.md +30 -1
- package/CHANGELOG.md +99 -0
- package/README.md +15 -12
- package/dist/adapters/web/CacheAdapter.d.ts +4 -0
- package/dist/adapters/web/CacheAdapter.js +4 -1
- package/dist/adapters/web/CookieAdapter.d.ts +20 -1
- package/dist/adapters/web/CookieAdapter.js +89 -39
- package/dist/adapters/web/IndexedDBAdapter.d.ts +5 -0
- package/dist/adapters/web/IndexedDBAdapter.js +4 -1
- package/dist/adapters/web/LocalStorageAdapter.d.ts +39 -0
- package/dist/adapters/web/LocalStorageAdapter.js +133 -69
- package/dist/adapters/web/MemoryAdapter.d.ts +5 -0
- package/dist/adapters/web/MemoryAdapter.js +9 -1
- package/dist/adapters/web/SessionStorageAdapter.d.ts +16 -56
- package/dist/adapters/web/SessionStorageAdapter.js +19 -223
- package/dist/adapters/web/URLAdapter.d.ts +3 -0
- package/dist/adapters/web/URLAdapter.js +8 -1
- package/dist/core/BaseAdapter.d.ts +32 -0
- package/dist/core/BaseAdapter.js +53 -1
- package/dist/core/Strata.d.ts +10 -0
- package/dist/core/Strata.js +97 -7
- package/dist/index.d.ts +3 -2
- package/dist/index.js +32 -8
- package/dist/types/index.d.ts +13 -0
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.js +29 -0
- package/package.json +1 -1
- package/scripts/build.js +93 -0
package/AI-INTEGRATION-GUIDE.md
CHANGED
|
@@ -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
|
+
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.
|
|
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
|
|
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
|
-
- **
|
|
469
|
-
`
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
`
|
|
473
|
-
|
|
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 [#](#changelog)
|
|
496
501
|
|
|
497
|
-
Latest release: **`2.8.
|
|
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
|
|
|
@@ -39,11 +39,14 @@ export class CacheAdapter extends BaseAdapter {
|
|
|
39
39
|
/**
|
|
40
40
|
* Initialize the adapter
|
|
41
41
|
*/
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
if (config)
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
for (const
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*/
|