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
|
@@ -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
|
*/
|
|
@@ -60,6 +78,40 @@ export class BaseAdapter {
|
|
|
60
78
|
}
|
|
61
79
|
return removed;
|
|
62
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a raw stored string into a value THIS adapter owns, or null.
|
|
83
|
+
*
|
|
84
|
+
* ๐ด The classification is the point, not the parse. Web adapters share their
|
|
85
|
+
* storage area with every other script on the origin, so a raw value that is
|
|
86
|
+
* not our envelope means the key belongs to somebody else โ the ordinary case,
|
|
87
|
+
* not a failure. Reporting it through `logger.error` asserts something untrue
|
|
88
|
+
* about the consumer's own data, and on any origin running a third-party
|
|
89
|
+
* script that stores a raw string (Microsoft Clarity's `_cltk` is the usual
|
|
90
|
+
* one) it produces a permanent error stream that reaches their error tracker.
|
|
91
|
+
*
|
|
92
|
+
* `logger.error` is therefore reserved for a key carrying OUR envelope that
|
|
93
|
+
* still fails โ which is a real defect, and currently cannot be distinguished
|
|
94
|
+
* from somebody else's data at all.
|
|
95
|
+
*/
|
|
96
|
+
parseOwnValue(raw, key) {
|
|
97
|
+
if (raw === null || raw === '')
|
|
98
|
+
return null;
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = deserialize(raw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
logger.debug(`${this.name}: skipping key "${key}" โ value is not readable as ${this.name} data ` +
|
|
105
|
+
`(not written by this adapter).`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (!isStorageEnvelope(parsed)) {
|
|
109
|
+
logger.debug(`${this.name}: skipping key "${key}" โ value carries no storage envelope ` +
|
|
110
|
+
`(not written by this adapter).`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
63
115
|
/**
|
|
64
116
|
* Check if value is expired
|
|
65
117
|
*/
|
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,10 @@ export declare class Strata {
|
|
|
314
320
|
* ```
|
|
315
321
|
*/
|
|
316
322
|
registerAdapter(adapter: StorageAdapter): void;
|
|
323
|
+
/** The configured options for one adapter, or undefined when it has none. */
|
|
324
|
+
private adapterConfigFor;
|
|
325
|
+
/** Push the configured options into an adapter synchronously. */
|
|
326
|
+
private applyAdapterConfig;
|
|
317
327
|
/**
|
|
318
328
|
* Initialize and attach any registered adapters that are not yet active, then
|
|
319
329
|
* re-pick the default adapter. Call this after `registerAdapter()` on an
|
package/dist/core/Strata.js
CHANGED
|
@@ -729,6 +729,8 @@ export class Strata {
|
|
|
729
729
|
// Synchronous adapter lookup โ falls back to the registry so sync operations
|
|
730
730
|
// work even before async initialize() has completed.
|
|
731
731
|
selectAdapterSync(storage) {
|
|
732
|
+
// An explicitly named storage is honoured as asked โ the caller chose it, and
|
|
733
|
+
// a hard error naming it is more useful than a silent substitution.
|
|
732
734
|
if (storage) {
|
|
733
735
|
const names = Array.isArray(storage) ? storage : [storage];
|
|
734
736
|
for (const name of names) {
|
|
@@ -738,19 +740,51 @@ export class Strata {
|
|
|
738
740
|
}
|
|
739
741
|
throw new StorageError(`No adapter registered for storage type(s): ${names.join(', ')}`);
|
|
740
742
|
}
|
|
741
|
-
if (this.defaultAdapter)
|
|
743
|
+
if (this.defaultAdapter && Strata.isUsableSync(this.defaultAdapter)) {
|
|
742
744
|
return this.defaultAdapter;
|
|
745
|
+
}
|
|
746
|
+
// ๐ด `defaultStorages` reads as an ordered fallback list, so honour it as
|
|
747
|
+
// one HERE too. It previously guarded only the async path: with
|
|
748
|
+
// localStorage unavailable (SSR, private mode, a blocked cookie policy),
|
|
749
|
+
// `defineStorage({ defaultStorages: ['localStorage','memory'] }).setSync()`
|
|
750
|
+
// still selected localStorage and threw SerializationError rather than
|
|
751
|
+
// falling through to memory, which is the whole point of listing memory.
|
|
743
752
|
const preferred = this.config.defaultStorages ?? [];
|
|
744
753
|
for (const name of preferred) {
|
|
745
754
|
const adapter = this.adapters.get(name) ?? this.registry.get(name);
|
|
746
|
-
if (adapter)
|
|
755
|
+
if (adapter && Strata.isUsableSync(adapter))
|
|
756
|
+
return adapter;
|
|
757
|
+
}
|
|
758
|
+
for (const adapter of this.registry.getAll().values()) {
|
|
759
|
+
if (Strata.isUsableSync(adapter))
|
|
747
760
|
return adapter;
|
|
748
761
|
}
|
|
762
|
+
// Nothing usable: fall back to the previous behaviour so the caller gets the
|
|
763
|
+
// adapter's own specific error rather than a vague one from here.
|
|
764
|
+
if (this.defaultAdapter)
|
|
765
|
+
return this.defaultAdapter;
|
|
749
766
|
const first = this.registry.getAll().values().next().value;
|
|
750
767
|
if (first)
|
|
751
768
|
return first;
|
|
752
769
|
throw new StorageError('No storage adapter registered for synchronous operation.');
|
|
753
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Whether an adapter can serve a synchronous operation right now. An adapter
|
|
773
|
+
* with no `isAvailableSync` is assumed usable โ absence of a probe is not
|
|
774
|
+
* evidence of unavailability.
|
|
775
|
+
*/
|
|
776
|
+
static isUsableSync(adapter) {
|
|
777
|
+
if (!adapter.capabilities.synchronous || !adapter.getSync)
|
|
778
|
+
return false;
|
|
779
|
+
if (!adapter.isAvailableSync)
|
|
780
|
+
return true;
|
|
781
|
+
try {
|
|
782
|
+
return adapter.isAvailableSync();
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
754
788
|
// All sync-capable adapters (initialized set, or the registry before init).
|
|
755
789
|
syncCapableAdapters() {
|
|
756
790
|
const source = this.adapters.size > 0 ? this.adapters.values() : this.registry.getAll().values();
|
|
@@ -863,13 +897,43 @@ export class Strata {
|
|
|
863
897
|
const attach = () => {
|
|
864
898
|
if (cancelled)
|
|
865
899
|
return;
|
|
866
|
-
const
|
|
900
|
+
const explicit = options?.storage !== undefined;
|
|
901
|
+
const targets = explicit
|
|
867
902
|
? [this.adapters.get(options.storage)]
|
|
868
903
|
: Array.from(this.adapters.values());
|
|
869
904
|
for (const adapter of targets) {
|
|
870
|
-
if (adapter?.subscribe)
|
|
905
|
+
if (!adapter?.subscribe)
|
|
906
|
+
continue;
|
|
907
|
+
// ๐ด Skip backends that cannot observe rather than letting the first one
|
|
908
|
+
// abort the fan-out. `BaseAdapter.subscribe` throws NotSupportedError
|
|
909
|
+
// when `capabilities.observable` is false โ true for indexedDB, cookies
|
|
910
|
+
// and cache, ALL of which the default registration includes โ so the
|
|
911
|
+
// documented options-less "hear every adapter" form threw on every
|
|
912
|
+
// default instance and took application boot down with it.
|
|
913
|
+
//
|
|
914
|
+
// An observer that hears fewer backends is the correct outcome of "hear
|
|
915
|
+
// every adapter" when some cannot speak. When the caller named ONE
|
|
916
|
+
// backend explicitly they get told, because silence is not what they
|
|
917
|
+
// asked for.
|
|
918
|
+
if (!adapter.capabilities.observable) {
|
|
919
|
+
if (explicit) {
|
|
920
|
+
logger.warn(`subscribe: storage "${adapter.name}" does not support change events, so this ` +
|
|
921
|
+
`subscription will never fire. Target an observable backend ` +
|
|
922
|
+
`(memory, localStorage, sessionStorage, url).`);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
logger.debug(`subscribe: skipping non-observable adapter "${adapter.name}".`);
|
|
926
|
+
}
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
// A custom adapter may still refuse despite advertising the capability;
|
|
930
|
+
// one that does must not cost the caller every other subscription.
|
|
931
|
+
try {
|
|
871
932
|
unsubscribers.push(adapter.subscribe(effectiveCallback));
|
|
872
933
|
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
logger.debug(`subscribe: adapter "${adapter.name}" refused to attach:`, error);
|
|
936
|
+
}
|
|
873
937
|
}
|
|
874
938
|
};
|
|
875
939
|
// Attach now if ready; otherwise once initialization completes โ so a
|
|
@@ -1231,6 +1295,31 @@ export class Strata {
|
|
|
1231
1295
|
*/
|
|
1232
1296
|
registerAdapter(adapter) {
|
|
1233
1297
|
this.registry.register(adapter);
|
|
1298
|
+
// ๐ด Apply the adapter's config NOW, not at initialize(). The synchronous
|
|
1299
|
+
// API is usable before initialization completes (selectAdapterSync falls
|
|
1300
|
+
// back to the registry), so a `prefix` applied only in the async
|
|
1301
|
+
// initialize() is silently absent for every setSync/getSync issued in that
|
|
1302
|
+
// window and the value lands at the bare key. That was ISSUE-08: the
|
|
1303
|
+
// adapter honoured `initialize({ prefix })` all along, and the config path
|
|
1304
|
+
// to it did not reach the sync path.
|
|
1305
|
+
this.applyAdapterConfig(adapter);
|
|
1306
|
+
}
|
|
1307
|
+
/** The configured options for one adapter, or undefined when it has none. */
|
|
1308
|
+
adapterConfigFor(name) {
|
|
1309
|
+
const raw = this.config.adapters?.[name];
|
|
1310
|
+
return typeof raw === 'object' && raw !== null ? raw : undefined;
|
|
1311
|
+
}
|
|
1312
|
+
/** Push the configured options into an adapter synchronously. */
|
|
1313
|
+
applyAdapterConfig(adapter) {
|
|
1314
|
+
const config = this.adapterConfigFor(adapter.name);
|
|
1315
|
+
if (!config || !adapter.configure)
|
|
1316
|
+
return;
|
|
1317
|
+
try {
|
|
1318
|
+
adapter.configure(config);
|
|
1319
|
+
}
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
logger.warn(`Failed to configure ${adapter.name} adapter:`, error);
|
|
1322
|
+
}
|
|
1234
1323
|
}
|
|
1235
1324
|
/**
|
|
1236
1325
|
* Initialize and attach any registered adapters that are not yet active, then
|
|
@@ -1397,8 +1486,7 @@ export class Strata {
|
|
|
1397
1486
|
try {
|
|
1398
1487
|
if (!(await adapter.isAvailable()))
|
|
1399
1488
|
continue;
|
|
1400
|
-
|
|
1401
|
-
await adapter.initialize(adapterConfig);
|
|
1489
|
+
await adapter.initialize(this.adapterConfigFor(name));
|
|
1402
1490
|
this.adapters.set(name, adapter);
|
|
1403
1491
|
}
|
|
1404
1492
|
catch (error) {
|
|
@@ -1609,7 +1697,9 @@ export class Strata {
|
|
|
1609
1697
|
for (const s of storages) {
|
|
1610
1698
|
const adapter = this.registry.get(s);
|
|
1611
1699
|
if (adapter && (await adapter.isAvailable())) {
|
|
1612
|
-
|
|
1700
|
+
// Pass the adapter's configured options โ initializing bare here dropped
|
|
1701
|
+
// the prefix for any adapter first reached through this path.
|
|
1702
|
+
await adapter.initialize(this.adapterConfigFor(s));
|
|
1613
1703
|
this.adapters.set(s, adapter);
|
|
1614
1704
|
return adapter;
|
|
1615
1705
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export { MigrationManager } from "./features/migration.js";
|
|
|
19
19
|
export type { Migration } from "./features/migration.js";
|
|
20
20
|
export { StrataError, StorageError, IntegrityError, QuotaExceededError, EncryptionError, CompressionError, SerializationError, ValidationError, NotSupportedError, AdapterNotAvailableError, } from "./utils/errors.js";
|
|
21
21
|
export type { StorageType, StorageOptions, StorageValue, StorageAdapter, AdapterConfig, QueryOptions, SyncConfig, EncryptionConfig, CompressionConfig, ObserverCallback, StorageEvent, StorageCapabilities, StorageMetadata, TTLConfig, StrataConfig, StorageChange, SubscriptionCallback, UnsubscribeFunction, } from "./types/index.js";
|
|
22
|
-
export {
|
|
22
|
+
export { setLogLevel, getLogLevel, type LogLevel } from "./utils/logger.js";
|
|
23
|
+
export { isValidKey, isValidValue, isStorageEnvelope, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
23
24
|
import { Strata } from "./core/Strata.js";
|
|
24
25
|
import type { StrataConfig } from "./types/index.js";
|
|
25
26
|
/**
|
|
@@ -28,7 +29,7 @@ import type { StrataConfig } from "./types/index.js";
|
|
|
28
29
|
* custom instance can opt into the same default set. Returns the same instance
|
|
29
30
|
* for chaining.
|
|
30
31
|
*/
|
|
31
|
-
export declare function registerWebAdapters(strata: Strata): Strata;
|
|
32
|
+
export declare function registerWebAdapters(strata: Strata, config?: StrataConfig): Strata;
|
|
32
33
|
/**
|
|
33
34
|
* Create a ready-to-use Strata instance with the standard web adapters
|
|
34
35
|
* pre-registered โ the framework-agnostic, Zustand-style entry point. Create it
|
package/dist/index.js
CHANGED
|
@@ -23,8 +23,17 @@ export { computeChecksum, verifyChecksum } from "./features/integrity.js";
|
|
|
23
23
|
export { MigrationManager } from "./features/migration.js";
|
|
24
24
|
// Error classes (exported as values so consumers can use `instanceof`)
|
|
25
25
|
export { StrataError, StorageError, IntegrityError, QuotaExceededError, EncryptionError, CompressionError, SerializationError, ValidationError, NotSupportedError, AdapterNotAvailableError, } from "./utils/errors.js";
|
|
26
|
+
// Diagnostics.
|
|
27
|
+
//
|
|
28
|
+
// ๐ด The logger's own documentation names `setLogLevel('debug')` as the way to
|
|
29
|
+
// raise verbosity, and it was never exported from this entry point โ so the
|
|
30
|
+
// documented control was unreachable. That matters as of 2.9.0: keys this
|
|
31
|
+
// library does not own are now SKIPPED and reported at `debug`, which is below
|
|
32
|
+
// the default `warn`. Raising the level is how a consumer answers "why is my
|
|
33
|
+
// key missing from keys()?", so the control has to be reachable.
|
|
34
|
+
export { setLogLevel, getLogLevel } from "./utils/logger.js";
|
|
26
35
|
// Utils
|
|
27
|
-
export { isValidKey, isValidValue, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
36
|
+
export { isValidKey, isValidValue, isStorageEnvelope, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
28
37
|
// Create and export a default storage instance that works immediately
|
|
29
38
|
import { Strata } from "./core/Strata.js";
|
|
30
39
|
import { LocalStorageAdapter } from "./adapters/web/LocalStorageAdapter.js";
|
|
@@ -40,14 +49,29 @@ import { logger } from "./utils/logger.js";
|
|
|
40
49
|
* custom instance can opt into the same default set. Returns the same instance
|
|
41
50
|
* for chaining.
|
|
42
51
|
*/
|
|
43
|
-
export function registerWebAdapters(strata) {
|
|
52
|
+
export function registerWebAdapters(strata, config) {
|
|
53
|
+
// `adapters: { <name>: false }` opts an adapter out of REGISTRATION, not just
|
|
54
|
+
// out of initialization. Registering one the instance will never use still
|
|
55
|
+
// costs a TTL sweep over a storage area it does not own.
|
|
56
|
+
//
|
|
57
|
+
// ๐ด `defaultStorages` is NOT this switch. It is the preference order for
|
|
58
|
+
// picking the DEFAULT adapter; multi-adapter operations (keys/clear/size/
|
|
59
|
+
// subscribe with no `storage`) deliberately span everything registered. That
|
|
60
|
+
// distinction was undocumented, and reading `defaultStorages` as a
|
|
61
|
+
// registration allow-list is what produced ISSUE-09.
|
|
62
|
+
const enabled = (name) => config?.adapters?.[name] !== false;
|
|
44
63
|
try {
|
|
45
64
|
strata.registerAdapter(new MemoryAdapter()); // always-available fallback
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
if (enabled('localStorage'))
|
|
66
|
+
strata.registerAdapter(new LocalStorageAdapter());
|
|
67
|
+
if (enabled('sessionStorage'))
|
|
68
|
+
strata.registerAdapter(new SessionStorageAdapter());
|
|
69
|
+
if (enabled('indexedDB'))
|
|
70
|
+
strata.registerAdapter(new IndexedDBAdapter());
|
|
71
|
+
if (enabled('cookies'))
|
|
72
|
+
strata.registerAdapter(new CookieAdapter());
|
|
73
|
+
if (enabled('cache'))
|
|
74
|
+
strata.registerAdapter(new CacheAdapter());
|
|
51
75
|
}
|
|
52
76
|
catch (error) {
|
|
53
77
|
logger.warn('Strata Storage adapter registration warning:', error);
|
|
@@ -71,7 +95,7 @@ export function registerWebAdapters(strata) {
|
|
|
71
95
|
* ```
|
|
72
96
|
*/
|
|
73
97
|
export function defineStorage(config) {
|
|
74
|
-
return registerWebAdapters(new Strata(config));
|
|
98
|
+
return registerWebAdapters(new Strata(config), config);
|
|
75
99
|
}
|
|
76
100
|
// Default singleton โ created via the same factory so behavior is identical.
|
|
77
101
|
// It initializes lazily on first use, so importing the package has no I/O cost.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -518,6 +518,19 @@ export interface StorageAdapter {
|
|
|
518
518
|
* Get all keys
|
|
519
519
|
*/
|
|
520
520
|
keys(pattern?: string | RegExp): Promise<string[]>;
|
|
521
|
+
/**
|
|
522
|
+
* Apply configuration synchronously, before any operation runs. Optional โ
|
|
523
|
+
* an adapter with no configurable fields omits it. Implementations do pure
|
|
524
|
+
* assignment only: config that changes the physical key cannot wait for the
|
|
525
|
+
* async `initialize()`, because the synchronous API is usable before it.
|
|
526
|
+
*/
|
|
527
|
+
configure?(config?: unknown): void;
|
|
528
|
+
/**
|
|
529
|
+
* Whether this backend can serve a SYNCHRONOUS operation right now, without
|
|
530
|
+
* awaiting anything. Optional; absence means "assume usable". Lets the sync
|
|
531
|
+
* path honour `defaultStorages` as the ordered fallback list it reads as.
|
|
532
|
+
*/
|
|
533
|
+
isAvailableSync?(): boolean;
|
|
521
534
|
/** Synchronous get โ sync-capable adapters only. */
|
|
522
535
|
getSync?<T = unknown>(key: string): StorageValue<T> | null;
|
|
523
536
|
/** Synchronous set โ sync-capable adapters only. */
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Utility functions for Strata Storage
|
|
3
3
|
* Zero dependencies - all utilities implemented from scratch
|
|
4
4
|
*/
|
|
5
|
+
import type { StorageValue } from "../types/index.js";
|
|
5
6
|
/**
|
|
6
7
|
* Check if code is running in a browser environment
|
|
7
8
|
*/
|
|
@@ -134,6 +135,24 @@ export declare function serialize(value: unknown): string;
|
|
|
134
135
|
* pathological patterns (see SAFE_REGEX_MAX_LENGTH).
|
|
135
136
|
*/
|
|
136
137
|
export declare function deserialize(json: string): unknown;
|
|
138
|
+
/**
|
|
139
|
+
* Whether a deserialized value is a StorageValue envelope this library wrote.
|
|
140
|
+
*
|
|
141
|
+
* ๐ด This is the definition of "a key this adapter owns", and it is the ONLY
|
|
142
|
+
* thing that can answer that question when the key prefix is empty. Web
|
|
143
|
+
* adapters share their storage area with every other script on the origin, and
|
|
144
|
+
* an empty prefix makes `startsWith(prefix)` true for every key there โ so name
|
|
145
|
+
* alone cannot distinguish our data from a third-party script's. Shape can.
|
|
146
|
+
*
|
|
147
|
+
* A value failing this check is not an error: it is evidence the key belongs to
|
|
148
|
+
* somebody else, which is the ordinary case in a shared area. Callers skip it
|
|
149
|
+
* silently rather than reporting a failure about data they do not own.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately strict โ `created` and `updated` are written by every write path
|
|
152
|
+
* (`Strata.set`, `setSync`, import, restore) and are finite numbers, so a
|
|
153
|
+
* foreign object that merely happens to carry a `value` key does not qualify.
|
|
154
|
+
*/
|
|
155
|
+
export declare function isStorageEnvelope(value: unknown): value is StorageValue;
|
|
137
156
|
/**
|
|
138
157
|
* Calculate object size in bytes (rough estimate)
|
|
139
158
|
*/
|
package/dist/utils/index.js
CHANGED
|
@@ -334,6 +334,35 @@ export function deserialize(json) {
|
|
|
334
334
|
return val;
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Whether a deserialized value is a StorageValue envelope this library wrote.
|
|
339
|
+
*
|
|
340
|
+
* ๐ด This is the definition of "a key this adapter owns", and it is the ONLY
|
|
341
|
+
* thing that can answer that question when the key prefix is empty. Web
|
|
342
|
+
* adapters share their storage area with every other script on the origin, and
|
|
343
|
+
* an empty prefix makes `startsWith(prefix)` true for every key there โ so name
|
|
344
|
+
* alone cannot distinguish our data from a third-party script's. Shape can.
|
|
345
|
+
*
|
|
346
|
+
* A value failing this check is not an error: it is evidence the key belongs to
|
|
347
|
+
* somebody else, which is the ordinary case in a shared area. Callers skip it
|
|
348
|
+
* silently rather than reporting a failure about data they do not own.
|
|
349
|
+
*
|
|
350
|
+
* Deliberately strict โ `created` and `updated` are written by every write path
|
|
351
|
+
* (`Strata.set`, `setSync`, import, restore) and are finite numbers, so a
|
|
352
|
+
* foreign object that merely happens to carry a `value` key does not qualify.
|
|
353
|
+
*/
|
|
354
|
+
export function isStorageEnvelope(value) {
|
|
355
|
+
if (!isObject(value))
|
|
356
|
+
return false;
|
|
357
|
+
if (!('value' in value))
|
|
358
|
+
return false;
|
|
359
|
+
if (!Number.isFinite(value.created) || !Number.isFinite(value.updated))
|
|
360
|
+
return false;
|
|
361
|
+
if ('expires' in value && value.expires !== undefined && !Number.isFinite(value.expires)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
337
366
|
/**
|
|
338
367
|
* Calculate object size in bytes (rough estimate)
|
|
339
368
|
*/
|
package/package.json
CHANGED
package/scripts/build.js
CHANGED
|
@@ -153,5 +153,98 @@ fs.writeFileSync(
|
|
|
153
153
|
JSON.stringify({ type: 'module', sideEffects: false }, null, 2) + '\n'
|
|
154
154
|
);
|
|
155
155
|
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Build gates
|
|
158
|
+
//
|
|
159
|
+
// This project deliberately ships no test framework โ typecheck, lint and build
|
|
160
|
+
// are the gates (README โธ Limitations). These two checks live inside the build
|
|
161
|
+
// for that reason: they add no dependency and no runner, and they cover exactly
|
|
162
|
+
// the two things the other gates are blind to.
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
const fail = (message) => {
|
|
166
|
+
console.error(`\nโ ${message}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// GATE 1 โ the storage-ownership predicate.
|
|
171
|
+
//
|
|
172
|
+
// `isStorageEnvelope` decides whether a key in a SHARED storage area belongs to
|
|
173
|
+
// this library. Get it wrong in the permissive direction and we are back to
|
|
174
|
+
// reading, error-logging about, and deleting other applications' data (ISSUE-01,
|
|
175
|
+
// ISSUE-09); wrong in the strict direction and we stop recognising our own.
|
|
176
|
+
// Neither failure is visible to typecheck, lint or a green build.
|
|
177
|
+
console.log('๐ Gate: storage-ownership predicate...');
|
|
178
|
+
const { isStorageEnvelope } = await import(
|
|
179
|
+
new URL('../dist/utils/index.js', import.meta.url).href
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const envelope = (extra = {}) => ({ value: 'v', created: 1, updated: 2, ...extra });
|
|
183
|
+
const OWNERSHIP_CASES = [
|
|
184
|
+
// Foreign values seen in the wild โ every one of these reached logger.error
|
|
185
|
+
// before 2.9.0. `ts3hsf` is Microsoft Clarity's `_cltk`; `warn` is a consumer
|
|
186
|
+
// logger's own level key.
|
|
187
|
+
['clarity _cltk raw string', 'ts3hsf', false],
|
|
188
|
+
['logger-level raw string', 'warn', false],
|
|
189
|
+
['foreign JSON object', { a: 1 }, false],
|
|
190
|
+
['foreign JSON array', [1, 2, 3], false],
|
|
191
|
+
['foreign object carrying a value key', { value: 'v' }, false],
|
|
192
|
+
['null', null, false],
|
|
193
|
+
['undefined', undefined, false],
|
|
194
|
+
['number', 42, false],
|
|
195
|
+
['string', 'plain', false],
|
|
196
|
+
// Near-misses: shaped like ours but not written by us.
|
|
197
|
+
['envelope missing created', { value: 'v', updated: 2 }, false],
|
|
198
|
+
['envelope with non-numeric updated', { value: 'v', created: 1, updated: 'x' }, false],
|
|
199
|
+
['envelope with NaN created', { value: 'v', created: NaN, updated: 2 }, false],
|
|
200
|
+
['envelope with non-numeric expires', envelope({ expires: 'soon' }), false],
|
|
201
|
+
// Ours, in every shape a write path produces.
|
|
202
|
+
['plain envelope', envelope(), true],
|
|
203
|
+
['envelope holding null', envelope({ value: null }), true],
|
|
204
|
+
['envelope with expires', envelope({ expires: Date.now() }), true],
|
|
205
|
+
['envelope with tags + metadata', envelope({ tags: ['a'], metadata: { b: 1 } }), true],
|
|
206
|
+
['envelope with created 0', { value: 'v', created: 0, updated: 0 }, true],
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const ownershipFailures = OWNERSHIP_CASES.filter(
|
|
210
|
+
([, input, expected]) => isStorageEnvelope(input) !== expected
|
|
211
|
+
).map(([name, , expected]) => ` ยท ${name}: expected ${expected}, got ${!expected}`);
|
|
212
|
+
|
|
213
|
+
if (ownershipFailures.length > 0) {
|
|
214
|
+
fail(
|
|
215
|
+
`Storage-ownership predicate is wrong in ${ownershipFailures.length} case(s):\n` +
|
|
216
|
+
ownershipFailures.join('\n')
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
console.log(` โ ${OWNERSHIP_CASES.length} ownership cases`);
|
|
220
|
+
|
|
221
|
+
// GATE 2 โ the README's version row cannot drift from package.json.
|
|
222
|
+
//
|
|
223
|
+
// The row is a hand-maintained duplicate of `version`, so it goes stale the
|
|
224
|
+
// moment the version is bumped โ it has already shipped stale once (2.8.5), and
|
|
225
|
+
// three in-repo files disagreeing about the current version is ISSUE-06. This
|
|
226
|
+
// asserts on the ARTEFACT that npm actually renders, not on the release process
|
|
227
|
+
// that is supposed to update it.
|
|
228
|
+
console.log('๐ Gate: README version row matches package.json...');
|
|
229
|
+
const pkgVersion = JSON.parse(
|
|
230
|
+
fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')
|
|
231
|
+
).version;
|
|
232
|
+
const readme = fs.readFileSync(path.join(rootDir, 'README.md'), 'utf8');
|
|
233
|
+
const versionRow = readme.match(/^\|\s*\*\*Version\*\*\s*\|\s*`([^`]+)`/m);
|
|
234
|
+
|
|
235
|
+
if (!versionRow) {
|
|
236
|
+
fail(
|
|
237
|
+
'README has no at-a-glance `| **Version** | `x.y.z` |` row, so nothing pins the ' +
|
|
238
|
+
'published version claim. Restore the row rather than removing this gate.'
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (versionRow[1] !== pkgVersion) {
|
|
242
|
+
fail(
|
|
243
|
+
`README version row says \`${versionRow[1]}\` but package.json says \`${pkgVersion}\`. ` +
|
|
244
|
+
'Update the README row โ npm renders it verbatim on the package page.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
console.log(` โ README version row is ${pkgVersion}`);
|
|
248
|
+
|
|
156
249
|
console.log('โ
Build completed successfully!');
|
|
157
250
|
console.log(`๐ Output: ${distDir}`);
|