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.
@@ -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, deserialize, getObjectSize } from "../../utils/index.js";
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
- * Initialize the adapter
50
+ * Apply configuration synchronously. See `BaseAdapter.configure` for why the
51
+ * prefix cannot wait for the async `initialize()`.
51
52
  */
52
- async initialize(config) {
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
- const item = this.getStorage().getItem(this.prefix + key);
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
- logger.error(`Failed to get key ${key} from ${this.name}:`, error);
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
- const item = this.getStorage().getItem(this.prefix + key);
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 all with our prefix
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 keysToRemove = [];
170
- for (let i = 0; i < storage.length; i++) {
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
- for (const key of this.keysSync()) {
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
- const value = this.getSync(key);
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
- const value = this.getSync(key);
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 keys = [];
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
- const key = fullKey.substring(this.prefix.length);
220
- // Check if not expired
221
- const value = this.getSync(key);
222
- if (value) {
223
- keys.push(key);
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 this.filterKeys(keys, pattern);
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
- for (let i = 0; i < window.localStorage.length; i++) {
246
- const fullKey = window.localStorage.key(i);
247
- if (fullKey?.startsWith(this.prefix)) {
248
- const item = window.localStorage.getItem(fullKey);
249
- if (item) {
250
- count++;
251
- const key = fullKey.substring(this.prefix.length);
252
- const itemSize = (fullKey.length + item.length) * 2; // UTF-16
253
- total += itemSize;
254
- if (detailed) {
255
- keySize += fullKey.length * 2;
256
- valueSize += item.length * 2;
257
- byKey[key] = itemSize;
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 other windows/tabs
282
- if (event.storageArea !== window.localStorage)
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
- // Check if the key belongs to us
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 = event.oldValue ? deserialize(event.oldValue) : null;
289
- const newValue = event.newValue ? deserialize(event.newValue) : null;
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.maxSize = config?.maxSize;
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
- * Extends LocalStorageAdapter as the API is identical
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
- * Override all methods to use sessionStorage instead of localStorage
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 { serialize, deserialize } from "../../utils/index.js";
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
- * Extends LocalStorageAdapter as the API is identical
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
- try {
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
- * Override all methods to use sessionStorage instead of localStorage
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;