strata-storage 2.9.0 → 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.
- package/AI-INTEGRATION-GUIDE.md +15 -1
- package/CHANGELOG.md +60 -0
- package/README.md +14 -11
- package/dist/adapters/web/LocalStorageAdapter.d.ts +62 -0
- package/dist/adapters/web/LocalStorageAdapter.js +133 -7
- package/dist/core/BaseAdapter.js +7 -0
- package/dist/core/Strata.d.ts +21 -1
- package/dist/core/Strata.js +45 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/package.json +1 -1
package/AI-INTEGRATION-GUIDE.md
CHANGED
|
@@ -82,7 +82,21 @@ export const storage = defineStorage({
|
|
|
82
82
|
});
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
##
|
|
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
|
|
86
100
|
|
|
87
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.**
|
|
88
102
|
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,66 @@ 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
|
+
|
|
8
68
|
## [2.9.0] - 2026-09-01
|
|
9
69
|
|
|
10
70
|
Closes the entire consumer-reported issue queue — six entries from five projects (LabFlow, ClearHire,
|
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** | `
|
|
35
|
+
| **Version** | `3.0.0` |
|
|
36
36
|
| **License** | MIT |
|
|
37
37
|
| **Node** | `>=24.13.0` |
|
|
38
38
|
| **Platforms** | Web · iOS · Android (via Capacitor) |
|
|
@@ -306,6 +306,8 @@ Passed to `defineStorage(config)` or `new Strata(config)`:
|
|
|
306
306
|
| Option | Type | Default | What it does |
|
|
307
307
|
|---|---|---|---|
|
|
308
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. |
|
|
309
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. |
|
|
@@ -466,16 +468,17 @@ More: [Troubleshooting](https://stratastorage-docs.aoneahsan.com/reference/troub
|
|
|
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
470
|
- **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
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
the
|
|
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).
|
|
479
482
|
- **Firebase adapter names are not in the `StorageType` union.** `'firestore'` and `'realtime'` are runtime
|
|
480
483
|
names, so strict TypeScript may need a cast on the options object.
|
|
481
484
|
|
|
@@ -7,11 +7,32 @@ import type { StorageType, StorageCapabilities, StorageValue, ClearOptions, Size
|
|
|
7
7
|
/**
|
|
8
8
|
* Browser localStorage adapter
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* The key prefix web adapters use unless told otherwise, as of 3.0.0.
|
|
12
|
+
*
|
|
13
|
+
* 🔴 Before 3.0.0 this was the empty string, which meant this library's keys sat
|
|
14
|
+
* unprefixed among every other script's in a shared storage area. 2.9.0 made that
|
|
15
|
+
* safe (a key is ours only if its value is a `StorageValue` envelope); this makes
|
|
16
|
+
* it tidy as well, so our keys are identifiable by name too.
|
|
17
|
+
*
|
|
18
|
+
* Opt out with `defineStorage({ keyPrefix: false })` — see `StrataConfig`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_WEB_KEY_PREFIX = "strata:";
|
|
10
21
|
export declare class LocalStorageAdapter extends BaseAdapter {
|
|
11
22
|
readonly name: StorageType;
|
|
12
23
|
readonly capabilities: StorageCapabilities;
|
|
13
24
|
protected prefix: string;
|
|
14
25
|
protected listeners: Map<SubscriptionCallback, (event: StorageEvent) => void>;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to adopt pre-3.0 unprefixed entries on read.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 Defaults to FALSE, and only `Strata` turns it on — for the adapters whose
|
|
30
|
+
* prefix it resolved. A directly constructed adapter must never adopt bare
|
|
31
|
+
* keys: `plugin/web.ts` builds a `strata_prefs_` instance beside the main one,
|
|
32
|
+
* and if that adopted every unprefixed entry it found, it would take them from
|
|
33
|
+
* the instance they belong to.
|
|
34
|
+
*/
|
|
35
|
+
protected migrateLegacyKeys: boolean;
|
|
15
36
|
constructor(prefix?: string);
|
|
16
37
|
/**
|
|
17
38
|
* Check if localStorage is available
|
|
@@ -23,7 +44,34 @@ export declare class LocalStorageAdapter extends BaseAdapter {
|
|
|
23
44
|
*/
|
|
24
45
|
configure(config?: {
|
|
25
46
|
prefix?: string;
|
|
47
|
+
migrateLegacyKeys?: boolean;
|
|
26
48
|
}): void;
|
|
49
|
+
/**
|
|
50
|
+
* Adopt a pre-3.0 unprefixed entry for `key`, moving it under the current
|
|
51
|
+
* prefix. Returns the adopted value, or null when there is nothing to adopt.
|
|
52
|
+
*
|
|
53
|
+
* This is the whole of the 3.0.0 migration, and it is deliberately **per key,
|
|
54
|
+
* on read** rather than a bulk sweep. A sweep would adopt every unprefixed
|
|
55
|
+
* envelope on the origin — including keys belonging to another instance that
|
|
56
|
+
* opted out of the prefix, or to a sibling application still on 2.x. Adopting
|
|
57
|
+
* only what the caller actually asks for keeps the blast radius to keys this
|
|
58
|
+
* instance already uses.
|
|
59
|
+
*
|
|
60
|
+
* Three conditions, all required:
|
|
61
|
+
* 1. migration is enabled and a prefix is actually in effect (nothing to move
|
|
62
|
+
* data *into* otherwise);
|
|
63
|
+
* 2. the legacy value is **ours** — `parseOwnValue`, the 2.9.0 shape check. It
|
|
64
|
+
* is what makes this safe at all: with no prefix to go on, shape is the only
|
|
65
|
+
* way to tell our data from a third party's;
|
|
66
|
+
* 3. the prefixed slot is **empty**. A value already there is authoritative, so
|
|
67
|
+
* the legacy entry is left alone rather than overwriting newer data.
|
|
68
|
+
*
|
|
69
|
+
* It MOVES rather than copies. A copy leaves a stale duplicate that diverges the
|
|
70
|
+
* moment anything writes — a silent wrong answer, worse than a clean break. A
|
|
71
|
+
* consumer that needs the bare key to keep existing (a pre-paint script, a
|
|
72
|
+
* logger reading its own level) takes `keyPrefix: false` instead.
|
|
73
|
+
*/
|
|
74
|
+
protected adoptLegacyKey<T = unknown>(key: string): StorageValue<T> | null;
|
|
27
75
|
/**
|
|
28
76
|
* Initialize the adapter
|
|
29
77
|
*/
|
|
@@ -98,6 +146,20 @@ export declare class LocalStorageAdapter extends BaseAdapter {
|
|
|
98
146
|
fullKey: string;
|
|
99
147
|
value: StorageValue;
|
|
100
148
|
}>;
|
|
149
|
+
/**
|
|
150
|
+
* Remove an entry `ownKeys()` returned, by its PHYSICAL key.
|
|
151
|
+
*
|
|
152
|
+
* 🔴 Not `removeSync(key)`. That rebuilds the physical key as `prefix + key`,
|
|
153
|
+
* which is wrong for a pre-3.0 legacy entry — those live at the bare key, so
|
|
154
|
+
* `ownKeys()` reports `fullKey === key` for them. Rebuilding would delete
|
|
155
|
+
* `strata:<key>` instead and leave the legacy entry behind, so `clear()` and
|
|
156
|
+
* the expiry sweep would silently skip exactly the entries not yet migrated.
|
|
157
|
+
*/
|
|
158
|
+
protected removeOwnedEntry(entry: {
|
|
159
|
+
key: string;
|
|
160
|
+
fullKey: string;
|
|
161
|
+
value: StorageValue;
|
|
162
|
+
}): void;
|
|
101
163
|
/**
|
|
102
164
|
* Reclaim expired entries, returning how many were removed.
|
|
103
165
|
*
|
|
@@ -9,6 +9,17 @@ import { logger } from "../../utils/logger.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Browser localStorage adapter
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* The key prefix web adapters use unless told otherwise, as of 3.0.0.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 Before 3.0.0 this was the empty string, which meant this library's keys sat
|
|
16
|
+
* unprefixed among every other script's in a shared storage area. 2.9.0 made that
|
|
17
|
+
* safe (a key is ours only if its value is a `StorageValue` envelope); this makes
|
|
18
|
+
* it tidy as well, so our keys are identifiable by name too.
|
|
19
|
+
*
|
|
20
|
+
* Opt out with `defineStorage({ keyPrefix: false })` — see `StrataConfig`.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_WEB_KEY_PREFIX = 'strata:';
|
|
12
23
|
export class LocalStorageAdapter extends BaseAdapter {
|
|
13
24
|
name = 'localStorage';
|
|
14
25
|
capabilities = {
|
|
@@ -24,7 +35,17 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
24
35
|
};
|
|
25
36
|
prefix;
|
|
26
37
|
listeners = new Map();
|
|
27
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Whether to adopt pre-3.0 unprefixed entries on read.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 Defaults to FALSE, and only `Strata` turns it on — for the adapters whose
|
|
42
|
+
* prefix it resolved. A directly constructed adapter must never adopt bare
|
|
43
|
+
* keys: `plugin/web.ts` builds a `strata_prefs_` instance beside the main one,
|
|
44
|
+
* and if that adopted every unprefixed entry it found, it would take them from
|
|
45
|
+
* the instance they belong to.
|
|
46
|
+
*/
|
|
47
|
+
migrateLegacyKeys = false;
|
|
48
|
+
constructor(prefix = DEFAULT_WEB_KEY_PREFIX) {
|
|
28
49
|
super();
|
|
29
50
|
this.prefix = prefix;
|
|
30
51
|
}
|
|
@@ -54,6 +75,70 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
54
75
|
if (config?.prefix !== undefined) {
|
|
55
76
|
this.prefix = config.prefix;
|
|
56
77
|
}
|
|
78
|
+
if (config?.migrateLegacyKeys !== undefined) {
|
|
79
|
+
this.migrateLegacyKeys = config.migrateLegacyKeys;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Adopt a pre-3.0 unprefixed entry for `key`, moving it under the current
|
|
84
|
+
* prefix. Returns the adopted value, or null when there is nothing to adopt.
|
|
85
|
+
*
|
|
86
|
+
* This is the whole of the 3.0.0 migration, and it is deliberately **per key,
|
|
87
|
+
* on read** rather than a bulk sweep. A sweep would adopt every unprefixed
|
|
88
|
+
* envelope on the origin — including keys belonging to another instance that
|
|
89
|
+
* opted out of the prefix, or to a sibling application still on 2.x. Adopting
|
|
90
|
+
* only what the caller actually asks for keeps the blast radius to keys this
|
|
91
|
+
* instance already uses.
|
|
92
|
+
*
|
|
93
|
+
* Three conditions, all required:
|
|
94
|
+
* 1. migration is enabled and a prefix is actually in effect (nothing to move
|
|
95
|
+
* data *into* otherwise);
|
|
96
|
+
* 2. the legacy value is **ours** — `parseOwnValue`, the 2.9.0 shape check. It
|
|
97
|
+
* is what makes this safe at all: with no prefix to go on, shape is the only
|
|
98
|
+
* way to tell our data from a third party's;
|
|
99
|
+
* 3. the prefixed slot is **empty**. A value already there is authoritative, so
|
|
100
|
+
* the legacy entry is left alone rather than overwriting newer data.
|
|
101
|
+
*
|
|
102
|
+
* It MOVES rather than copies. A copy leaves a stale duplicate that diverges the
|
|
103
|
+
* moment anything writes — a silent wrong answer, worse than a clean break. A
|
|
104
|
+
* consumer that needs the bare key to keep existing (a pre-paint script, a
|
|
105
|
+
* logger reading its own level) takes `keyPrefix: false` instead.
|
|
106
|
+
*/
|
|
107
|
+
adoptLegacyKey(key) {
|
|
108
|
+
if (!this.migrateLegacyKeys || !this.prefix)
|
|
109
|
+
return null;
|
|
110
|
+
// A key already carrying our prefix is not a legacy key.
|
|
111
|
+
if (key.startsWith(this.prefix))
|
|
112
|
+
return null;
|
|
113
|
+
let storage;
|
|
114
|
+
try {
|
|
115
|
+
storage = this.getStorage();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const raw = storage.getItem(key);
|
|
121
|
+
if (raw === null)
|
|
122
|
+
return null;
|
|
123
|
+
const value = this.parseOwnValue(raw, key);
|
|
124
|
+
if (!value)
|
|
125
|
+
return null;
|
|
126
|
+
if (storage.getItem(this.prefix + key) !== null) {
|
|
127
|
+
logger.debug(`${this.name}: legacy key "${key}" not adopted — "${this.prefix}${key}" already exists and wins.`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
storage.setItem(this.prefix + key, raw);
|
|
132
|
+
storage.removeItem(key);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
// Out of quota, or the area turned read-only mid-flight. The legacy entry
|
|
136
|
+
// is still intact and still readable, so report and return it.
|
|
137
|
+
logger.warn(`${this.name}: could not migrate legacy key "${key}":`, error);
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
logger.debug(`${this.name}: migrated legacy key "${key}" to "${this.prefix}${key}".`);
|
|
141
|
+
return value;
|
|
57
142
|
}
|
|
58
143
|
/**
|
|
59
144
|
* Initialize the adapter
|
|
@@ -115,7 +200,8 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
115
200
|
}
|
|
116
201
|
// A value that is not our envelope belongs to somebody else sharing this
|
|
117
202
|
// area. parseOwnValue() logs it at debug and returns null — never an error.
|
|
118
|
-
|
|
203
|
+
// A miss falls through to a pre-3.0 unprefixed entry, if there is one.
|
|
204
|
+
const value = this.parseOwnValue(item, key) ?? this.adoptLegacyKey(key);
|
|
119
205
|
if (!value)
|
|
120
206
|
return null;
|
|
121
207
|
if (this.isExpired(value)) {
|
|
@@ -200,7 +286,8 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
200
286
|
// Iterates owned entries INCLUDING expired ones: `expiredOnly` filters on
|
|
201
287
|
// exactly the entries `keysSync()` leaves out, so driving this loop from
|
|
202
288
|
// `keysSync()` made that option a guaranteed no-op.
|
|
203
|
-
for (const
|
|
289
|
+
for (const entry of this.ownKeys(true)) {
|
|
290
|
+
const { key, value } = entry;
|
|
204
291
|
let shouldDelete = true;
|
|
205
292
|
const pattern = options.pattern || options.prefix;
|
|
206
293
|
if (pattern) {
|
|
@@ -215,7 +302,7 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
215
302
|
shouldDelete = false;
|
|
216
303
|
}
|
|
217
304
|
if (shouldDelete) {
|
|
218
|
-
this.
|
|
305
|
+
this.removeOwnedEntry(entry);
|
|
219
306
|
}
|
|
220
307
|
}
|
|
221
308
|
}
|
|
@@ -243,6 +330,7 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
243
330
|
ownKeys(includeExpired = false) {
|
|
244
331
|
const storage = this.getStorage();
|
|
245
332
|
const owned = [];
|
|
333
|
+
const seen = new Set();
|
|
246
334
|
for (let i = 0; i < storage.length; i++) {
|
|
247
335
|
const fullKey = storage.key(i);
|
|
248
336
|
if (!fullKey?.startsWith(this.prefix))
|
|
@@ -253,10 +341,48 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
253
341
|
continue;
|
|
254
342
|
if (!includeExpired && this.isExpired(value))
|
|
255
343
|
continue;
|
|
344
|
+
seen.add(key);
|
|
256
345
|
owned.push({ key, fullKey, value });
|
|
257
346
|
}
|
|
347
|
+
// Pre-3.0 unprefixed entries are still ours and must appear here, or `keys()`,
|
|
348
|
+
// `clear()` and `size()` would silently omit everything not yet read back
|
|
349
|
+
// (adoption is per-read, so a freshly upgraded app has migrated nothing yet).
|
|
350
|
+
//
|
|
351
|
+
// 🔴 Listing is NOT adopting. Enumerating must not move data — a `keys()` call
|
|
352
|
+
// is a question, not a write — so these are reported at their real physical
|
|
353
|
+
// key and migrate only when actually read. A prefixed entry for the same
|
|
354
|
+
// logical key always wins, so upgraded keys are never listed twice.
|
|
355
|
+
if (this.migrateLegacyKeys && this.prefix) {
|
|
356
|
+
for (let i = 0; i < storage.length; i++) {
|
|
357
|
+
const fullKey = storage.key(i);
|
|
358
|
+
if (!fullKey || fullKey.startsWith(this.prefix) || seen.has(fullKey))
|
|
359
|
+
continue;
|
|
360
|
+
const value = this.parseOwnValue(storage.getItem(fullKey), fullKey);
|
|
361
|
+
if (!value)
|
|
362
|
+
continue;
|
|
363
|
+
if (!includeExpired && this.isExpired(value))
|
|
364
|
+
continue;
|
|
365
|
+
seen.add(fullKey);
|
|
366
|
+
owned.push({ key: fullKey, fullKey, value });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
258
369
|
return owned;
|
|
259
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Remove an entry `ownKeys()` returned, by its PHYSICAL key.
|
|
373
|
+
*
|
|
374
|
+
* 🔴 Not `removeSync(key)`. That rebuilds the physical key as `prefix + key`,
|
|
375
|
+
* which is wrong for a pre-3.0 legacy entry — those live at the bare key, so
|
|
376
|
+
* `ownKeys()` reports `fullKey === key` for them. Rebuilding would delete
|
|
377
|
+
* `strata:<key>` instead and leave the legacy entry behind, so `clear()` and
|
|
378
|
+
* the expiry sweep would silently skip exactly the entries not yet migrated.
|
|
379
|
+
*/
|
|
380
|
+
removeOwnedEntry(entry) {
|
|
381
|
+
this.getStorage().removeItem(entry.fullKey);
|
|
382
|
+
if (this.hasChangeListeners()) {
|
|
383
|
+
this.emitChange(entry.key, entry.value.value, undefined, 'local');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
260
386
|
/**
|
|
261
387
|
* Reclaim expired entries, returning how many were removed.
|
|
262
388
|
*
|
|
@@ -269,9 +395,9 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
269
395
|
*/
|
|
270
396
|
async cleanupExpired() {
|
|
271
397
|
let removed = 0;
|
|
272
|
-
for (const
|
|
273
|
-
if (this.isExpired(value)) {
|
|
274
|
-
this.
|
|
398
|
+
for (const entry of this.ownKeys(true)) {
|
|
399
|
+
if (this.isExpired(entry.value)) {
|
|
400
|
+
this.removeOwnedEntry(entry);
|
|
275
401
|
removed++;
|
|
276
402
|
}
|
|
277
403
|
}
|
package/dist/core/BaseAdapter.js
CHANGED
|
@@ -45,6 +45,13 @@ export class BaseAdapter {
|
|
|
45
45
|
logger.error(`TTL cleanup error in ${this.name}:`, error);
|
|
46
46
|
}
|
|
47
47
|
}, this.ttlCheckInterval);
|
|
48
|
+
// 🔴 Do not hold a Node process open. An outstanding interval keeps the event
|
|
49
|
+
// loop alive, so a short-lived script — a build step, a CLI, an SSR warmup —
|
|
50
|
+
// that creates an instance and finishes its work never exits unless it also
|
|
51
|
+
// calls close(). `unref` does not exist in browsers (setInterval returns a
|
|
52
|
+
// number there), so the optional call is simply a no-op; in Node the timer
|
|
53
|
+
// keeps working for as long as the process does.
|
|
54
|
+
this.ttlCleanupInterval.unref?.();
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
50
57
|
* Stop TTL cleanup
|
package/dist/core/Strata.d.ts
CHANGED
|
@@ -320,7 +320,27 @@ export declare class Strata {
|
|
|
320
320
|
* ```
|
|
321
321
|
*/
|
|
322
322
|
registerAdapter(adapter: StorageAdapter): void;
|
|
323
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* Web adapters that share their storage area with every other script on the
|
|
325
|
+
* origin, and therefore take the instance-wide `keyPrefix`.
|
|
326
|
+
*
|
|
327
|
+
* Cookies are excluded deliberately: they already default to `strata_`, so
|
|
328
|
+
* moving them would break existing cookies for no gain. IndexedDB and the Cache
|
|
329
|
+
* API own a named store, memory owns its own Map, and the URL adapter already
|
|
330
|
+
* prefixes its params — none of them can collide with another script's keys.
|
|
331
|
+
*/
|
|
332
|
+
private static readonly PREFIXED_WEB_ADAPTERS;
|
|
333
|
+
/**
|
|
334
|
+
* The configured options for one adapter, or undefined when it has none.
|
|
335
|
+
*
|
|
336
|
+
* For the shared-area web adapters this also resolves the 3.0.0 key prefix and
|
|
337
|
+
* decides whether that adapter may adopt pre-3.0 unprefixed entries. Precedence,
|
|
338
|
+
* and it lives only here:
|
|
339
|
+
*
|
|
340
|
+
* 1. `adapters.<name>.prefix` — the most specific thing the caller wrote
|
|
341
|
+
* 2. `keyPrefix` — the instance-wide switch (`false` restores 2.x behaviour)
|
|
342
|
+
* 3. the adapter's own constructor default (`DEFAULT_WEB_KEY_PREFIX`)
|
|
343
|
+
*/
|
|
324
344
|
private adapterConfigFor;
|
|
325
345
|
/** Push the configured options into an adapter synchronously. */
|
|
326
346
|
private applyAdapterConfig;
|
package/dist/core/Strata.js
CHANGED
|
@@ -116,6 +116,9 @@ export class Strata {
|
|
|
116
116
|
this._ttlCleanupTimer = setInterval(() => {
|
|
117
117
|
void this.cleanupAllAdapters();
|
|
118
118
|
}, interval);
|
|
119
|
+
// See BaseAdapter.startTTLCleanup — a live interval must not keep a Node
|
|
120
|
+
// process alive. No-op in browsers.
|
|
121
|
+
this._ttlCleanupTimer.unref?.();
|
|
119
122
|
}
|
|
120
123
|
// Start periodic auto-backup if configured
|
|
121
124
|
if (this.config.autoBackup?.interval) {
|
|
@@ -1304,10 +1307,49 @@ export class Strata {
|
|
|
1304
1307
|
// to it did not reach the sync path.
|
|
1305
1308
|
this.applyAdapterConfig(adapter);
|
|
1306
1309
|
}
|
|
1307
|
-
/**
|
|
1310
|
+
/**
|
|
1311
|
+
* Web adapters that share their storage area with every other script on the
|
|
1312
|
+
* origin, and therefore take the instance-wide `keyPrefix`.
|
|
1313
|
+
*
|
|
1314
|
+
* Cookies are excluded deliberately: they already default to `strata_`, so
|
|
1315
|
+
* moving them would break existing cookies for no gain. IndexedDB and the Cache
|
|
1316
|
+
* API own a named store, memory owns its own Map, and the URL adapter already
|
|
1317
|
+
* prefixes its params — none of them can collide with another script's keys.
|
|
1318
|
+
*/
|
|
1319
|
+
static PREFIXED_WEB_ADAPTERS = new Set([
|
|
1320
|
+
'localStorage',
|
|
1321
|
+
'sessionStorage',
|
|
1322
|
+
]);
|
|
1323
|
+
/**
|
|
1324
|
+
* The configured options for one adapter, or undefined when it has none.
|
|
1325
|
+
*
|
|
1326
|
+
* For the shared-area web adapters this also resolves the 3.0.0 key prefix and
|
|
1327
|
+
* decides whether that adapter may adopt pre-3.0 unprefixed entries. Precedence,
|
|
1328
|
+
* and it lives only here:
|
|
1329
|
+
*
|
|
1330
|
+
* 1. `adapters.<name>.prefix` — the most specific thing the caller wrote
|
|
1331
|
+
* 2. `keyPrefix` — the instance-wide switch (`false` restores 2.x behaviour)
|
|
1332
|
+
* 3. the adapter's own constructor default (`DEFAULT_WEB_KEY_PREFIX`)
|
|
1333
|
+
*/
|
|
1308
1334
|
adapterConfigFor(name) {
|
|
1309
1335
|
const raw = this.config.adapters?.[name];
|
|
1310
|
-
|
|
1336
|
+
const explicit = typeof raw === 'object' && raw !== null ? { ...raw } : undefined;
|
|
1337
|
+
if (!Strata.PREFIXED_WEB_ADAPTERS.has(name))
|
|
1338
|
+
return explicit;
|
|
1339
|
+
const resolved = explicit ?? {};
|
|
1340
|
+
// Only fill in the prefix when the caller did not name one for this adapter.
|
|
1341
|
+
if (resolved.prefix === undefined && this.config.keyPrefix !== undefined) {
|
|
1342
|
+
resolved.prefix = this.config.keyPrefix === false ? '' : this.config.keyPrefix;
|
|
1343
|
+
}
|
|
1344
|
+
// 🔴 Migration is enabled HERE and nowhere else — only for an adapter whose
|
|
1345
|
+
// prefix this instance resolved. A directly constructed adapter must never
|
|
1346
|
+
// adopt bare keys: `plugin/web.ts` builds a `strata_prefs_` instance beside
|
|
1347
|
+
// the main one, and if that adopted every unprefixed entry it found it would
|
|
1348
|
+
// take them from the instance they belong to.
|
|
1349
|
+
if (resolved.migrateLegacyKeys === undefined) {
|
|
1350
|
+
resolved.migrateLegacyKeys = this.config.migrateLegacyKeys !== false;
|
|
1351
|
+
}
|
|
1352
|
+
return resolved;
|
|
1311
1353
|
}
|
|
1312
1354
|
/** Push the configured options into an adapter synchronously. */
|
|
1313
1355
|
applyAdapterConfig(adapter) {
|
|
@@ -1678,6 +1720,7 @@ export class Strata {
|
|
|
1678
1720
|
}
|
|
1679
1721
|
})();
|
|
1680
1722
|
}, cfg.interval);
|
|
1723
|
+
this._autoBackupTimer.unref?.();
|
|
1681
1724
|
}
|
|
1682
1725
|
async selectAdapter(storage) {
|
|
1683
1726
|
await this.ensureReady();
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { CookieAdapter } from "./adapters/web/CookieAdapter.js";
|
|
|
8
8
|
export { CacheAdapter } from "./adapters/web/CacheAdapter.js";
|
|
9
9
|
export { MemoryAdapter } from "./adapters/web/MemoryAdapter.js";
|
|
10
10
|
export { URLAdapter, type URLAdapterConfig } from "./adapters/web/URLAdapter.js";
|
|
11
|
+
export { DEFAULT_WEB_KEY_PREFIX } from "./adapters/web/LocalStorageAdapter.js";
|
|
11
12
|
export { EncryptionManager } from "./features/encryption.js";
|
|
12
13
|
export { CompressionManager } from "./features/compression.js";
|
|
13
14
|
export { TTLManager } from "./features/ttl.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { CookieAdapter } from "./adapters/web/CookieAdapter.js";
|
|
|
10
10
|
export { CacheAdapter } from "./adapters/web/CacheAdapter.js";
|
|
11
11
|
export { MemoryAdapter } from "./adapters/web/MemoryAdapter.js";
|
|
12
12
|
export { URLAdapter } from "./adapters/web/URLAdapter.js";
|
|
13
|
+
export { DEFAULT_WEB_KEY_PREFIX } from "./adapters/web/LocalStorageAdapter.js";
|
|
13
14
|
// Core features
|
|
14
15
|
export { EncryptionManager } from "./features/encryption.js";
|
|
15
16
|
export { CompressionManager } from "./features/compression.js";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -308,6 +308,38 @@ export interface StrataConfig {
|
|
|
308
308
|
* Default storage types in order of preference
|
|
309
309
|
*/
|
|
310
310
|
defaultStorages?: StorageType[];
|
|
311
|
+
/**
|
|
312
|
+
* Key prefix for the web adapters that share a storage area with every other
|
|
313
|
+
* script on the origin (`localStorage`, `sessionStorage`).
|
|
314
|
+
*
|
|
315
|
+
* Defaults to `'strata:'` as of 3.0.0. Set `false` (or `''`) for the pre-3.0
|
|
316
|
+
* behaviour of writing to the bare key.
|
|
317
|
+
*
|
|
318
|
+
* 🔴 **Take the opt-out when anything outside this library reads a physical key
|
|
319
|
+
* directly** — a pre-paint theme script that runs before any module loads, or a
|
|
320
|
+
* logger reading its own level. Those readers know the exact key name, and a
|
|
321
|
+
* prefix changes it underneath them. `migrateLegacyKeys` keeps the *data*
|
|
322
|
+
* reachable through this library, but it cannot fix a hard-coded reader.
|
|
323
|
+
*
|
|
324
|
+
* Composes with `namespace`, which is a separate mechanism and unaffected:
|
|
325
|
+
* the physical key is `<keyPrefix><namespace>:<key>`. A per-adapter
|
|
326
|
+
* `adapters.localStorage.prefix` overrides this for that adapter.
|
|
327
|
+
*/
|
|
328
|
+
keyPrefix?: string | false;
|
|
329
|
+
/**
|
|
330
|
+
* Whether the shared-area web adapters may adopt pre-3.0 unprefixed entries.
|
|
331
|
+
* Default `true`.
|
|
332
|
+
*
|
|
333
|
+
* Migration is per key and happens on read: a miss at the prefixed key falls
|
|
334
|
+
* back to the bare key, and if the value is one of ours it is moved under the
|
|
335
|
+
* prefix. It never overwrites an existing prefixed value, and it never adopts a
|
|
336
|
+
* value that is not a `StorageValue` envelope — which is what stops it taking
|
|
337
|
+
* another application's keys.
|
|
338
|
+
*
|
|
339
|
+
* Set `false` when two applications share an origin and one of them is still on
|
|
340
|
+
* 2.x, so that upgrading one does not move keys the other still reads.
|
|
341
|
+
*/
|
|
342
|
+
migrateLegacyKeys?: boolean;
|
|
311
343
|
/**
|
|
312
344
|
* Adapter configuration
|
|
313
345
|
*/
|