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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides persistent storage with 5-10MB limit
|
|
4
4
|
*/
|
|
5
5
|
import { BaseAdapter } from "../../core/BaseAdapter.js";
|
|
6
|
-
import { serialize,
|
|
6
|
+
import { serialize, getObjectSize } from "../../utils/index.js";
|
|
7
7
|
import { QuotaExceededError, SerializationError, StorageError } from "../../utils/errors.js";
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
9
|
/**
|
|
@@ -47,14 +47,39 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Apply configuration synchronously. See `BaseAdapter.configure` for why the
|
|
51
|
+
* prefix cannot wait for the async `initialize()`.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
-
if (config?.prefix) {
|
|
53
|
+
configure(config) {
|
|
54
|
+
if (config?.prefix !== undefined) {
|
|
54
55
|
this.prefix = config.prefix;
|
|
55
56
|
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Initialize the adapter
|
|
60
|
+
*/
|
|
61
|
+
async initialize(config) {
|
|
62
|
+
this.configure(config);
|
|
56
63
|
this.startTTLCleanup();
|
|
57
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether this storage area is usable RIGHT NOW, without awaiting anything.
|
|
67
|
+
* The synchronous API needs this: `defaultStorages` reads as an ordered
|
|
68
|
+
* fallback list, and without a sync probe `setSync` selects an unusable
|
|
69
|
+
* backend and throws instead of falling through to the next one.
|
|
70
|
+
*/
|
|
71
|
+
isAvailableSync() {
|
|
72
|
+
try {
|
|
73
|
+
const storage = this.getStorage();
|
|
74
|
+
const testKey = `${this.prefix}__test__`;
|
|
75
|
+
storage.setItem(testKey, 'test');
|
|
76
|
+
storage.removeItem(testKey);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
/**
|
|
59
84
|
* Get the backing Storage object.
|
|
60
85
|
* Subclasses (e.g. SessionStorageAdapter) override this to target a
|
|
@@ -77,22 +102,27 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
77
102
|
* Get a value from localStorage (synchronous)
|
|
78
103
|
*/
|
|
79
104
|
getSync(key) {
|
|
105
|
+
let item;
|
|
80
106
|
try {
|
|
81
|
-
|
|
82
|
-
if (!item)
|
|
83
|
-
return null;
|
|
84
|
-
const value = deserialize(item);
|
|
85
|
-
// Check TTL
|
|
86
|
-
if (this.isExpired(value)) {
|
|
87
|
-
this.removeSync(key);
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
return value;
|
|
107
|
+
item = this.getStorage().getItem(this.prefix + key);
|
|
91
108
|
}
|
|
92
109
|
catch (error) {
|
|
93
|
-
|
|
110
|
+
// A genuine storage-access fault — the area is blocked (private mode, a
|
|
111
|
+
// cookie policy, a SecurityError on an opaque origin). This one IS ours to
|
|
112
|
+
// report: the read we were asked for did not happen.
|
|
113
|
+
logger.error(`Failed to read key ${key} from ${this.name}:`, error);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// A value that is not our envelope belongs to somebody else sharing this
|
|
117
|
+
// area. parseOwnValue() logs it at debug and returns null — never an error.
|
|
118
|
+
const value = this.parseOwnValue(item, key);
|
|
119
|
+
if (!value)
|
|
120
|
+
return null;
|
|
121
|
+
if (this.isExpired(value)) {
|
|
122
|
+
this.removeSync(key);
|
|
94
123
|
return null;
|
|
95
124
|
}
|
|
125
|
+
return value;
|
|
96
126
|
}
|
|
97
127
|
/**
|
|
98
128
|
* Set a value in localStorage
|
|
@@ -137,15 +167,7 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
137
167
|
// overflow). Only read when a listener actually needs the old value.
|
|
138
168
|
let oldValue = null;
|
|
139
169
|
if (this.hasChangeListeners()) {
|
|
140
|
-
|
|
141
|
-
if (item) {
|
|
142
|
-
try {
|
|
143
|
-
oldValue = deserialize(item);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
oldValue = null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
170
|
+
oldValue = this.parseOwnValue(this.getStorage().getItem(this.prefix + key), key);
|
|
149
171
|
}
|
|
150
172
|
this.getStorage().removeItem(this.prefix + key);
|
|
151
173
|
if (oldValue) {
|
|
@@ -164,37 +186,33 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
164
186
|
clearSync(options) {
|
|
165
187
|
if (!options ||
|
|
166
188
|
(!options.pattern && !options.prefix && !options.tags && !options.expiredOnly)) {
|
|
167
|
-
// Clear
|
|
189
|
+
// Clear everything WE wrote — never the whole area. With an empty prefix a
|
|
190
|
+
// name-only sweep here deletes every key on the origin, including another
|
|
191
|
+
// application's; ownKeys() bounds it to our own envelopes.
|
|
168
192
|
const storage = this.getStorage();
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const key = storage.key(i);
|
|
172
|
-
if (key?.startsWith(this.prefix)) {
|
|
173
|
-
keysToRemove.push(key);
|
|
174
|
-
}
|
|
193
|
+
for (const { fullKey } of this.ownKeys(true)) {
|
|
194
|
+
storage.removeItem(fullKey);
|
|
175
195
|
}
|
|
176
|
-
keysToRemove.forEach((key) => storage.removeItem(key));
|
|
177
196
|
this.emitChange('*', undefined, undefined, 'local');
|
|
178
197
|
return;
|
|
179
198
|
}
|
|
180
|
-
// Synchronous filtered clear (mirrors BaseAdapter.clear logic)
|
|
181
|
-
|
|
199
|
+
// Synchronous filtered clear (mirrors BaseAdapter.clear logic).
|
|
200
|
+
// Iterates owned entries INCLUDING expired ones: `expiredOnly` filters on
|
|
201
|
+
// exactly the entries `keysSync()` leaves out, so driving this loop from
|
|
202
|
+
// `keysSync()` made that option a guaranteed no-op.
|
|
203
|
+
for (const { key, value } of this.ownKeys(true)) {
|
|
182
204
|
let shouldDelete = true;
|
|
183
205
|
const pattern = options.pattern || options.prefix;
|
|
184
206
|
if (pattern) {
|
|
185
207
|
shouldDelete = this.filterKeys([key], pattern).length > 0;
|
|
186
208
|
}
|
|
187
209
|
if (shouldDelete && options.tags) {
|
|
188
|
-
|
|
189
|
-
if (!value?.tags || !options.tags.some((tag) => value.tags?.includes(tag))) {
|
|
210
|
+
if (!value.tags || !options.tags.some((tag) => value.tags?.includes(tag))) {
|
|
190
211
|
shouldDelete = false;
|
|
191
212
|
}
|
|
192
213
|
}
|
|
193
|
-
if (shouldDelete && options.expiredOnly) {
|
|
194
|
-
|
|
195
|
-
if (!value || !this.isExpired(value)) {
|
|
196
|
-
shouldDelete = false;
|
|
197
|
-
}
|
|
214
|
+
if (shouldDelete && options.expiredOnly && !this.isExpired(value)) {
|
|
215
|
+
shouldDelete = false;
|
|
198
216
|
}
|
|
199
217
|
if (shouldDelete) {
|
|
200
218
|
this.removeSync(key);
|
|
@@ -211,20 +229,53 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
211
229
|
* Get all keys (synchronous)
|
|
212
230
|
*/
|
|
213
231
|
keysSync(pattern) {
|
|
232
|
+
return this.filterKeys(this.ownKeys().map((entry) => entry.key), pattern);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Every key in this storage area that this adapter actually owns, with the
|
|
236
|
+
* envelope already parsed (one read per key, not two).
|
|
237
|
+
*
|
|
238
|
+
* 🔴 Name is not enough. With the default empty prefix `startsWith(prefix)` is
|
|
239
|
+
* true for EVERY key on the origin, so this method — not the prefix — is what
|
|
240
|
+
* keeps `keys()`, the TTL sweep and `clear()` off other scripts' data. An
|
|
241
|
+
* expired entry is skipped here exactly as before.
|
|
242
|
+
*/
|
|
243
|
+
ownKeys(includeExpired = false) {
|
|
214
244
|
const storage = this.getStorage();
|
|
215
|
-
const
|
|
245
|
+
const owned = [];
|
|
216
246
|
for (let i = 0; i < storage.length; i++) {
|
|
217
247
|
const fullKey = storage.key(i);
|
|
218
|
-
if (fullKey?.startsWith(this.prefix))
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
if (!fullKey?.startsWith(this.prefix))
|
|
249
|
+
continue;
|
|
250
|
+
const key = fullKey.substring(this.prefix.length);
|
|
251
|
+
const value = this.parseOwnValue(storage.getItem(fullKey), key);
|
|
252
|
+
if (!value)
|
|
253
|
+
continue;
|
|
254
|
+
if (!includeExpired && this.isExpired(value))
|
|
255
|
+
continue;
|
|
256
|
+
owned.push({ key, fullKey, value });
|
|
257
|
+
}
|
|
258
|
+
return owned;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Reclaim expired entries, returning how many were removed.
|
|
262
|
+
*
|
|
263
|
+
* Overrides the base per-key sweep for two reasons. It reads each key once
|
|
264
|
+
* instead of twice, and — the load-bearing one — the base sweep is built on
|
|
265
|
+
* `keys()`, which does not surface expired entries here, so it could only ever
|
|
266
|
+
* report 0. Before this override the reaping happened as an undocumented side
|
|
267
|
+
* effect of `getSync()` deleting what it found expired during enumeration,
|
|
268
|
+
* while the returned count stayed 0.
|
|
269
|
+
*/
|
|
270
|
+
async cleanupExpired() {
|
|
271
|
+
let removed = 0;
|
|
272
|
+
for (const { key, value } of this.ownKeys(true)) {
|
|
273
|
+
if (this.isExpired(value)) {
|
|
274
|
+
this.removeSync(key);
|
|
275
|
+
removed++;
|
|
225
276
|
}
|
|
226
277
|
}
|
|
227
|
-
return
|
|
278
|
+
return removed;
|
|
228
279
|
}
|
|
229
280
|
/**
|
|
230
281
|
* Check if key exists (synchronous)
|
|
@@ -242,20 +293,21 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
242
293
|
let keySize = 0;
|
|
243
294
|
let valueSize = 0;
|
|
244
295
|
const byKey = {};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
296
|
+
// `this.getStorage()`, never `window.localStorage` — the subclass points at a
|
|
297
|
+
// different area, and hard-coding it here made every inherited method wrong
|
|
298
|
+
// for sessionStorage until the subclass re-implemented it. Owned keys only,
|
|
299
|
+
// so a shared area is not reported as this adapter's footprint.
|
|
300
|
+
const storage = this.getStorage();
|
|
301
|
+
for (const { fullKey, key } of this.ownKeys(true)) {
|
|
302
|
+
const item = storage.getItem(fullKey);
|
|
303
|
+
if (item) {
|
|
304
|
+
count++;
|
|
305
|
+
const itemSize = (fullKey.length + item.length) * 2; // UTF-16
|
|
306
|
+
total += itemSize;
|
|
307
|
+
if (detailed) {
|
|
308
|
+
keySize += fullKey.length * 2;
|
|
309
|
+
valueSize += item.length * 2;
|
|
310
|
+
byKey[key] = itemSize;
|
|
259
311
|
}
|
|
260
312
|
}
|
|
261
313
|
}
|
|
@@ -278,15 +330,27 @@ export class LocalStorageAdapter extends BaseAdapter {
|
|
|
278
330
|
const unsubscribeLocal = super.subscribe(callback);
|
|
279
331
|
// Also subscribe to remote changes via storage events
|
|
280
332
|
const listener = (event) => {
|
|
281
|
-
// Only process events from
|
|
282
|
-
|
|
333
|
+
// Only process events from this adapter's own area. Comparing against
|
|
334
|
+
// `window.localStorage` by name meant the sessionStorage subclass could
|
|
335
|
+
// never match its own events.
|
|
336
|
+
let area;
|
|
337
|
+
try {
|
|
338
|
+
area = this.getStorage();
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
283
341
|
return;
|
|
284
|
-
|
|
342
|
+
}
|
|
343
|
+
if (event.storageArea !== area)
|
|
344
|
+
return;
|
|
345
|
+
// Check if the key belongs to us — by name AND by shape, since an empty
|
|
346
|
+
// prefix matches every key another script on this origin writes.
|
|
285
347
|
if (!event.key || !event.key.startsWith(this.prefix))
|
|
286
348
|
return;
|
|
287
349
|
const key = event.key.substring(this.prefix.length);
|
|
288
|
-
const oldValue =
|
|
289
|
-
const newValue =
|
|
350
|
+
const oldValue = this.parseOwnValue(event.oldValue, key);
|
|
351
|
+
const newValue = this.parseOwnValue(event.newValue, key);
|
|
352
|
+
if (!oldValue && !newValue)
|
|
353
|
+
return;
|
|
290
354
|
callback({
|
|
291
355
|
key,
|
|
292
356
|
oldValue: oldValue?.value ?? undefined,
|
|
@@ -20,6 +20,11 @@ export declare class MemoryAdapter extends BaseAdapter {
|
|
|
20
20
|
/**
|
|
21
21
|
* Initialize the adapter
|
|
22
22
|
*/
|
|
23
|
+
configure(config?: {
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
}): void;
|
|
26
|
+
/** Memory is always usable — no environment can take it away. */
|
|
27
|
+
isAvailableSync(): boolean;
|
|
23
28
|
initialize(config?: {
|
|
24
29
|
maxSize?: number;
|
|
25
30
|
}): Promise<void>;
|
|
@@ -33,8 +33,16 @@ export class MemoryAdapter extends BaseAdapter {
|
|
|
33
33
|
/**
|
|
34
34
|
* Initialize the adapter
|
|
35
35
|
*/
|
|
36
|
+
configure(config) {
|
|
37
|
+
if (config?.maxSize !== undefined)
|
|
38
|
+
this.maxSize = config.maxSize;
|
|
39
|
+
}
|
|
40
|
+
/** Memory is always usable — no environment can take it away. */
|
|
41
|
+
isAvailableSync() {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
36
44
|
async initialize(config) {
|
|
37
|
-
this.
|
|
45
|
+
this.configure(config);
|
|
38
46
|
this.startTTLCleanup();
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
import { LocalStorageAdapter } from "./LocalStorageAdapter.js";
|
|
6
6
|
import type { StorageType, StorageCapabilities } from "../../types/index.js";
|
|
7
7
|
/**
|
|
8
|
-
* Browser sessionStorage adapter
|
|
9
|
-
*
|
|
8
|
+
* Browser sessionStorage adapter.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 This class overrides ONLY what actually differs: the identity, the
|
|
11
|
+
* capabilities, and which `Storage` object to talk to. Everything else is
|
|
12
|
+
* inherited, because `LocalStorageAdapter` routes every read and write through
|
|
13
|
+
* `getStorage()`.
|
|
14
|
+
*
|
|
15
|
+
* It used to re-implement `get`/`set`/`remove`/`clear`/`keys`/`size`/`subscribe`
|
|
16
|
+
* — ~230 lines differing only by naming `window.sessionStorage`. That is how one
|
|
17
|
+
* defect became two issue numbers: the copy carried its own
|
|
18
|
+
* `logger.error('Failed to get key … from sessionStorage')`, so fixing the
|
|
19
|
+
* localStorage path left the sessionStorage path untouched, and the reports came
|
|
20
|
+
* back separately (ISSUE-01 for `localStorage`, ISSUE-09 for `sessionStorage`,
|
|
21
|
+
* one root cause). Keep the override surface minimal so that cannot recur.
|
|
10
22
|
*/
|
|
11
23
|
export declare class SessionStorageAdapter extends LocalStorageAdapter {
|
|
12
24
|
readonly name: StorageType;
|
|
@@ -17,60 +29,8 @@ export declare class SessionStorageAdapter extends LocalStorageAdapter {
|
|
|
17
29
|
*/
|
|
18
30
|
isAvailable(): Promise<boolean>;
|
|
19
31
|
/**
|
|
20
|
-
*
|
|
32
|
+
* The backing Storage object. This is the ONLY thing that differs from the
|
|
33
|
+
* parent — every inherited method reads and writes through it.
|
|
21
34
|
*/
|
|
22
35
|
protected getStorage(): Storage;
|
|
23
|
-
/**
|
|
24
|
-
* Get a value from sessionStorage
|
|
25
|
-
*/
|
|
26
|
-
get<T = unknown>(key: string): Promise<import("../../types/index.js").StorageValue<T> | null>;
|
|
27
|
-
/**
|
|
28
|
-
* Get a value from sessionStorage (synchronous)
|
|
29
|
-
*/
|
|
30
|
-
getSync<T = unknown>(key: string): import("../../types/index.js").StorageValue<T> | null;
|
|
31
|
-
/**
|
|
32
|
-
* Set a value in sessionStorage
|
|
33
|
-
*/
|
|
34
|
-
set<T = unknown>(key: string, value: import("../../types/index.js").StorageValue<T>): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
* Set a value in sessionStorage (synchronous)
|
|
37
|
-
*/
|
|
38
|
-
setSync<T = unknown>(key: string, value: import("../../types/index.js").StorageValue<T>): void;
|
|
39
|
-
/**
|
|
40
|
-
* Remove a value from sessionStorage
|
|
41
|
-
*/
|
|
42
|
-
remove(key: string): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
* Remove a value from sessionStorage (synchronous)
|
|
45
|
-
*/
|
|
46
|
-
removeSync(key: string): void;
|
|
47
|
-
/**
|
|
48
|
-
* Clear sessionStorage
|
|
49
|
-
*/
|
|
50
|
-
clear(options?: import("../../types/index.js").ClearOptions): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Clear sessionStorage (synchronous)
|
|
53
|
-
*/
|
|
54
|
-
clearSync(options?: import("../../types/index.js").ClearOptions): void;
|
|
55
|
-
/**
|
|
56
|
-
* Get all keys
|
|
57
|
-
*/
|
|
58
|
-
keys(pattern?: string | RegExp): Promise<string[]>;
|
|
59
|
-
/**
|
|
60
|
-
* Get all keys (synchronous)
|
|
61
|
-
*/
|
|
62
|
-
keysSync(pattern?: string | RegExp): string[];
|
|
63
|
-
/**
|
|
64
|
-
* Check if key exists (synchronous)
|
|
65
|
-
*/
|
|
66
|
-
hasSync(key: string): boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Get storage size
|
|
69
|
-
*/
|
|
70
|
-
size(detailed?: boolean): Promise<import("../../types/index.js").SizeInfo>;
|
|
71
|
-
/**
|
|
72
|
-
* Subscribe to storage changes
|
|
73
|
-
* Note: sessionStorage doesn't fire storage events in the same tab
|
|
74
|
-
*/
|
|
75
|
-
subscribe(callback: import("../../types/index.js").SubscriptionCallback): import("../../types/index.js").UnsubscribeFunction;
|
|
76
36
|
}
|
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
* Provides session-scoped storage with 5-10MB limit
|
|
4
4
|
*/
|
|
5
5
|
import { LocalStorageAdapter } from "./LocalStorageAdapter.js";
|
|
6
|
-
import {
|
|
7
|
-
import { QuotaExceededError, SerializationError, StorageError } from "../../utils/errors.js";
|
|
8
|
-
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { StorageError } from "../../utils/errors.js";
|
|
9
7
|
/**
|
|
10
|
-
* Browser sessionStorage adapter
|
|
11
|
-
*
|
|
8
|
+
* Browser sessionStorage adapter.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 This class overrides ONLY what actually differs: the identity, the
|
|
11
|
+
* capabilities, and which `Storage` object to talk to. Everything else is
|
|
12
|
+
* inherited, because `LocalStorageAdapter` routes every read and write through
|
|
13
|
+
* `getStorage()`.
|
|
14
|
+
*
|
|
15
|
+
* It used to re-implement `get`/`set`/`remove`/`clear`/`keys`/`size`/`subscribe`
|
|
16
|
+
* — ~230 lines differing only by naming `window.sessionStorage`. That is how one
|
|
17
|
+
* defect became two issue numbers: the copy carried its own
|
|
18
|
+
* `logger.error('Failed to get key … from sessionStorage')`, so fixing the
|
|
19
|
+
* localStorage path left the sessionStorage path untouched, and the reports came
|
|
20
|
+
* back separately (ISSUE-01 for `localStorage`, ISSUE-09 for `sessionStorage`,
|
|
21
|
+
* one root cause). Keep the override surface minimal so that cannot recur.
|
|
12
22
|
*/
|
|
13
23
|
export class SessionStorageAdapter extends LocalStorageAdapter {
|
|
14
24
|
name = 'sessionStorage';
|
|
@@ -30,22 +40,14 @@ export class SessionStorageAdapter extends LocalStorageAdapter {
|
|
|
30
40
|
* Check if sessionStorage is available
|
|
31
41
|
*/
|
|
32
42
|
async isAvailable() {
|
|
33
|
-
|
|
34
|
-
if (typeof window === 'undefined' || !window.sessionStorage) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
// Test if we can actually use it
|
|
38
|
-
const testKey = `${this.prefix}__test__`;
|
|
39
|
-
window.sessionStorage.setItem(testKey, 'test');
|
|
40
|
-
window.sessionStorage.removeItem(testKey);
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
43
|
+
if (typeof window === 'undefined' || !window.sessionStorage) {
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
|
+
return this.isAvailableSync();
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
+
* The backing Storage object. This is the ONLY thing that differs from the
|
|
50
|
+
* parent — every inherited method reads and writes through it.
|
|
49
51
|
*/
|
|
50
52
|
getStorage() {
|
|
51
53
|
if (typeof window === 'undefined' || !window.sessionStorage) {
|
|
@@ -53,210 +55,4 @@ export class SessionStorageAdapter extends LocalStorageAdapter {
|
|
|
53
55
|
}
|
|
54
56
|
return window.sessionStorage;
|
|
55
57
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Get a value from sessionStorage
|
|
58
|
-
*/
|
|
59
|
-
async get(key) {
|
|
60
|
-
return this.getSync(key);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Get a value from sessionStorage (synchronous)
|
|
64
|
-
*/
|
|
65
|
-
getSync(key) {
|
|
66
|
-
try {
|
|
67
|
-
const item = window.sessionStorage.getItem(this.prefix + key);
|
|
68
|
-
if (!item)
|
|
69
|
-
return null;
|
|
70
|
-
const value = deserialize(item);
|
|
71
|
-
// Check TTL
|
|
72
|
-
if (this.isExpired(value)) {
|
|
73
|
-
this.removeSync(key);
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
logger.error(`Failed to get key ${key} from sessionStorage:`, error);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Set a value in sessionStorage
|
|
85
|
-
*/
|
|
86
|
-
async set(key, value) {
|
|
87
|
-
this.setSync(key, value);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Set a value in sessionStorage (synchronous)
|
|
91
|
-
*/
|
|
92
|
-
setSync(key, value) {
|
|
93
|
-
const fullKey = this.prefix + key;
|
|
94
|
-
const oldValue = this.getSync(key);
|
|
95
|
-
try {
|
|
96
|
-
const serialized = serialize(value);
|
|
97
|
-
window.sessionStorage.setItem(fullKey, serialized);
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
if (this.isQuotaError(error)) {
|
|
101
|
-
throw new QuotaExceededError('SessionStorage quota exceeded', { key, error });
|
|
102
|
-
}
|
|
103
|
-
throw new SerializationError(`Failed to store key ${key} in sessionStorage`, error);
|
|
104
|
-
}
|
|
105
|
-
// Emit change event
|
|
106
|
-
this.emitChange(key, oldValue?.value, value.value, 'local');
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Remove a value from sessionStorage
|
|
110
|
-
*/
|
|
111
|
-
async remove(key) {
|
|
112
|
-
this.removeSync(key);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Remove a value from sessionStorage (synchronous)
|
|
116
|
-
*/
|
|
117
|
-
removeSync(key) {
|
|
118
|
-
// Read raw — NOT via getSync(), which deletes expired entries by calling
|
|
119
|
-
// removeSync() and would recurse here forever. Only read with a listener.
|
|
120
|
-
let oldValue = null;
|
|
121
|
-
if (this.hasChangeListeners()) {
|
|
122
|
-
const item = window.sessionStorage.getItem(this.prefix + key);
|
|
123
|
-
if (item) {
|
|
124
|
-
try {
|
|
125
|
-
oldValue = deserialize(item);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
oldValue = null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
window.sessionStorage.removeItem(this.prefix + key);
|
|
133
|
-
if (oldValue) {
|
|
134
|
-
this.emitChange(key, oldValue.value, undefined, 'local');
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Clear sessionStorage
|
|
139
|
-
*/
|
|
140
|
-
async clear(options) {
|
|
141
|
-
this.clearSync(options);
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Clear sessionStorage (synchronous)
|
|
145
|
-
*/
|
|
146
|
-
clearSync(options) {
|
|
147
|
-
if (!options ||
|
|
148
|
-
(!options.pattern && !options.prefix && !options.tags && !options.expiredOnly)) {
|
|
149
|
-
// Clear all with our prefix
|
|
150
|
-
const keysToRemove = [];
|
|
151
|
-
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
152
|
-
const key = window.sessionStorage.key(i);
|
|
153
|
-
if (key?.startsWith(this.prefix)) {
|
|
154
|
-
keysToRemove.push(key);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
keysToRemove.forEach((key) => window.sessionStorage.removeItem(key));
|
|
158
|
-
this.emitChange('*', undefined, undefined, 'local');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
// Synchronous filtered clear (mirrors BaseAdapter.clear logic)
|
|
162
|
-
for (const key of this.keysSync()) {
|
|
163
|
-
let shouldDelete = true;
|
|
164
|
-
const pattern = options.pattern || options.prefix;
|
|
165
|
-
if (pattern) {
|
|
166
|
-
shouldDelete = this.filterKeys([key], pattern).length > 0;
|
|
167
|
-
}
|
|
168
|
-
if (shouldDelete && options.tags) {
|
|
169
|
-
const value = this.getSync(key);
|
|
170
|
-
if (!value?.tags || !options.tags.some((tag) => value.tags?.includes(tag))) {
|
|
171
|
-
shouldDelete = false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (shouldDelete && options.expiredOnly) {
|
|
175
|
-
const value = this.getSync(key);
|
|
176
|
-
if (!value || !this.isExpired(value)) {
|
|
177
|
-
shouldDelete = false;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (shouldDelete) {
|
|
181
|
-
this.removeSync(key);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Get all keys
|
|
187
|
-
*/
|
|
188
|
-
async keys(pattern) {
|
|
189
|
-
return this.keysSync(pattern);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Get all keys (synchronous)
|
|
193
|
-
*/
|
|
194
|
-
keysSync(pattern) {
|
|
195
|
-
const keys = [];
|
|
196
|
-
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
197
|
-
const fullKey = window.sessionStorage.key(i);
|
|
198
|
-
if (fullKey?.startsWith(this.prefix)) {
|
|
199
|
-
const key = fullKey.substring(this.prefix.length);
|
|
200
|
-
// Check if not expired
|
|
201
|
-
const value = this.getSync(key);
|
|
202
|
-
if (value) {
|
|
203
|
-
keys.push(key);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return this.filterKeys(keys, pattern);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Check if key exists (synchronous)
|
|
211
|
-
*/
|
|
212
|
-
hasSync(key) {
|
|
213
|
-
const value = this.getSync(key);
|
|
214
|
-
return value !== null && !this.isExpired(value);
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Get storage size
|
|
218
|
-
*/
|
|
219
|
-
async size(detailed) {
|
|
220
|
-
let total = 0;
|
|
221
|
-
let count = 0;
|
|
222
|
-
let keySize = 0;
|
|
223
|
-
let valueSize = 0;
|
|
224
|
-
const byKey = {};
|
|
225
|
-
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
226
|
-
const fullKey = window.sessionStorage.key(i);
|
|
227
|
-
if (fullKey?.startsWith(this.prefix)) {
|
|
228
|
-
const item = window.sessionStorage.getItem(fullKey);
|
|
229
|
-
if (item) {
|
|
230
|
-
count++;
|
|
231
|
-
const key = fullKey.substring(this.prefix.length);
|
|
232
|
-
const itemSize = (fullKey.length + item.length) * 2; // UTF-16
|
|
233
|
-
total += itemSize;
|
|
234
|
-
if (detailed) {
|
|
235
|
-
keySize += fullKey.length * 2;
|
|
236
|
-
valueSize += item.length * 2;
|
|
237
|
-
byKey[key] = itemSize;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const result = { total, count };
|
|
243
|
-
if (detailed) {
|
|
244
|
-
result.byKey = byKey;
|
|
245
|
-
result.detailed = {
|
|
246
|
-
keys: keySize,
|
|
247
|
-
values: valueSize,
|
|
248
|
-
metadata: 0,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
return result;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Subscribe to storage changes
|
|
255
|
-
* Note: sessionStorage doesn't fire storage events in the same tab
|
|
256
|
-
*/
|
|
257
|
-
subscribe(callback) {
|
|
258
|
-
// For sessionStorage, we only get local changes, not cross-tab
|
|
259
|
-
// Use the base class subscription for local changes
|
|
260
|
-
return super.subscribe(callback);
|
|
261
|
-
}
|
|
262
58
|
}
|
|
@@ -35,6 +35,9 @@ export declare class URLAdapter extends BaseAdapter {
|
|
|
35
35
|
private snapshot;
|
|
36
36
|
private changeListener?;
|
|
37
37
|
isAvailable(): Promise<boolean>;
|
|
38
|
+
configure(config?: URLAdapterConfig): void;
|
|
39
|
+
/** The URL is usable whenever `window.location` is. */
|
|
40
|
+
isAvailableSync(): boolean;
|
|
38
41
|
initialize(config?: URLAdapterConfig): Promise<void>;
|
|
39
42
|
getSync<T = unknown>(key: string): StorageValue<T> | null;
|
|
40
43
|
setSync<T = unknown>(key: string, value: StorageValue<T>): void;
|