strata-storage 2.8.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-INTEGRATION-GUIDE.md +43 -0
- package/CHANGELOG.md +151 -0
- package/README.md +17 -9
- 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 +101 -0
- package/dist/adapters/web/LocalStorageAdapter.js +261 -71
- 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 +60 -1
- package/dist/core/Strata.d.ts +30 -0
- package/dist/core/Strata.js +140 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +33 -8
- package/dist/types/index.d.ts +45 -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
|
@@ -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;
|
|
@@ -39,7 +39,7 @@ export class URLAdapter extends BaseAdapter {
|
|
|
39
39
|
typeof window.location !== 'undefined' &&
|
|
40
40
|
typeof window.history !== 'undefined');
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
configure(config) {
|
|
43
43
|
if (config?.mode)
|
|
44
44
|
this.mode = config.mode;
|
|
45
45
|
if (config?.prefix !== undefined)
|
|
@@ -48,6 +48,13 @@ export class URLAdapter extends BaseAdapter {
|
|
|
48
48
|
this.historyMode = config.history;
|
|
49
49
|
if (typeof config?.maxLength === 'number')
|
|
50
50
|
this.maxLength = config.maxLength;
|
|
51
|
+
}
|
|
52
|
+
/** The URL is usable whenever `window.location` is. */
|
|
53
|
+
isAvailableSync() {
|
|
54
|
+
return typeof window !== 'undefined' && typeof window.location !== 'undefined';
|
|
55
|
+
}
|
|
56
|
+
async initialize(config) {
|
|
57
|
+
this.configure(config);
|
|
51
58
|
if (typeof window === 'undefined')
|
|
52
59
|
return;
|
|
53
60
|
// Seed the snapshot and listen for external navigation (back/forward, manual
|
|
@@ -14,6 +14,22 @@ export declare abstract class BaseAdapter implements StorageAdapter {
|
|
|
14
14
|
protected queryEngine: QueryEngine;
|
|
15
15
|
protected ttlCleanupInterval?: ReturnType<typeof setInterval>;
|
|
16
16
|
protected ttlCheckInterval: number;
|
|
17
|
+
/**
|
|
18
|
+
* Apply adapter configuration SYNCHRONOUSLY, before any operation runs.
|
|
19
|
+
*
|
|
20
|
+
* 🔴 Config that changes the PHYSICAL KEY (`prefix`) cannot wait for the async
|
|
21
|
+
* `initialize()`. The synchronous API is deliberately usable before
|
|
22
|
+
* initialization completes — `Strata.selectAdapterSync()` falls back to the
|
|
23
|
+
* registry — so a `prefix` applied only at `initialize()` is silently absent
|
|
24
|
+
* for every `setSync`/`getSync` issued in that window, and the value lands at
|
|
25
|
+
* the bare key. That is the whole of ISSUE-08: `initialize({ prefix })` worked,
|
|
26
|
+
* and the config path to it did not.
|
|
27
|
+
*
|
|
28
|
+
* `initialize()` calls this first, so both paths agree. Default is a no-op;
|
|
29
|
+
* adapters with configurable fields override it with pure assignment only —
|
|
30
|
+
* never I/O, never a timer.
|
|
31
|
+
*/
|
|
32
|
+
configure(_config?: unknown): void;
|
|
17
33
|
/**
|
|
18
34
|
* Initialize TTL cleanup if needed
|
|
19
35
|
*/
|
|
@@ -33,6 +49,22 @@ export declare abstract class BaseAdapter implements StorageAdapter {
|
|
|
33
49
|
* `Strata.cleanupExpired()`.
|
|
34
50
|
*/
|
|
35
51
|
cleanupExpired(): Promise<number>;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a raw stored string into a value THIS adapter owns, or null.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 The classification is the point, not the parse. Web adapters share their
|
|
56
|
+
* storage area with every other script on the origin, so a raw value that is
|
|
57
|
+
* not our envelope means the key belongs to somebody else — the ordinary case,
|
|
58
|
+
* not a failure. Reporting it through `logger.error` asserts something untrue
|
|
59
|
+
* about the consumer's own data, and on any origin running a third-party
|
|
60
|
+
* script that stores a raw string (Microsoft Clarity's `_cltk` is the usual
|
|
61
|
+
* one) it produces a permanent error stream that reaches their error tracker.
|
|
62
|
+
*
|
|
63
|
+
* `logger.error` is therefore reserved for a key carrying OUR envelope that
|
|
64
|
+
* still fails — which is a real defect, and currently cannot be distinguished
|
|
65
|
+
* from somebody else's data at all.
|
|
66
|
+
*/
|
|
67
|
+
protected parseOwnValue<T = unknown>(raw: string | null, key: string): StorageValue<T> | null;
|
|
36
68
|
/**
|
|
37
69
|
* Check if value is expired
|
|
38
70
|
*/
|
package/dist/core/BaseAdapter.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Base adapter implementation with common functionality
|
|
3
3
|
*/
|
|
4
4
|
import { NotSupportedError } from "../utils/errors.js";
|
|
5
|
-
import { EventEmitter, matchGlob, getObjectSize } from "../utils/index.js";
|
|
5
|
+
import { EventEmitter, matchGlob, getObjectSize, deserialize, isStorageEnvelope } from "../utils/index.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
import { QueryEngine } from "../features/query.js";
|
|
8
8
|
/**
|
|
@@ -13,6 +13,24 @@ export class BaseAdapter {
|
|
|
13
13
|
queryEngine = new QueryEngine();
|
|
14
14
|
ttlCleanupInterval;
|
|
15
15
|
ttlCheckInterval = 60000; // Check every minute
|
|
16
|
+
/**
|
|
17
|
+
* Apply adapter configuration SYNCHRONOUSLY, before any operation runs.
|
|
18
|
+
*
|
|
19
|
+
* 🔴 Config that changes the PHYSICAL KEY (`prefix`) cannot wait for the async
|
|
20
|
+
* `initialize()`. The synchronous API is deliberately usable before
|
|
21
|
+
* initialization completes — `Strata.selectAdapterSync()` falls back to the
|
|
22
|
+
* registry — so a `prefix` applied only at `initialize()` is silently absent
|
|
23
|
+
* for every `setSync`/`getSync` issued in that window, and the value lands at
|
|
24
|
+
* the bare key. That is the whole of ISSUE-08: `initialize({ prefix })` worked,
|
|
25
|
+
* and the config path to it did not.
|
|
26
|
+
*
|
|
27
|
+
* `initialize()` calls this first, so both paths agree. Default is a no-op;
|
|
28
|
+
* adapters with configurable fields override it with pure assignment only —
|
|
29
|
+
* never I/O, never a timer.
|
|
30
|
+
*/
|
|
31
|
+
configure(_config) {
|
|
32
|
+
// No configurable fields by default.
|
|
33
|
+
}
|
|
16
34
|
/**
|
|
17
35
|
* Initialize TTL cleanup if needed
|
|
18
36
|
*/
|
|
@@ -27,6 +45,13 @@ export class BaseAdapter {
|
|
|
27
45
|
logger.error(`TTL cleanup error in ${this.name}:`, error);
|
|
28
46
|
}
|
|
29
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?.();
|
|
30
55
|
}
|
|
31
56
|
/**
|
|
32
57
|
* Stop TTL cleanup
|
|
@@ -60,6 +85,40 @@ export class BaseAdapter {
|
|
|
60
85
|
}
|
|
61
86
|
return removed;
|
|
62
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse a raw stored string into a value THIS adapter owns, or null.
|
|
90
|
+
*
|
|
91
|
+
* 🔴 The classification is the point, not the parse. Web adapters share their
|
|
92
|
+
* storage area with every other script on the origin, so a raw value that is
|
|
93
|
+
* not our envelope means the key belongs to somebody else — the ordinary case,
|
|
94
|
+
* not a failure. Reporting it through `logger.error` asserts something untrue
|
|
95
|
+
* about the consumer's own data, and on any origin running a third-party
|
|
96
|
+
* script that stores a raw string (Microsoft Clarity's `_cltk` is the usual
|
|
97
|
+
* one) it produces a permanent error stream that reaches their error tracker.
|
|
98
|
+
*
|
|
99
|
+
* `logger.error` is therefore reserved for a key carrying OUR envelope that
|
|
100
|
+
* still fails — which is a real defect, and currently cannot be distinguished
|
|
101
|
+
* from somebody else's data at all.
|
|
102
|
+
*/
|
|
103
|
+
parseOwnValue(raw, key) {
|
|
104
|
+
if (raw === null || raw === '')
|
|
105
|
+
return null;
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = deserialize(raw);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
logger.debug(`${this.name}: skipping key "${key}" — value is not readable as ${this.name} data ` +
|
|
112
|
+
`(not written by this adapter).`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (!isStorageEnvelope(parsed)) {
|
|
116
|
+
logger.debug(`${this.name}: skipping key "${key}" — value carries no storage envelope ` +
|
|
117
|
+
`(not written by this adapter).`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
63
122
|
/**
|
|
64
123
|
* Check if value is expired
|
|
65
124
|
*/
|
package/dist/core/Strata.d.ts
CHANGED
|
@@ -222,6 +222,12 @@ export declare class Strata {
|
|
|
222
222
|
clearSync(options?: ClearOptions & StorageOptions): void;
|
|
223
223
|
private requireSyncAdapter;
|
|
224
224
|
private selectAdapterSync;
|
|
225
|
+
/**
|
|
226
|
+
* Whether an adapter can serve a synchronous operation right now. An adapter
|
|
227
|
+
* with no `isAvailableSync` is assumed usable — absence of a probe is not
|
|
228
|
+
* evidence of unavailability.
|
|
229
|
+
*/
|
|
230
|
+
private static isUsableSync;
|
|
225
231
|
private syncCapableAdapters;
|
|
226
232
|
private computeExpiration;
|
|
227
233
|
/**
|
|
@@ -314,6 +320,30 @@ export declare class Strata {
|
|
|
314
320
|
* ```
|
|
315
321
|
*/
|
|
316
322
|
registerAdapter(adapter: StorageAdapter): void;
|
|
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
|
+
*/
|
|
344
|
+
private adapterConfigFor;
|
|
345
|
+
/** Push the configured options into an adapter synchronously. */
|
|
346
|
+
private applyAdapterConfig;
|
|
317
347
|
/**
|
|
318
348
|
* Initialize and attach any registered adapters that are not yet active, then
|
|
319
349
|
* re-pick the default adapter. Call this after `registerAdapter()` on an
|