strata-storage 1.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/Readme.md +113 -0
- package/android/src/main/java/com/strata/storage/EncryptedStorage.java +65 -0
- package/android/src/main/java/com/strata/storage/SQLiteStorage.java +147 -0
- package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +74 -0
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +256 -0
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts +46 -0
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -0
- package/dist/adapters/capacitor/FilesystemAdapter.js +162 -0
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts +46 -0
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -0
- package/dist/adapters/capacitor/PreferencesAdapter.js +162 -0
- package/dist/adapters/capacitor/SecureAdapter.d.ts +69 -0
- package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -0
- package/dist/adapters/capacitor/SecureAdapter.js +214 -0
- package/dist/adapters/capacitor/SqliteAdapter.d.ts +68 -0
- package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -0
- package/dist/adapters/capacitor/SqliteAdapter.js +277 -0
- package/dist/adapters/capacitor/index.d.ts +9 -0
- package/dist/adapters/capacitor/index.d.ts.map +1 -0
- package/dist/adapters/capacitor/index.js +8 -0
- package/dist/adapters/web/CacheAdapter.d.ts +91 -0
- package/dist/adapters/web/CacheAdapter.d.ts.map +1 -0
- package/dist/adapters/web/CacheAdapter.js +291 -0
- package/dist/adapters/web/CookieAdapter.d.ts +77 -0
- package/dist/adapters/web/CookieAdapter.d.ts.map +1 -0
- package/dist/adapters/web/CookieAdapter.js +260 -0
- package/dist/adapters/web/IndexedDBAdapter.d.ts +78 -0
- package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -0
- package/dist/adapters/web/IndexedDBAdapter.js +371 -0
- package/dist/adapters/web/LocalStorageAdapter.d.ts +63 -0
- package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -0
- package/dist/adapters/web/LocalStorageAdapter.js +238 -0
- package/dist/adapters/web/MemoryAdapter.d.ts +69 -0
- package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -0
- package/dist/adapters/web/MemoryAdapter.js +165 -0
- package/dist/adapters/web/SessionStorageAdapter.d.ts +53 -0
- package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -0
- package/dist/adapters/web/SessionStorageAdapter.js +180 -0
- package/dist/adapters/web/index.d.ts +10 -0
- package/dist/adapters/web/index.d.ts.map +1 -0
- package/dist/adapters/web/index.js +9 -0
- package/dist/core/AdapterRegistry.d.ts +52 -0
- package/dist/core/AdapterRegistry.d.ts.map +1 -0
- package/dist/core/AdapterRegistry.js +102 -0
- package/dist/core/BaseAdapter.d.ts +79 -0
- package/dist/core/BaseAdapter.d.ts.map +1 -0
- package/dist/core/BaseAdapter.js +197 -0
- package/dist/core/StorageStrategy.d.ts +55 -0
- package/dist/core/StorageStrategy.d.ts.map +1 -0
- package/dist/core/StorageStrategy.js +199 -0
- package/dist/core/Strata.d.ts +122 -0
- package/dist/core/Strata.d.ts.map +1 -0
- package/dist/core/Strata.js +568 -0
- package/dist/features/compression.d.ts +65 -0
- package/dist/features/compression.d.ts.map +1 -0
- package/dist/features/compression.js +205 -0
- package/dist/features/encryption.d.ts +68 -0
- package/dist/features/encryption.d.ts.map +1 -0
- package/dist/features/encryption.js +172 -0
- package/dist/features/migration.d.ts +17 -0
- package/dist/features/migration.d.ts.map +1 -0
- package/dist/features/migration.js +43 -0
- package/dist/features/query.d.ts +75 -0
- package/dist/features/query.d.ts.map +1 -0
- package/dist/features/query.js +305 -0
- package/dist/features/sync.d.ts +87 -0
- package/dist/features/sync.d.ts.map +1 -0
- package/dist/features/sync.js +233 -0
- package/dist/features/ttl.d.ts +124 -0
- package/dist/features/ttl.d.ts.map +1 -0
- package/dist/features/ttl.js +236 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/package.json +60 -0
- package/dist/plugin/definitions.d.ts +219 -0
- package/dist/plugin/definitions.d.ts.map +1 -0
- package/dist/plugin/definitions.js +5 -0
- package/dist/plugin/index.d.ts +8 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +27 -0
- package/dist/plugin/web.d.ts +24 -0
- package/dist/plugin/web.d.ts.map +1 -0
- package/dist/plugin/web.js +35 -0
- package/dist/types/index.d.ts +558 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- package/dist/utils/errors.d.ts +92 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +153 -0
- package/dist/utils/index.d.ts +105 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +329 -0
- package/ios/Plugin/KeychainStorage.swift +87 -0
- package/ios/Plugin/SQLiteStorage.swift +167 -0
- package/ios/Plugin/StrataStoragePlugin.swift +204 -0
- package/ios/Plugin/UserDefaultsStorage.swift +44 -0
- package/package.json +126 -0
- package/scripts/build.js +52 -0
- package/scripts/cli.js +60 -0
- package/scripts/configure.js +444 -0
- package/scripts/postinstall.js +33 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IndexedDBAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/web/IndexedDBAdapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,WAAW,EACZ,MAAM,SAAS,CAAC;AAIjB;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAe;IACzC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAUxC;IAEF,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,EAAE,CAAC,CAAc;gBAEb,MAAM,SAAkB,EAAE,SAAS,SAAY,EAAE,OAAO,SAAI;IAOxE;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAIrC;;OAEG;IACG,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IASjB;;OAEG;YACW,YAAY;IAgC1B;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAiCpE;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B1E;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBxC;;OAEG;IACG,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBlD;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BxD;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IAkC9F;;OAEG;IACG,IAAI,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwDjD;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAQzC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B;;OAEG;IACH,OAAO,CAAC,YAAY;CAMrB"}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB Adapter - Browser IndexedDB implementation
|
|
3
|
+
* Provides large-scale persistent storage with advanced features
|
|
4
|
+
*/
|
|
5
|
+
import { BaseAdapter } from '@/core/BaseAdapter';
|
|
6
|
+
import { createDeferred, getObjectSize } from '@/utils';
|
|
7
|
+
import { StorageError, QuotaExceededError, TransactionError } from '@/utils/errors';
|
|
8
|
+
/**
|
|
9
|
+
* Browser IndexedDB adapter
|
|
10
|
+
*/
|
|
11
|
+
export class IndexedDBAdapter extends BaseAdapter {
|
|
12
|
+
name = 'indexedDB';
|
|
13
|
+
capabilities = {
|
|
14
|
+
persistent: true,
|
|
15
|
+
synchronous: false,
|
|
16
|
+
observable: false, // No native change events
|
|
17
|
+
transactional: true,
|
|
18
|
+
queryable: true,
|
|
19
|
+
maxSize: -1, // Browser dependent, typically GBs
|
|
20
|
+
binary: true,
|
|
21
|
+
encrypted: false,
|
|
22
|
+
crossTab: false, // No built-in cross-tab sync
|
|
23
|
+
};
|
|
24
|
+
dbName;
|
|
25
|
+
storeName;
|
|
26
|
+
version;
|
|
27
|
+
db;
|
|
28
|
+
constructor(dbName = 'StrataStorage', storeName = 'storage', version = 1) {
|
|
29
|
+
super();
|
|
30
|
+
this.dbName = dbName;
|
|
31
|
+
this.storeName = storeName;
|
|
32
|
+
this.version = version;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if IndexedDB is available
|
|
36
|
+
*/
|
|
37
|
+
async isAvailable() {
|
|
38
|
+
return typeof window !== 'undefined' && 'indexedDB' in window && window.indexedDB !== null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Initialize the adapter
|
|
42
|
+
*/
|
|
43
|
+
async initialize(config) {
|
|
44
|
+
if (config?.dbName)
|
|
45
|
+
this.dbName = config.dbName;
|
|
46
|
+
if (config?.storeName)
|
|
47
|
+
this.storeName = config.storeName;
|
|
48
|
+
if (config?.version)
|
|
49
|
+
this.version = config.version;
|
|
50
|
+
await this.openDatabase();
|
|
51
|
+
this.startTTLCleanup();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Open IndexedDB database
|
|
55
|
+
*/
|
|
56
|
+
async openDatabase() {
|
|
57
|
+
if (this.db)
|
|
58
|
+
return this.db;
|
|
59
|
+
const { promise, resolve, reject } = createDeferred();
|
|
60
|
+
const request = window.indexedDB.open(this.dbName, this.version);
|
|
61
|
+
request.onerror = () => reject(new StorageError(`Failed to open IndexedDB: ${request.error}`));
|
|
62
|
+
request.onsuccess = () => {
|
|
63
|
+
this.db = request.result;
|
|
64
|
+
resolve(request.result);
|
|
65
|
+
};
|
|
66
|
+
request.onupgradeneeded = (event) => {
|
|
67
|
+
const db = event.target.result;
|
|
68
|
+
// Create object store if it doesn't exist
|
|
69
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
70
|
+
const store = db.createObjectStore(this.storeName, { keyPath: 'key' });
|
|
71
|
+
// Create indexes for efficient querying
|
|
72
|
+
store.createIndex('expires', 'expires', { unique: false });
|
|
73
|
+
store.createIndex('tags', 'tags', { unique: false, multiEntry: true });
|
|
74
|
+
store.createIndex('created', 'created', { unique: false });
|
|
75
|
+
store.createIndex('updated', 'updated', { unique: false });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return promise;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get a value from IndexedDB
|
|
82
|
+
*/
|
|
83
|
+
async get(key) {
|
|
84
|
+
const db = await this.openDatabase();
|
|
85
|
+
const { promise, resolve, reject } = createDeferred();
|
|
86
|
+
const transaction = db.transaction([this.storeName], 'readonly');
|
|
87
|
+
const store = transaction.objectStore(this.storeName);
|
|
88
|
+
const request = store.get(key);
|
|
89
|
+
request.onsuccess = () => {
|
|
90
|
+
const result = request.result;
|
|
91
|
+
if (!result) {
|
|
92
|
+
resolve(null);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Remove the key property as it's not part of StorageValue
|
|
96
|
+
const { key: _, ...value } = result;
|
|
97
|
+
// Check TTL
|
|
98
|
+
if (this.isExpired(value)) {
|
|
99
|
+
// Don't wait for removal
|
|
100
|
+
this.remove(key).catch(console.error);
|
|
101
|
+
resolve(null);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
resolve(value);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
request.onerror = () => reject(new StorageError(`Failed to get key ${key}: ${request.error}`));
|
|
108
|
+
return promise;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Set a value in IndexedDB
|
|
112
|
+
*/
|
|
113
|
+
async set(key, value) {
|
|
114
|
+
const db = await this.openDatabase();
|
|
115
|
+
const oldValue = await this.get(key);
|
|
116
|
+
const { promise, resolve, reject } = createDeferred();
|
|
117
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
118
|
+
const store = transaction.objectStore(this.storeName);
|
|
119
|
+
// Add key to the value for IndexedDB storage
|
|
120
|
+
const record = { key, ...value };
|
|
121
|
+
const request = store.put(record);
|
|
122
|
+
request.onsuccess = () => {
|
|
123
|
+
resolve();
|
|
124
|
+
this.emitChange(key, oldValue?.value, value.value, 'local');
|
|
125
|
+
};
|
|
126
|
+
request.onerror = () => {
|
|
127
|
+
if (this.isQuotaError(request.error)) {
|
|
128
|
+
reject(new QuotaExceededError('IndexedDB quota exceeded', { key, size: getObjectSize(value) }));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
reject(new StorageError(`Failed to set key ${key}: ${request.error}`));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return promise;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Remove a value from IndexedDB
|
|
138
|
+
*/
|
|
139
|
+
async remove(key) {
|
|
140
|
+
const db = await this.openDatabase();
|
|
141
|
+
const oldValue = await this.get(key);
|
|
142
|
+
const { promise, resolve, reject } = createDeferred();
|
|
143
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
144
|
+
const store = transaction.objectStore(this.storeName);
|
|
145
|
+
const request = store.delete(key);
|
|
146
|
+
request.onsuccess = () => {
|
|
147
|
+
resolve();
|
|
148
|
+
if (oldValue) {
|
|
149
|
+
this.emitChange(key, oldValue.value, undefined, 'local');
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
request.onerror = () => reject(new StorageError(`Failed to remove key ${key}: ${request.error}`));
|
|
153
|
+
return promise;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Clear IndexedDB
|
|
157
|
+
*/
|
|
158
|
+
async clear(options) {
|
|
159
|
+
if (!options || (!options.pattern && !options.tags && !options.expiredOnly)) {
|
|
160
|
+
// Clear everything
|
|
161
|
+
const db = await this.openDatabase();
|
|
162
|
+
const { promise, resolve, reject } = createDeferred();
|
|
163
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
164
|
+
const store = transaction.objectStore(this.storeName);
|
|
165
|
+
const request = store.clear();
|
|
166
|
+
request.onsuccess = () => {
|
|
167
|
+
resolve();
|
|
168
|
+
this.emitChange('*', undefined, undefined, 'local');
|
|
169
|
+
};
|
|
170
|
+
request.onerror = () => reject(new StorageError(`Failed to clear store: ${request.error}`));
|
|
171
|
+
return promise;
|
|
172
|
+
}
|
|
173
|
+
// Use base implementation for filtered clear
|
|
174
|
+
await super.clear(options);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get all keys
|
|
178
|
+
*/
|
|
179
|
+
async keys(pattern) {
|
|
180
|
+
const db = await this.openDatabase();
|
|
181
|
+
const { promise, resolve, reject } = createDeferred();
|
|
182
|
+
const transaction = db.transaction([this.storeName], 'readonly');
|
|
183
|
+
const store = transaction.objectStore(this.storeName);
|
|
184
|
+
const request = store.getAllKeys();
|
|
185
|
+
request.onsuccess = async () => {
|
|
186
|
+
const allKeys = request.result;
|
|
187
|
+
// Filter expired keys
|
|
188
|
+
const validKeys = [];
|
|
189
|
+
for (const key of allKeys) {
|
|
190
|
+
const value = await this.get(key);
|
|
191
|
+
if (value) {
|
|
192
|
+
validKeys.push(key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
resolve(this.filterKeys(validKeys, pattern));
|
|
196
|
+
};
|
|
197
|
+
request.onerror = () => reject(new StorageError(`Failed to get keys: ${request.error}`));
|
|
198
|
+
return promise;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Query IndexedDB with conditions
|
|
202
|
+
*/
|
|
203
|
+
async query(condition) {
|
|
204
|
+
const db = await this.openDatabase();
|
|
205
|
+
const { promise, resolve, reject } = createDeferred();
|
|
206
|
+
const transaction = db.transaction([this.storeName], 'readonly');
|
|
207
|
+
const store = transaction.objectStore(this.storeName);
|
|
208
|
+
const results = [];
|
|
209
|
+
// For now, we'll do a full scan and filter
|
|
210
|
+
// In the future, we can optimize using indexes
|
|
211
|
+
const request = store.openCursor();
|
|
212
|
+
request.onsuccess = (event) => {
|
|
213
|
+
const cursor = event.target.result;
|
|
214
|
+
if (cursor) {
|
|
215
|
+
const record = cursor.value;
|
|
216
|
+
const { key, ...value } = record;
|
|
217
|
+
if (!this.isExpired(value) && this.queryEngine.matches(value.value, condition)) {
|
|
218
|
+
results.push({ key, value: value.value });
|
|
219
|
+
}
|
|
220
|
+
cursor.continue();
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
resolve(results);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
request.onerror = () => reject(new StorageError(`Query failed: ${request.error}`));
|
|
227
|
+
return promise;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get storage size
|
|
231
|
+
*/
|
|
232
|
+
async size(detailed) {
|
|
233
|
+
const db = await this.openDatabase();
|
|
234
|
+
const { promise, resolve, reject } = createDeferred();
|
|
235
|
+
const transaction = db.transaction([this.storeName], 'readonly');
|
|
236
|
+
const store = transaction.objectStore(this.storeName);
|
|
237
|
+
let total = 0;
|
|
238
|
+
let count = 0;
|
|
239
|
+
let keySize = 0;
|
|
240
|
+
let valueSize = 0;
|
|
241
|
+
let metadataSize = 0;
|
|
242
|
+
const request = store.openCursor();
|
|
243
|
+
request.onsuccess = (event) => {
|
|
244
|
+
const cursor = event.target.result;
|
|
245
|
+
if (cursor) {
|
|
246
|
+
const record = cursor.value;
|
|
247
|
+
const { key, value, ...metadata } = record;
|
|
248
|
+
count++;
|
|
249
|
+
const recordKeySize = key.length * 2;
|
|
250
|
+
const recordValueSize = getObjectSize(value);
|
|
251
|
+
const recordMetadataSize = getObjectSize(metadata);
|
|
252
|
+
total += recordKeySize + recordValueSize + recordMetadataSize;
|
|
253
|
+
if (detailed) {
|
|
254
|
+
keySize += recordKeySize;
|
|
255
|
+
valueSize += recordValueSize;
|
|
256
|
+
metadataSize += recordMetadataSize;
|
|
257
|
+
}
|
|
258
|
+
cursor.continue();
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const result = { total, count };
|
|
262
|
+
if (detailed) {
|
|
263
|
+
result.detailed = {
|
|
264
|
+
keys: keySize,
|
|
265
|
+
values: valueSize,
|
|
266
|
+
metadata: metadataSize,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
resolve(result);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
request.onerror = () => reject(new StorageError(`Failed to calculate size: ${request.error}`));
|
|
273
|
+
return promise;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Begin a transaction
|
|
277
|
+
*/
|
|
278
|
+
async transaction() {
|
|
279
|
+
const db = await this.openDatabase();
|
|
280
|
+
const txn = db.transaction([this.storeName], 'readwrite');
|
|
281
|
+
const store = txn.objectStore(this.storeName);
|
|
282
|
+
return new IndexedDBTransaction(store, txn);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Close the adapter
|
|
286
|
+
*/
|
|
287
|
+
async close() {
|
|
288
|
+
if (this.db) {
|
|
289
|
+
this.db.close();
|
|
290
|
+
this.db = undefined;
|
|
291
|
+
}
|
|
292
|
+
if (super.close) {
|
|
293
|
+
await super.close();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if error is quota exceeded
|
|
298
|
+
*/
|
|
299
|
+
isQuotaError(error) {
|
|
300
|
+
if (error instanceof Error || error instanceof DOMException) {
|
|
301
|
+
return error.name === 'QuotaExceededError' || error.message.toLowerCase().includes('quota');
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* IndexedDB transaction implementation
|
|
308
|
+
*/
|
|
309
|
+
class IndexedDBTransaction {
|
|
310
|
+
store;
|
|
311
|
+
txn;
|
|
312
|
+
committed = false;
|
|
313
|
+
aborted = false;
|
|
314
|
+
constructor(store, txn) {
|
|
315
|
+
this.store = store;
|
|
316
|
+
this.txn = txn;
|
|
317
|
+
}
|
|
318
|
+
async get(key) {
|
|
319
|
+
if (this.aborted)
|
|
320
|
+
throw new TransactionError('Transaction already aborted');
|
|
321
|
+
const { promise, resolve, reject } = createDeferred();
|
|
322
|
+
const request = this.store.get(key);
|
|
323
|
+
request.onsuccess = () => {
|
|
324
|
+
const result = request.result;
|
|
325
|
+
resolve(result ? result.value : null);
|
|
326
|
+
};
|
|
327
|
+
request.onerror = () => reject(new StorageError(`Transaction get failed: ${request.error}`));
|
|
328
|
+
return promise;
|
|
329
|
+
}
|
|
330
|
+
async set(key, value) {
|
|
331
|
+
if (this.aborted)
|
|
332
|
+
throw new TransactionError('Transaction already aborted');
|
|
333
|
+
const { promise, resolve, reject } = createDeferred();
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const record = {
|
|
336
|
+
key,
|
|
337
|
+
value,
|
|
338
|
+
created: now,
|
|
339
|
+
updated: now,
|
|
340
|
+
};
|
|
341
|
+
const request = this.store.put(record);
|
|
342
|
+
request.onsuccess = () => resolve();
|
|
343
|
+
request.onerror = () => reject(new StorageError(`Transaction set failed: ${request.error}`));
|
|
344
|
+
return promise;
|
|
345
|
+
}
|
|
346
|
+
async remove(key) {
|
|
347
|
+
if (this.aborted)
|
|
348
|
+
throw new TransactionError('Transaction already aborted');
|
|
349
|
+
const { promise, resolve, reject } = createDeferred();
|
|
350
|
+
const request = this.store.delete(key);
|
|
351
|
+
request.onsuccess = () => resolve();
|
|
352
|
+
request.onerror = () => reject(new StorageError(`Transaction remove failed: ${request.error}`));
|
|
353
|
+
return promise;
|
|
354
|
+
}
|
|
355
|
+
async commit() {
|
|
356
|
+
if (this.aborted)
|
|
357
|
+
throw new TransactionError('Cannot commit aborted transaction');
|
|
358
|
+
if (this.committed)
|
|
359
|
+
throw new TransactionError('Transaction already committed');
|
|
360
|
+
this.committed = true;
|
|
361
|
+
// IndexedDB auto-commits when transaction completes
|
|
362
|
+
}
|
|
363
|
+
async rollback() {
|
|
364
|
+
if (this.committed)
|
|
365
|
+
throw new TransactionError('Cannot rollback committed transaction');
|
|
366
|
+
if (this.aborted)
|
|
367
|
+
return;
|
|
368
|
+
this.aborted = true;
|
|
369
|
+
this.txn.abort();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage Adapter - Browser localStorage implementation
|
|
3
|
+
* Provides persistent storage with 5-10MB limit
|
|
4
|
+
*/
|
|
5
|
+
import { BaseAdapter } from '@/core/BaseAdapter';
|
|
6
|
+
import type { StorageType, StorageCapabilities, StorageValue, ClearOptions, SizeInfo, SubscriptionCallback, UnsubscribeFunction } from '@/types';
|
|
7
|
+
/**
|
|
8
|
+
* Browser localStorage adapter
|
|
9
|
+
*/
|
|
10
|
+
export declare class LocalStorageAdapter extends BaseAdapter {
|
|
11
|
+
readonly name: StorageType;
|
|
12
|
+
readonly capabilities: StorageCapabilities;
|
|
13
|
+
protected prefix: string;
|
|
14
|
+
protected listeners: Map<SubscriptionCallback, (event: StorageEvent) => void>;
|
|
15
|
+
constructor(prefix?: string);
|
|
16
|
+
/**
|
|
17
|
+
* Check if localStorage is available
|
|
18
|
+
*/
|
|
19
|
+
isAvailable(): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the adapter
|
|
22
|
+
*/
|
|
23
|
+
initialize(config?: {
|
|
24
|
+
prefix?: string;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Get a value from localStorage
|
|
28
|
+
*/
|
|
29
|
+
get<T = unknown>(key: string): Promise<StorageValue<T> | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Set a value in localStorage
|
|
32
|
+
*/
|
|
33
|
+
set<T = unknown>(key: string, value: StorageValue<T>): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Remove a value from localStorage
|
|
36
|
+
*/
|
|
37
|
+
remove(key: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Clear localStorage
|
|
40
|
+
*/
|
|
41
|
+
clear(options?: ClearOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Get all keys
|
|
44
|
+
*/
|
|
45
|
+
keys(pattern?: string | RegExp): Promise<string[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Get storage size
|
|
48
|
+
*/
|
|
49
|
+
size(detailed?: boolean): Promise<SizeInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* Subscribe to storage changes
|
|
52
|
+
*/
|
|
53
|
+
subscribe(callback: SubscriptionCallback): UnsubscribeFunction;
|
|
54
|
+
/**
|
|
55
|
+
* Close the adapter
|
|
56
|
+
*/
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Check if error is quota exceeded
|
|
60
|
+
*/
|
|
61
|
+
protected isQuotaError(error: unknown): boolean;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=LocalStorageAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalStorageAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/web/LocalStorageAdapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAIjB;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,WAAW;IAClD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAkB;IAC5C,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAUxC;IAEF,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,SAAS,EAAE,GAAG,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,CAAa;gBAE9E,MAAM,SAAY;IAK9B;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBrC;;OAEG;IACG,UAAU,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7D;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAoBpE;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB1E;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASxC;;OAEG;IACG,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBlD;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBxD;;OAEG;IACG,IAAI,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoCjD;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,mBAAmB;IAkC9D;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B;;OAEG;IACH,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;CAUhD"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage Adapter - Browser localStorage implementation
|
|
3
|
+
* Provides persistent storage with 5-10MB limit
|
|
4
|
+
*/
|
|
5
|
+
import { BaseAdapter } from '@/core/BaseAdapter';
|
|
6
|
+
import { serialize, deserialize, getObjectSize } from '@/utils';
|
|
7
|
+
import { QuotaExceededError, SerializationError } from '@/utils/errors';
|
|
8
|
+
/**
|
|
9
|
+
* Browser localStorage adapter
|
|
10
|
+
*/
|
|
11
|
+
export class LocalStorageAdapter extends BaseAdapter {
|
|
12
|
+
name = 'localStorage';
|
|
13
|
+
capabilities = {
|
|
14
|
+
persistent: true,
|
|
15
|
+
synchronous: false, // We use async for consistency
|
|
16
|
+
observable: true, // Via storage events
|
|
17
|
+
transactional: false,
|
|
18
|
+
queryable: true,
|
|
19
|
+
maxSize: 10 * 1024 * 1024, // Typically 5-10MB
|
|
20
|
+
binary: false, // Only strings
|
|
21
|
+
encrypted: false,
|
|
22
|
+
crossTab: true, // Storage events work across tabs
|
|
23
|
+
};
|
|
24
|
+
prefix;
|
|
25
|
+
listeners = new Map();
|
|
26
|
+
constructor(prefix = 'strata:') {
|
|
27
|
+
super();
|
|
28
|
+
this.prefix = prefix;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if localStorage is available
|
|
32
|
+
*/
|
|
33
|
+
async isAvailable() {
|
|
34
|
+
try {
|
|
35
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Test if we can actually use it
|
|
39
|
+
const testKey = `${this.prefix}__test__`;
|
|
40
|
+
window.localStorage.setItem(testKey, 'test');
|
|
41
|
+
window.localStorage.removeItem(testKey);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the adapter
|
|
50
|
+
*/
|
|
51
|
+
async initialize(config) {
|
|
52
|
+
if (config?.prefix) {
|
|
53
|
+
this.prefix = config.prefix;
|
|
54
|
+
}
|
|
55
|
+
this.startTTLCleanup();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get a value from localStorage
|
|
59
|
+
*/
|
|
60
|
+
async get(key) {
|
|
61
|
+
try {
|
|
62
|
+
const item = window.localStorage.getItem(this.prefix + key);
|
|
63
|
+
if (!item)
|
|
64
|
+
return null;
|
|
65
|
+
const value = deserialize(item);
|
|
66
|
+
// Check TTL
|
|
67
|
+
if (this.isExpired(value)) {
|
|
68
|
+
await this.remove(key);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error(`Failed to get key ${key} from localStorage:`, error);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Set a value in localStorage
|
|
80
|
+
*/
|
|
81
|
+
async set(key, value) {
|
|
82
|
+
const fullKey = this.prefix + key;
|
|
83
|
+
const oldValue = await this.get(key);
|
|
84
|
+
try {
|
|
85
|
+
const serialized = serialize(value);
|
|
86
|
+
window.localStorage.setItem(fullKey, serialized);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (this.isQuotaError(error)) {
|
|
90
|
+
throw new QuotaExceededError('LocalStorage quota exceeded', {
|
|
91
|
+
key,
|
|
92
|
+
size: getObjectSize(value),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw new SerializationError(`Failed to store key ${key} in localStorage`, error);
|
|
96
|
+
}
|
|
97
|
+
// Emit change event (storage events don't fire in same window)
|
|
98
|
+
this.emitChange(key, oldValue?.value, value.value, 'local');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove a value from localStorage
|
|
102
|
+
*/
|
|
103
|
+
async remove(key) {
|
|
104
|
+
const oldValue = await this.get(key);
|
|
105
|
+
window.localStorage.removeItem(this.prefix + key);
|
|
106
|
+
if (oldValue) {
|
|
107
|
+
this.emitChange(key, oldValue.value, undefined, 'local');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clear localStorage
|
|
112
|
+
*/
|
|
113
|
+
async clear(options) {
|
|
114
|
+
if (!options || (!options.pattern && !options.tags && !options.expiredOnly)) {
|
|
115
|
+
// Clear all with our prefix
|
|
116
|
+
const keysToRemove = [];
|
|
117
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
118
|
+
const key = window.localStorage.key(i);
|
|
119
|
+
if (key?.startsWith(this.prefix)) {
|
|
120
|
+
keysToRemove.push(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
keysToRemove.forEach((key) => window.localStorage.removeItem(key));
|
|
124
|
+
this.emitChange('*', undefined, undefined, 'local');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Use base implementation for filtered clear
|
|
128
|
+
await super.clear(options);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get all keys
|
|
132
|
+
*/
|
|
133
|
+
async keys(pattern) {
|
|
134
|
+
const keys = [];
|
|
135
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
136
|
+
const fullKey = window.localStorage.key(i);
|
|
137
|
+
if (fullKey?.startsWith(this.prefix)) {
|
|
138
|
+
const key = fullKey.substring(this.prefix.length);
|
|
139
|
+
// Check if not expired
|
|
140
|
+
const value = await this.get(key);
|
|
141
|
+
if (value) {
|
|
142
|
+
keys.push(key);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return this.filterKeys(keys, pattern);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get storage size
|
|
150
|
+
*/
|
|
151
|
+
async size(detailed) {
|
|
152
|
+
let total = 0;
|
|
153
|
+
let count = 0;
|
|
154
|
+
let keySize = 0;
|
|
155
|
+
let valueSize = 0;
|
|
156
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
157
|
+
const fullKey = window.localStorage.key(i);
|
|
158
|
+
if (fullKey?.startsWith(this.prefix)) {
|
|
159
|
+
const item = window.localStorage.getItem(fullKey);
|
|
160
|
+
if (item) {
|
|
161
|
+
count++;
|
|
162
|
+
const itemSize = (fullKey.length + item.length) * 2; // UTF-16
|
|
163
|
+
total += itemSize;
|
|
164
|
+
if (detailed) {
|
|
165
|
+
keySize += fullKey.length * 2;
|
|
166
|
+
valueSize += item.length * 2;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const result = { total, count };
|
|
172
|
+
if (detailed) {
|
|
173
|
+
result.detailed = {
|
|
174
|
+
keys: keySize,
|
|
175
|
+
values: valueSize,
|
|
176
|
+
metadata: 0, // Metadata is included in values for localStorage
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Subscribe to storage changes
|
|
183
|
+
*/
|
|
184
|
+
subscribe(callback) {
|
|
185
|
+
const listener = (event) => {
|
|
186
|
+
// Only process events from other windows/tabs
|
|
187
|
+
if (event.storageArea !== window.localStorage)
|
|
188
|
+
return;
|
|
189
|
+
// Check if the key belongs to us
|
|
190
|
+
if (!event.key || !event.key.startsWith(this.prefix))
|
|
191
|
+
return;
|
|
192
|
+
const key = event.key.substring(this.prefix.length);
|
|
193
|
+
const oldValue = event.oldValue ? deserialize(event.oldValue) : null;
|
|
194
|
+
const newValue = event.newValue ? deserialize(event.newValue) : null;
|
|
195
|
+
callback({
|
|
196
|
+
key,
|
|
197
|
+
oldValue: oldValue?.value ?? undefined,
|
|
198
|
+
newValue: newValue?.value ?? undefined,
|
|
199
|
+
source: 'remote',
|
|
200
|
+
storage: this.name,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
window.addEventListener('storage', listener);
|
|
205
|
+
this.listeners.set(callback, listener);
|
|
206
|
+
return () => {
|
|
207
|
+
const storedListener = this.listeners.get(callback);
|
|
208
|
+
if (storedListener) {
|
|
209
|
+
window.removeEventListener('storage', storedListener);
|
|
210
|
+
this.listeners.delete(callback);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Close the adapter
|
|
216
|
+
*/
|
|
217
|
+
async close() {
|
|
218
|
+
// Remove all storage event listeners
|
|
219
|
+
this.listeners.forEach((listener) => {
|
|
220
|
+
window.removeEventListener('storage', listener);
|
|
221
|
+
});
|
|
222
|
+
this.listeners.clear();
|
|
223
|
+
if (super.close) {
|
|
224
|
+
await super.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Check if error is quota exceeded
|
|
229
|
+
*/
|
|
230
|
+
isQuotaError(error) {
|
|
231
|
+
if (error instanceof Error) {
|
|
232
|
+
return (error.name === 'QuotaExceededError' ||
|
|
233
|
+
error.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
|
|
234
|
+
error.message.toLowerCase().includes('quota'));
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|