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
package/dist/core/Strata.js
CHANGED
|
@@ -116,6 +116,9 @@ export class Strata {
|
|
|
116
116
|
this._ttlCleanupTimer = setInterval(() => {
|
|
117
117
|
void this.cleanupAllAdapters();
|
|
118
118
|
}, interval);
|
|
119
|
+
// See BaseAdapter.startTTLCleanup — a live interval must not keep a Node
|
|
120
|
+
// process alive. No-op in browsers.
|
|
121
|
+
this._ttlCleanupTimer.unref?.();
|
|
119
122
|
}
|
|
120
123
|
// Start periodic auto-backup if configured
|
|
121
124
|
if (this.config.autoBackup?.interval) {
|
|
@@ -729,6 +732,8 @@ export class Strata {
|
|
|
729
732
|
// Synchronous adapter lookup — falls back to the registry so sync operations
|
|
730
733
|
// work even before async initialize() has completed.
|
|
731
734
|
selectAdapterSync(storage) {
|
|
735
|
+
// An explicitly named storage is honoured as asked — the caller chose it, and
|
|
736
|
+
// a hard error naming it is more useful than a silent substitution.
|
|
732
737
|
if (storage) {
|
|
733
738
|
const names = Array.isArray(storage) ? storage : [storage];
|
|
734
739
|
for (const name of names) {
|
|
@@ -738,19 +743,51 @@ export class Strata {
|
|
|
738
743
|
}
|
|
739
744
|
throw new StorageError(`No adapter registered for storage type(s): ${names.join(', ')}`);
|
|
740
745
|
}
|
|
741
|
-
if (this.defaultAdapter)
|
|
746
|
+
if (this.defaultAdapter && Strata.isUsableSync(this.defaultAdapter)) {
|
|
742
747
|
return this.defaultAdapter;
|
|
748
|
+
}
|
|
749
|
+
// 🔴 `defaultStorages` reads as an ordered fallback list, so honour it as
|
|
750
|
+
// one HERE too. It previously guarded only the async path: with
|
|
751
|
+
// localStorage unavailable (SSR, private mode, a blocked cookie policy),
|
|
752
|
+
// `defineStorage({ defaultStorages: ['localStorage','memory'] }).setSync()`
|
|
753
|
+
// still selected localStorage and threw SerializationError rather than
|
|
754
|
+
// falling through to memory, which is the whole point of listing memory.
|
|
743
755
|
const preferred = this.config.defaultStorages ?? [];
|
|
744
756
|
for (const name of preferred) {
|
|
745
757
|
const adapter = this.adapters.get(name) ?? this.registry.get(name);
|
|
746
|
-
if (adapter)
|
|
758
|
+
if (adapter && Strata.isUsableSync(adapter))
|
|
759
|
+
return adapter;
|
|
760
|
+
}
|
|
761
|
+
for (const adapter of this.registry.getAll().values()) {
|
|
762
|
+
if (Strata.isUsableSync(adapter))
|
|
747
763
|
return adapter;
|
|
748
764
|
}
|
|
765
|
+
// Nothing usable: fall back to the previous behaviour so the caller gets the
|
|
766
|
+
// adapter's own specific error rather than a vague one from here.
|
|
767
|
+
if (this.defaultAdapter)
|
|
768
|
+
return this.defaultAdapter;
|
|
749
769
|
const first = this.registry.getAll().values().next().value;
|
|
750
770
|
if (first)
|
|
751
771
|
return first;
|
|
752
772
|
throw new StorageError('No storage adapter registered for synchronous operation.');
|
|
753
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Whether an adapter can serve a synchronous operation right now. An adapter
|
|
776
|
+
* with no `isAvailableSync` is assumed usable — absence of a probe is not
|
|
777
|
+
* evidence of unavailability.
|
|
778
|
+
*/
|
|
779
|
+
static isUsableSync(adapter) {
|
|
780
|
+
if (!adapter.capabilities.synchronous || !adapter.getSync)
|
|
781
|
+
return false;
|
|
782
|
+
if (!adapter.isAvailableSync)
|
|
783
|
+
return true;
|
|
784
|
+
try {
|
|
785
|
+
return adapter.isAvailableSync();
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
754
791
|
// All sync-capable adapters (initialized set, or the registry before init).
|
|
755
792
|
syncCapableAdapters() {
|
|
756
793
|
const source = this.adapters.size > 0 ? this.adapters.values() : this.registry.getAll().values();
|
|
@@ -863,13 +900,43 @@ export class Strata {
|
|
|
863
900
|
const attach = () => {
|
|
864
901
|
if (cancelled)
|
|
865
902
|
return;
|
|
866
|
-
const
|
|
903
|
+
const explicit = options?.storage !== undefined;
|
|
904
|
+
const targets = explicit
|
|
867
905
|
? [this.adapters.get(options.storage)]
|
|
868
906
|
: Array.from(this.adapters.values());
|
|
869
907
|
for (const adapter of targets) {
|
|
870
|
-
if (adapter?.subscribe)
|
|
908
|
+
if (!adapter?.subscribe)
|
|
909
|
+
continue;
|
|
910
|
+
// 🔴 Skip backends that cannot observe rather than letting the first one
|
|
911
|
+
// abort the fan-out. `BaseAdapter.subscribe` throws NotSupportedError
|
|
912
|
+
// when `capabilities.observable` is false — true for indexedDB, cookies
|
|
913
|
+
// and cache, ALL of which the default registration includes — so the
|
|
914
|
+
// documented options-less "hear every adapter" form threw on every
|
|
915
|
+
// default instance and took application boot down with it.
|
|
916
|
+
//
|
|
917
|
+
// An observer that hears fewer backends is the correct outcome of "hear
|
|
918
|
+
// every adapter" when some cannot speak. When the caller named ONE
|
|
919
|
+
// backend explicitly they get told, because silence is not what they
|
|
920
|
+
// asked for.
|
|
921
|
+
if (!adapter.capabilities.observable) {
|
|
922
|
+
if (explicit) {
|
|
923
|
+
logger.warn(`subscribe: storage "${adapter.name}" does not support change events, so this ` +
|
|
924
|
+
`subscription will never fire. Target an observable backend ` +
|
|
925
|
+
`(memory, localStorage, sessionStorage, url).`);
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
logger.debug(`subscribe: skipping non-observable adapter "${adapter.name}".`);
|
|
929
|
+
}
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
// A custom adapter may still refuse despite advertising the capability;
|
|
933
|
+
// one that does must not cost the caller every other subscription.
|
|
934
|
+
try {
|
|
871
935
|
unsubscribers.push(adapter.subscribe(effectiveCallback));
|
|
872
936
|
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
logger.debug(`subscribe: adapter "${adapter.name}" refused to attach:`, error);
|
|
939
|
+
}
|
|
873
940
|
}
|
|
874
941
|
};
|
|
875
942
|
// Attach now if ready; otherwise once initialization completes — so a
|
|
@@ -1231,6 +1298,70 @@ export class Strata {
|
|
|
1231
1298
|
*/
|
|
1232
1299
|
registerAdapter(adapter) {
|
|
1233
1300
|
this.registry.register(adapter);
|
|
1301
|
+
// 🔴 Apply the adapter's config NOW, not at initialize(). The synchronous
|
|
1302
|
+
// API is usable before initialization completes (selectAdapterSync falls
|
|
1303
|
+
// back to the registry), so a `prefix` applied only in the async
|
|
1304
|
+
// initialize() is silently absent for every setSync/getSync issued in that
|
|
1305
|
+
// window and the value lands at the bare key. That was ISSUE-08: the
|
|
1306
|
+
// adapter honoured `initialize({ prefix })` all along, and the config path
|
|
1307
|
+
// to it did not reach the sync path.
|
|
1308
|
+
this.applyAdapterConfig(adapter);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Web adapters that share their storage area with every other script on the
|
|
1312
|
+
* origin, and therefore take the instance-wide `keyPrefix`.
|
|
1313
|
+
*
|
|
1314
|
+
* Cookies are excluded deliberately: they already default to `strata_`, so
|
|
1315
|
+
* moving them would break existing cookies for no gain. IndexedDB and the Cache
|
|
1316
|
+
* API own a named store, memory owns its own Map, and the URL adapter already
|
|
1317
|
+
* prefixes its params — none of them can collide with another script's keys.
|
|
1318
|
+
*/
|
|
1319
|
+
static PREFIXED_WEB_ADAPTERS = new Set([
|
|
1320
|
+
'localStorage',
|
|
1321
|
+
'sessionStorage',
|
|
1322
|
+
]);
|
|
1323
|
+
/**
|
|
1324
|
+
* The configured options for one adapter, or undefined when it has none.
|
|
1325
|
+
*
|
|
1326
|
+
* For the shared-area web adapters this also resolves the 3.0.0 key prefix and
|
|
1327
|
+
* decides whether that adapter may adopt pre-3.0 unprefixed entries. Precedence,
|
|
1328
|
+
* and it lives only here:
|
|
1329
|
+
*
|
|
1330
|
+
* 1. `adapters.<name>.prefix` — the most specific thing the caller wrote
|
|
1331
|
+
* 2. `keyPrefix` — the instance-wide switch (`false` restores 2.x behaviour)
|
|
1332
|
+
* 3. the adapter's own constructor default (`DEFAULT_WEB_KEY_PREFIX`)
|
|
1333
|
+
*/
|
|
1334
|
+
adapterConfigFor(name) {
|
|
1335
|
+
const raw = this.config.adapters?.[name];
|
|
1336
|
+
const explicit = typeof raw === 'object' && raw !== null ? { ...raw } : undefined;
|
|
1337
|
+
if (!Strata.PREFIXED_WEB_ADAPTERS.has(name))
|
|
1338
|
+
return explicit;
|
|
1339
|
+
const resolved = explicit ?? {};
|
|
1340
|
+
// Only fill in the prefix when the caller did not name one for this adapter.
|
|
1341
|
+
if (resolved.prefix === undefined && this.config.keyPrefix !== undefined) {
|
|
1342
|
+
resolved.prefix = this.config.keyPrefix === false ? '' : this.config.keyPrefix;
|
|
1343
|
+
}
|
|
1344
|
+
// 🔴 Migration is enabled HERE and nowhere else — only for an adapter whose
|
|
1345
|
+
// prefix this instance resolved. A directly constructed adapter must never
|
|
1346
|
+
// adopt bare keys: `plugin/web.ts` builds a `strata_prefs_` instance beside
|
|
1347
|
+
// the main one, and if that adopted every unprefixed entry it found it would
|
|
1348
|
+
// take them from the instance they belong to.
|
|
1349
|
+
if (resolved.migrateLegacyKeys === undefined) {
|
|
1350
|
+
resolved.migrateLegacyKeys = this.config.migrateLegacyKeys !== false;
|
|
1351
|
+
}
|
|
1352
|
+
return resolved;
|
|
1353
|
+
}
|
|
1354
|
+
/** Push the configured options into an adapter synchronously. */
|
|
1355
|
+
applyAdapterConfig(adapter) {
|
|
1356
|
+
const config = this.adapterConfigFor(adapter.name);
|
|
1357
|
+
if (!config || !adapter.configure)
|
|
1358
|
+
return;
|
|
1359
|
+
try {
|
|
1360
|
+
adapter.configure(config);
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
logger.warn(`Failed to configure ${adapter.name} adapter:`, error);
|
|
1364
|
+
}
|
|
1234
1365
|
}
|
|
1235
1366
|
/**
|
|
1236
1367
|
* Initialize and attach any registered adapters that are not yet active, then
|
|
@@ -1397,8 +1528,7 @@ export class Strata {
|
|
|
1397
1528
|
try {
|
|
1398
1529
|
if (!(await adapter.isAvailable()))
|
|
1399
1530
|
continue;
|
|
1400
|
-
|
|
1401
|
-
await adapter.initialize(adapterConfig);
|
|
1531
|
+
await adapter.initialize(this.adapterConfigFor(name));
|
|
1402
1532
|
this.adapters.set(name, adapter);
|
|
1403
1533
|
}
|
|
1404
1534
|
catch (error) {
|
|
@@ -1590,6 +1720,7 @@ export class Strata {
|
|
|
1590
1720
|
}
|
|
1591
1721
|
})();
|
|
1592
1722
|
}, cfg.interval);
|
|
1723
|
+
this._autoBackupTimer.unref?.();
|
|
1593
1724
|
}
|
|
1594
1725
|
async selectAdapter(storage) {
|
|
1595
1726
|
await this.ensureReady();
|
|
@@ -1609,7 +1740,9 @@ export class Strata {
|
|
|
1609
1740
|
for (const s of storages) {
|
|
1610
1741
|
const adapter = this.registry.get(s);
|
|
1611
1742
|
if (adapter && (await adapter.isAvailable())) {
|
|
1612
|
-
|
|
1743
|
+
// Pass the adapter's configured options — initializing bare here dropped
|
|
1744
|
+
// the prefix for any adapter first reached through this path.
|
|
1745
|
+
await adapter.initialize(this.adapterConfigFor(s));
|
|
1613
1746
|
this.adapters.set(s, adapter);
|
|
1614
1747
|
return adapter;
|
|
1615
1748
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { CookieAdapter } from "./adapters/web/CookieAdapter.js";
|
|
|
8
8
|
export { CacheAdapter } from "./adapters/web/CacheAdapter.js";
|
|
9
9
|
export { MemoryAdapter } from "./adapters/web/MemoryAdapter.js";
|
|
10
10
|
export { URLAdapter, type URLAdapterConfig } from "./adapters/web/URLAdapter.js";
|
|
11
|
+
export { DEFAULT_WEB_KEY_PREFIX } from "./adapters/web/LocalStorageAdapter.js";
|
|
11
12
|
export { EncryptionManager } from "./features/encryption.js";
|
|
12
13
|
export { CompressionManager } from "./features/compression.js";
|
|
13
14
|
export { TTLManager } from "./features/ttl.js";
|
|
@@ -19,7 +20,8 @@ export { MigrationManager } from "./features/migration.js";
|
|
|
19
20
|
export type { Migration } from "./features/migration.js";
|
|
20
21
|
export { StrataError, StorageError, IntegrityError, QuotaExceededError, EncryptionError, CompressionError, SerializationError, ValidationError, NotSupportedError, AdapterNotAvailableError, } from "./utils/errors.js";
|
|
21
22
|
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 {
|
|
23
|
+
export { setLogLevel, getLogLevel, type LogLevel } from "./utils/logger.js";
|
|
24
|
+
export { isValidKey, isValidValue, isStorageEnvelope, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
23
25
|
import { Strata } from "./core/Strata.js";
|
|
24
26
|
import type { StrataConfig } from "./types/index.js";
|
|
25
27
|
/**
|
|
@@ -28,7 +30,7 @@ import type { StrataConfig } from "./types/index.js";
|
|
|
28
30
|
* custom instance can opt into the same default set. Returns the same instance
|
|
29
31
|
* for chaining.
|
|
30
32
|
*/
|
|
31
|
-
export declare function registerWebAdapters(strata: Strata): Strata;
|
|
33
|
+
export declare function registerWebAdapters(strata: Strata, config?: StrataConfig): Strata;
|
|
32
34
|
/**
|
|
33
35
|
* Create a ready-to-use Strata instance with the standard web adapters
|
|
34
36
|
* pre-registered — the framework-agnostic, Zustand-style entry point. Create it
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { CookieAdapter } from "./adapters/web/CookieAdapter.js";
|
|
|
10
10
|
export { CacheAdapter } from "./adapters/web/CacheAdapter.js";
|
|
11
11
|
export { MemoryAdapter } from "./adapters/web/MemoryAdapter.js";
|
|
12
12
|
export { URLAdapter } from "./adapters/web/URLAdapter.js";
|
|
13
|
+
export { DEFAULT_WEB_KEY_PREFIX } from "./adapters/web/LocalStorageAdapter.js";
|
|
13
14
|
// Core features
|
|
14
15
|
export { EncryptionManager } from "./features/encryption.js";
|
|
15
16
|
export { CompressionManager } from "./features/compression.js";
|
|
@@ -23,8 +24,17 @@ export { computeChecksum, verifyChecksum } from "./features/integrity.js";
|
|
|
23
24
|
export { MigrationManager } from "./features/migration.js";
|
|
24
25
|
// Error classes (exported as values so consumers can use `instanceof`)
|
|
25
26
|
export { StrataError, StorageError, IntegrityError, QuotaExceededError, EncryptionError, CompressionError, SerializationError, ValidationError, NotSupportedError, AdapterNotAvailableError, } from "./utils/errors.js";
|
|
27
|
+
// Diagnostics.
|
|
28
|
+
//
|
|
29
|
+
// 🔴 The logger's own documentation names `setLogLevel('debug')` as the way to
|
|
30
|
+
// raise verbosity, and it was never exported from this entry point — so the
|
|
31
|
+
// documented control was unreachable. That matters as of 2.9.0: keys this
|
|
32
|
+
// library does not own are now SKIPPED and reported at `debug`, which is below
|
|
33
|
+
// the default `warn`. Raising the level is how a consumer answers "why is my
|
|
34
|
+
// key missing from keys()?", so the control has to be reachable.
|
|
35
|
+
export { setLogLevel, getLogLevel } from "./utils/logger.js";
|
|
26
36
|
// Utils
|
|
27
|
-
export { isValidKey, isValidValue, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
37
|
+
export { isValidKey, isValidValue, isStorageEnvelope, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
28
38
|
// Create and export a default storage instance that works immediately
|
|
29
39
|
import { Strata } from "./core/Strata.js";
|
|
30
40
|
import { LocalStorageAdapter } from "./adapters/web/LocalStorageAdapter.js";
|
|
@@ -40,14 +50,29 @@ import { logger } from "./utils/logger.js";
|
|
|
40
50
|
* custom instance can opt into the same default set. Returns the same instance
|
|
41
51
|
* for chaining.
|
|
42
52
|
*/
|
|
43
|
-
export function registerWebAdapters(strata) {
|
|
53
|
+
export function registerWebAdapters(strata, config) {
|
|
54
|
+
// `adapters: { <name>: false }` opts an adapter out of REGISTRATION, not just
|
|
55
|
+
// out of initialization. Registering one the instance will never use still
|
|
56
|
+
// costs a TTL sweep over a storage area it does not own.
|
|
57
|
+
//
|
|
58
|
+
// 🔴 `defaultStorages` is NOT this switch. It is the preference order for
|
|
59
|
+
// picking the DEFAULT adapter; multi-adapter operations (keys/clear/size/
|
|
60
|
+
// subscribe with no `storage`) deliberately span everything registered. That
|
|
61
|
+
// distinction was undocumented, and reading `defaultStorages` as a
|
|
62
|
+
// registration allow-list is what produced ISSUE-09.
|
|
63
|
+
const enabled = (name) => config?.adapters?.[name] !== false;
|
|
44
64
|
try {
|
|
45
65
|
strata.registerAdapter(new MemoryAdapter()); // always-available fallback
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
if (enabled('localStorage'))
|
|
67
|
+
strata.registerAdapter(new LocalStorageAdapter());
|
|
68
|
+
if (enabled('sessionStorage'))
|
|
69
|
+
strata.registerAdapter(new SessionStorageAdapter());
|
|
70
|
+
if (enabled('indexedDB'))
|
|
71
|
+
strata.registerAdapter(new IndexedDBAdapter());
|
|
72
|
+
if (enabled('cookies'))
|
|
73
|
+
strata.registerAdapter(new CookieAdapter());
|
|
74
|
+
if (enabled('cache'))
|
|
75
|
+
strata.registerAdapter(new CacheAdapter());
|
|
51
76
|
}
|
|
52
77
|
catch (error) {
|
|
53
78
|
logger.warn('Strata Storage adapter registration warning:', error);
|
|
@@ -71,7 +96,7 @@ export function registerWebAdapters(strata) {
|
|
|
71
96
|
* ```
|
|
72
97
|
*/
|
|
73
98
|
export function defineStorage(config) {
|
|
74
|
-
return registerWebAdapters(new Strata(config));
|
|
99
|
+
return registerWebAdapters(new Strata(config), config);
|
|
75
100
|
}
|
|
76
101
|
// Default singleton — created via the same factory so behavior is identical.
|
|
77
102
|
// It initializes lazily on first use, so importing the package has no I/O cost.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -308,6 +308,38 @@ export interface StrataConfig {
|
|
|
308
308
|
* Default storage types in order of preference
|
|
309
309
|
*/
|
|
310
310
|
defaultStorages?: StorageType[];
|
|
311
|
+
/**
|
|
312
|
+
* Key prefix for the web adapters that share a storage area with every other
|
|
313
|
+
* script on the origin (`localStorage`, `sessionStorage`).
|
|
314
|
+
*
|
|
315
|
+
* Defaults to `'strata:'` as of 3.0.0. Set `false` (or `''`) for the pre-3.0
|
|
316
|
+
* behaviour of writing to the bare key.
|
|
317
|
+
*
|
|
318
|
+
* 🔴 **Take the opt-out when anything outside this library reads a physical key
|
|
319
|
+
* directly** — a pre-paint theme script that runs before any module loads, or a
|
|
320
|
+
* logger reading its own level. Those readers know the exact key name, and a
|
|
321
|
+
* prefix changes it underneath them. `migrateLegacyKeys` keeps the *data*
|
|
322
|
+
* reachable through this library, but it cannot fix a hard-coded reader.
|
|
323
|
+
*
|
|
324
|
+
* Composes with `namespace`, which is a separate mechanism and unaffected:
|
|
325
|
+
* the physical key is `<keyPrefix><namespace>:<key>`. A per-adapter
|
|
326
|
+
* `adapters.localStorage.prefix` overrides this for that adapter.
|
|
327
|
+
*/
|
|
328
|
+
keyPrefix?: string | false;
|
|
329
|
+
/**
|
|
330
|
+
* Whether the shared-area web adapters may adopt pre-3.0 unprefixed entries.
|
|
331
|
+
* Default `true`.
|
|
332
|
+
*
|
|
333
|
+
* Migration is per key and happens on read: a miss at the prefixed key falls
|
|
334
|
+
* back to the bare key, and if the value is one of ours it is moved under the
|
|
335
|
+
* prefix. It never overwrites an existing prefixed value, and it never adopts a
|
|
336
|
+
* value that is not a `StorageValue` envelope — which is what stops it taking
|
|
337
|
+
* another application's keys.
|
|
338
|
+
*
|
|
339
|
+
* Set `false` when two applications share an origin and one of them is still on
|
|
340
|
+
* 2.x, so that upgrading one does not move keys the other still reads.
|
|
341
|
+
*/
|
|
342
|
+
migrateLegacyKeys?: boolean;
|
|
311
343
|
/**
|
|
312
344
|
* Adapter configuration
|
|
313
345
|
*/
|
|
@@ -518,6 +550,19 @@ export interface StorageAdapter {
|
|
|
518
550
|
* Get all keys
|
|
519
551
|
*/
|
|
520
552
|
keys(pattern?: string | RegExp): Promise<string[]>;
|
|
553
|
+
/**
|
|
554
|
+
* Apply configuration synchronously, before any operation runs. Optional —
|
|
555
|
+
* an adapter with no configurable fields omits it. Implementations do pure
|
|
556
|
+
* assignment only: config that changes the physical key cannot wait for the
|
|
557
|
+
* async `initialize()`, because the synchronous API is usable before it.
|
|
558
|
+
*/
|
|
559
|
+
configure?(config?: unknown): void;
|
|
560
|
+
/**
|
|
561
|
+
* Whether this backend can serve a SYNCHRONOUS operation right now, without
|
|
562
|
+
* awaiting anything. Optional; absence means "assume usable". Lets the sync
|
|
563
|
+
* path honour `defaultStorages` as the ordered fallback list it reads as.
|
|
564
|
+
*/
|
|
565
|
+
isAvailableSync?(): boolean;
|
|
521
566
|
/** Synchronous get — sync-capable adapters only. */
|
|
522
567
|
getSync?<T = unknown>(key: string): StorageValue<T> | null;
|
|
523
568
|
/** 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}`);
|