onelaraveljs 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 +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- package/src/utils/validation.js +32 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import logger from "./LoggerService.js";
|
|
2
|
+
|
|
3
|
+
// StorageService - Service thuần túy quản lý localStorage với Event System, TTL và Encrypt
|
|
4
|
+
export class StorageService {
|
|
5
|
+
static instance = null;
|
|
6
|
+
static privateProperties = [
|
|
7
|
+
'__key',
|
|
8
|
+
'__isSupport',
|
|
9
|
+
'__data',
|
|
10
|
+
'__listeners', '__eventQueue', '__isUpdating', 'instance',
|
|
11
|
+
'set', 'get', 'remove', 'clear', 'getAll', 'has', 'getAllKeys', 'size', 'isEmpty', 'getInfo', 'debug',
|
|
12
|
+
'export', 'import', 'backup', 'restore', 'getStorageUsage',
|
|
13
|
+
'isStorageFull',
|
|
14
|
+
'setKey', 'getKey', 'support', '__loadData', '__updateData', 'emit', 'on', 'off', 'removeAllListeners', 'getEvents', 'getListenerCount',
|
|
15
|
+
'getTTLInfo', 'cleanExpired', 'enableEncryption', '__generateEncryptionKey',
|
|
16
|
+
'__encrypt', '__decrypt', '__isExpired', '__cleanExpiredData', '__createDynamicProperty', '__removeDynamicProperty'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
constructor(key = 'onejs_storage') {
|
|
21
|
+
this.__key = key || "onejs_storage";
|
|
22
|
+
this.__isSupport = typeof (Storage) !== "undefined";
|
|
23
|
+
this.__data = {};
|
|
24
|
+
this.__listeners = new Map(); // Event listeners
|
|
25
|
+
this.__eventQueue = []; // Event queue for batching
|
|
26
|
+
this.__isUpdating = false;
|
|
27
|
+
|
|
28
|
+
// Encryption system
|
|
29
|
+
this.encryptionKey = null;
|
|
30
|
+
this.useEncryption = false;
|
|
31
|
+
this.dynamicProperties = [];
|
|
32
|
+
if (this.__isSupport) {
|
|
33
|
+
this.__loadData();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ==========================================
|
|
38
|
+
// PUBLIC STATIC METHODS
|
|
39
|
+
// ==========================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the instance of the StorageService
|
|
43
|
+
* @param {string} key - The key to use for the storage
|
|
44
|
+
* @returns {StorageService} - The instance of the StorageService
|
|
45
|
+
*/
|
|
46
|
+
static getInstance(key) {
|
|
47
|
+
if (!StorageService.instance) {
|
|
48
|
+
StorageService.instance = new StorageService(key);
|
|
49
|
+
}
|
|
50
|
+
return StorageService.instance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static make(key) {
|
|
54
|
+
return new StorageService(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ==========================================
|
|
58
|
+
// PUBLIC INSTANCE METHODS - Configuration
|
|
59
|
+
// ==========================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set the key for the storage
|
|
63
|
+
* @param {string} key - The key to use for the storage
|
|
64
|
+
* @returns {StorageService} - The instance of the StorageService
|
|
65
|
+
*/
|
|
66
|
+
setKey(key) {
|
|
67
|
+
if (typeof key !== 'string' || !key.trim()) {
|
|
68
|
+
throw new Error('Storage key must be a non-empty string');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.__key = key;
|
|
72
|
+
this.__loadData();
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current storage key
|
|
78
|
+
* @returns {string} - The current storage key
|
|
79
|
+
*/
|
|
80
|
+
getKey() {
|
|
81
|
+
return this.__key;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if the storage is supported
|
|
86
|
+
* @returns {boolean} - True if the storage is supported, false otherwise
|
|
87
|
+
*/
|
|
88
|
+
support() {
|
|
89
|
+
return this.__isSupport;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enable/disable encryption
|
|
94
|
+
* @param {boolean} enable - Enable encryption
|
|
95
|
+
* @param {string} key - Encryption key (optional, will generate if not provided)
|
|
96
|
+
*/
|
|
97
|
+
enableEncryption(enable = true, key = null) {
|
|
98
|
+
this.useEncryption = enable;
|
|
99
|
+
|
|
100
|
+
if (enable && !key) {
|
|
101
|
+
// Generate a random encryption key
|
|
102
|
+
this.encryptionKey = this.__generateEncryptionKey();
|
|
103
|
+
} else if (enable && key) {
|
|
104
|
+
this.encryptionKey = key;
|
|
105
|
+
} else {
|
|
106
|
+
this.encryptionKey = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.log(`🔧 StorageService: Encryption ${enable ? 'enabled' : 'disabled'}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ==========================================
|
|
113
|
+
// PUBLIC INSTANCE METHODS - Core Storage Operations
|
|
114
|
+
// ==========================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the data in the storage with optional TTL
|
|
118
|
+
* @param {string|object} key - The key to use for the storage
|
|
119
|
+
* @param {any} value - The value to use for the storage
|
|
120
|
+
* @param {number|null} ttl - Time to live in seconds (null = no expiration)
|
|
121
|
+
* @returns {boolean} - True if the data is set, false otherwise
|
|
122
|
+
*/
|
|
123
|
+
set(key, value, ttl = null) {
|
|
124
|
+
if (!this.__isSupport) return false;
|
|
125
|
+
|
|
126
|
+
if (key === null || key === undefined) {
|
|
127
|
+
throw new Error('Key cannot be null or undefined');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof key === 'object' && key !== null) {
|
|
131
|
+
// Nếu key là object, set nhiều key-value
|
|
132
|
+
let success = true;
|
|
133
|
+
Object.keys(key).forEach(k => {
|
|
134
|
+
const v = key[k];
|
|
135
|
+
if (!this.set(k, v, ttl)) { success = false; }
|
|
136
|
+
})
|
|
137
|
+
return success;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!(typeof key === 'string' || typeof key === 'number')) {
|
|
141
|
+
throw new Error('Key must be a string when setting single value');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate TTL
|
|
145
|
+
if (ttl && (typeof ttl !== 'number' || ttl <= 0)) {
|
|
146
|
+
throw new Error('TTL must be a positive number or null');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const oldValue = this.__data[key];
|
|
150
|
+
|
|
151
|
+
// Create data structure with TTL
|
|
152
|
+
const dataItem = {
|
|
153
|
+
value: value,
|
|
154
|
+
timestamp: Date.now()
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (ttl !== null) {
|
|
158
|
+
dataItem.ttl = ttl * 1000;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.__data[key] = dataItem;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
this.__updateData();
|
|
165
|
+
logger.log(`💾 StorageService: Set ${key}:`, value, ttl ? `(TTL: ${ttl}ms)` : '(no TTL)');
|
|
166
|
+
|
|
167
|
+
this.__createDynamicProperty(key, ttl);
|
|
168
|
+
// Emit events
|
|
169
|
+
this.emit(`set:${key}`, { key, value, oldValue, ttl });
|
|
170
|
+
this.emit('set', { key, value, oldValue, ttl });
|
|
171
|
+
|
|
172
|
+
// Create dynamic property for direct access
|
|
173
|
+
|
|
174
|
+
return true;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// Revert on error
|
|
177
|
+
this.__data[key] = oldValue;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the data from the storage with TTL check
|
|
184
|
+
* @param {string} key - The key to use for the storage
|
|
185
|
+
* @param {any} defaultValue - Default value if key not found or expired
|
|
186
|
+
* @returns {any} - The value of the data or null if expired
|
|
187
|
+
*/
|
|
188
|
+
get(key, defaultValue = null) {
|
|
189
|
+
if (!this.__isSupport) return defaultValue;
|
|
190
|
+
|
|
191
|
+
if (typeof key !== 'string') {
|
|
192
|
+
throw new Error('Key must be a string');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (typeof this.__data[key] === 'undefined') return defaultValue;
|
|
196
|
+
|
|
197
|
+
const dataItem = this.__data[key];
|
|
198
|
+
|
|
199
|
+
// Check if data has TTL structure
|
|
200
|
+
if (dataItem && typeof dataItem === 'object' && 'value' in dataItem) {
|
|
201
|
+
// Check if expired
|
|
202
|
+
if (this.__isExpired(dataItem)) {
|
|
203
|
+
logger.log(`⏰ StorageService: Key ${key} has expired, removing...`);
|
|
204
|
+
this.remove(key);
|
|
205
|
+
return defaultValue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
logger.log(`📖 StorageService: Retrieved ${key}:`, dataItem.value);
|
|
209
|
+
return dataItem.value;
|
|
210
|
+
} else {
|
|
211
|
+
// Legacy data without TTL structure
|
|
212
|
+
logger.log(`📖 StorageService: Retrieved ${key}:`, dataItem);
|
|
213
|
+
return dataItem;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove the data from the storage
|
|
219
|
+
* @param {string} key - The key to use for the storage
|
|
220
|
+
* @returns {boolean} - True if the data is removed, false otherwise
|
|
221
|
+
*/
|
|
222
|
+
remove(key) {
|
|
223
|
+
if (!this.__isSupport) return false;
|
|
224
|
+
|
|
225
|
+
if (typeof key !== 'string') {
|
|
226
|
+
throw new Error('Key must be a string');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (Object.keys(this.__data).length === 0 || typeof this.__data[key] === 'undefined') {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const oldValue = this.__data[key];
|
|
234
|
+
delete this.__data[key];
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
this.__updateData();
|
|
238
|
+
logger.log('🗑️ StorageService: Removed', key);
|
|
239
|
+
|
|
240
|
+
// Emit events
|
|
241
|
+
this.emit('remove', { key, oldValue });
|
|
242
|
+
this.emit(`remove:${key}`, { key, oldValue });
|
|
243
|
+
|
|
244
|
+
// Remove dynamic property
|
|
245
|
+
this.__removeDynamicProperty(key);
|
|
246
|
+
|
|
247
|
+
return true;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Revert on error
|
|
250
|
+
this.__data[key] = oldValue;
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Clear the data from the storage
|
|
257
|
+
*/
|
|
258
|
+
clear() {
|
|
259
|
+
const oldData = { ...this.__data };
|
|
260
|
+
this.__data = {};
|
|
261
|
+
|
|
262
|
+
// Remove all dynamic properties
|
|
263
|
+
if (this.dynamicProperties) {
|
|
264
|
+
for (const key of this.dynamicProperties) {
|
|
265
|
+
this.__removeDynamicProperty(key);
|
|
266
|
+
}
|
|
267
|
+
this.dynamicProperties = [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
this.__updateData();
|
|
272
|
+
logger.log('🧹 StorageService: Cleared all data');
|
|
273
|
+
|
|
274
|
+
// Emit events
|
|
275
|
+
this.emit('clear', { oldData });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// Revert on error
|
|
278
|
+
this.__data = oldData;
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get all data from the storage (excluding expired items)
|
|
285
|
+
* @returns {object} - All valid data in storage
|
|
286
|
+
*/
|
|
287
|
+
getAll() {
|
|
288
|
+
// Clean expired data first
|
|
289
|
+
this.__cleanExpiredData();
|
|
290
|
+
|
|
291
|
+
const result = {};
|
|
292
|
+
for (const [key, dataItem] of Object.entries(this.__data)) {
|
|
293
|
+
if (dataItem && typeof dataItem === 'object' && 'value' in dataItem) {
|
|
294
|
+
result[key] = dataItem.value;
|
|
295
|
+
} else {
|
|
296
|
+
result[key] = dataItem; // Legacy data
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if key exists and is not expired
|
|
304
|
+
* @param {string} key - The key to check
|
|
305
|
+
* @returns {boolean} - True if key exists and not expired, false otherwise
|
|
306
|
+
*/
|
|
307
|
+
has(key) {
|
|
308
|
+
if (typeof key !== 'string') {
|
|
309
|
+
throw new Error('Key must be a string');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (typeof this.__data[key] === 'undefined') return false;
|
|
313
|
+
|
|
314
|
+
const dataItem = this.__data[key];
|
|
315
|
+
|
|
316
|
+
// Check if data has TTL structure
|
|
317
|
+
if (dataItem && typeof dataItem === 'object' && 'value' in dataItem) {
|
|
318
|
+
return !this.__isExpired(dataItem);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return true; // Legacy data without TTL
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get all keys (excluding expired items)
|
|
326
|
+
* @returns {string[]} - Array of all valid keys
|
|
327
|
+
*/
|
|
328
|
+
getAllKeys() {
|
|
329
|
+
// Clean expired data first
|
|
330
|
+
this.__cleanExpiredData();
|
|
331
|
+
|
|
332
|
+
return Object.keys(this.__data);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get storage size (number of valid keys)
|
|
337
|
+
* @returns {number} - Number of valid keys in storage
|
|
338
|
+
*/
|
|
339
|
+
size() {
|
|
340
|
+
// Clean expired data first
|
|
341
|
+
this.__cleanExpiredData();
|
|
342
|
+
|
|
343
|
+
return Object.keys(this.__data).length;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check if storage is empty
|
|
348
|
+
* @returns {boolean} - True if storage is empty, false otherwise
|
|
349
|
+
*/
|
|
350
|
+
isEmpty() {
|
|
351
|
+
return this.size() === 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get storage info
|
|
356
|
+
* @returns {object} - Storage information
|
|
357
|
+
*/
|
|
358
|
+
getInfo() {
|
|
359
|
+
// Clean expired data first
|
|
360
|
+
const expiredRemoved = this.__cleanExpiredData();
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
key: this.__key,
|
|
364
|
+
isSupport: this.__isSupport,
|
|
365
|
+
size: this.size(),
|
|
366
|
+
keys: this.getAllKeys(),
|
|
367
|
+
isEmpty: this.isEmpty(),
|
|
368
|
+
events: this.getEvents(),
|
|
369
|
+
totalListeners: Array.from(this.__listeners.values()).reduce((sum, listeners) => sum + listeners.length, 0),
|
|
370
|
+
isUpdating: this.__isUpdating,
|
|
371
|
+
useEncryption: this.useEncryption,
|
|
372
|
+
hasEncryptionKey: !!this.encryptionKey,
|
|
373
|
+
expiredRemoved: expiredRemoved,
|
|
374
|
+
dynamicProperties: this.dynamicProperties ? this.dynamicProperties.length : 0
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Debug storage
|
|
380
|
+
*/
|
|
381
|
+
debug() {
|
|
382
|
+
logger.log('🔍 StorageService Debug:', this.getInfo());
|
|
383
|
+
|
|
384
|
+
// Show TTL info for each key
|
|
385
|
+
logger.log('📋 TTL Information:');
|
|
386
|
+
for (const [key, dataItem] of Object.entries(this.__data)) {
|
|
387
|
+
if (dataItem && typeof dataItem === 'object' && 'value' in dataItem) {
|
|
388
|
+
const isExpired = this.__isExpired(dataItem);
|
|
389
|
+
const ttl = dataItem.ttl;
|
|
390
|
+
const remaining = dataItem.ttl ? Math.max(0, (dataItem.timestamp + dataItem.ttl) - Date.now()) : null;
|
|
391
|
+
logger.log(` ${key}: ${isExpired ? 'EXPIRED' : 'VALID'}${dataItem.ttl ? ` (TTL: ${dataItem.ttl}ms, Remaining: ${remaining}ms)` : ' (no TTL)'}`);
|
|
392
|
+
} else {
|
|
393
|
+
logger.log(` ${key}: LEGACY (no TTL)`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Show dynamic properties
|
|
398
|
+
logger.log('🔧 Dynamic Properties:', this.dynamicProperties || []);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ==========================================
|
|
402
|
+
// PUBLIC INSTANCE METHODS - TTL Operations
|
|
403
|
+
// ==========================================
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get TTL information for a key
|
|
407
|
+
* @param {string} key - The key to check
|
|
408
|
+
* @returns {object|null} - TTL information or null if not found
|
|
409
|
+
*/
|
|
410
|
+
getTTLInfo(key) {
|
|
411
|
+
if (typeof key !== 'string') {
|
|
412
|
+
throw new Error('Key must be a string');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (typeof this.__data[key] === 'undefined') return null;
|
|
416
|
+
|
|
417
|
+
const dataItem = this.__data[key];
|
|
418
|
+
|
|
419
|
+
if (dataItem && typeof dataItem === 'object' && 'value' in dataItem) {
|
|
420
|
+
const isExpired = this.__isExpired(dataItem);
|
|
421
|
+
const remaining = dataItem.ttl ? Math.max(0, (dataItem.timestamp + dataItem.ttl) - Date.now()) : null;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
hasTTL: !!dataItem.ttl,
|
|
425
|
+
ttl: dataItem.ttl,
|
|
426
|
+
timestamp: dataItem.timestamp,
|
|
427
|
+
isExpired,
|
|
428
|
+
remaining,
|
|
429
|
+
expiryTime: dataItem.ttl ? dataItem.timestamp + dataItem.ttl : null
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return null; // Legacy data without TTL
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Clean all expired data
|
|
438
|
+
* @returns {number} - Number of expired items removed
|
|
439
|
+
*/
|
|
440
|
+
cleanExpired() {
|
|
441
|
+
return this.__cleanExpiredData();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ==========================================
|
|
445
|
+
// PUBLIC INSTANCE METHODS - Import/Export
|
|
446
|
+
// ==========================================
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Export data to JSON string
|
|
450
|
+
* @returns {string} - JSON string of all data
|
|
451
|
+
*/
|
|
452
|
+
export() {
|
|
453
|
+
return JSON.stringify(this.getAll(), null, 2);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Import data from JSON string
|
|
458
|
+
* @param {string} jsonString - JSON string to import
|
|
459
|
+
* @returns {boolean} - True if import successful, false otherwise
|
|
460
|
+
*/
|
|
461
|
+
import(jsonString) {
|
|
462
|
+
if (typeof jsonString !== 'string') {
|
|
463
|
+
throw new Error('JSON string must be a string');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const data = JSON.parse(jsonString);
|
|
468
|
+
const oldData = { ...this.__data };
|
|
469
|
+
|
|
470
|
+
// Convert imported data to TTL structure
|
|
471
|
+
this.__data = {};
|
|
472
|
+
for (const [key, value] of Object.entries(data)) {
|
|
473
|
+
this.__data[key] = {
|
|
474
|
+
value: value,
|
|
475
|
+
timestamp: Date.now()
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.__updateData();
|
|
480
|
+
|
|
481
|
+
logger.log('📥 StorageService: Imported data successfully');
|
|
482
|
+
|
|
483
|
+
// Emit events
|
|
484
|
+
this.emit('import', { oldData, newData: data });
|
|
485
|
+
|
|
486
|
+
return true;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
logger.error('❌ StorageService: Failed to import data:', error);
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Backup current data
|
|
495
|
+
* @returns {object} - Backup data with timestamp
|
|
496
|
+
*/
|
|
497
|
+
backup() {
|
|
498
|
+
return {
|
|
499
|
+
timestamp: Date.now(),
|
|
500
|
+
key: this.__key,
|
|
501
|
+
data: { ...this.__data } // Return copy
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Restore from backup
|
|
507
|
+
* @param {object} backup - Backup data
|
|
508
|
+
* @returns {boolean} - True if restore successful, false otherwise
|
|
509
|
+
*/
|
|
510
|
+
restore(backup) {
|
|
511
|
+
if (!backup || typeof backup !== 'object') {
|
|
512
|
+
throw new Error('Backup must be a valid object');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!backup.data) {
|
|
516
|
+
logger.error('❌ StorageService: Invalid backup data');
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const oldData = { ...this.__data };
|
|
521
|
+
this.__data = { ...backup.data }; // Use copy
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
this.__updateData();
|
|
525
|
+
logger.log('📤 StorageService: Restored from backup:', backup.timestamp);
|
|
526
|
+
|
|
527
|
+
// Emit events
|
|
528
|
+
this.emit('restore', { oldData, newData: backup.data, backup });
|
|
529
|
+
|
|
530
|
+
return true;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
// Revert on error
|
|
533
|
+
this.__data = oldData;
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ==========================================
|
|
539
|
+
// PUBLIC INSTANCE METHODS - Storage Utilities
|
|
540
|
+
// ==========================================
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get storage usage in bytes
|
|
544
|
+
* @returns {number} - Storage usage in bytes
|
|
545
|
+
*/
|
|
546
|
+
getStorageUsage() {
|
|
547
|
+
if (!this.__isSupport) return 0;
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const data = localStorage.getItem(this.__key);
|
|
551
|
+
return data ? new Blob([data]).size : 0;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
logger.error('❌ StorageService: Failed to get storage usage:', error);
|
|
554
|
+
return 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Check if storage is full
|
|
560
|
+
* @returns {boolean} - True if storage is full, false otherwise
|
|
561
|
+
*/
|
|
562
|
+
isStorageFull() {
|
|
563
|
+
try {
|
|
564
|
+
const testKey = '__storage_test__';
|
|
565
|
+
const testValue = 'x'.repeat(1024); // 1KB test
|
|
566
|
+
|
|
567
|
+
localStorage.setItem(testKey, testValue);
|
|
568
|
+
localStorage.removeItem(testKey);
|
|
569
|
+
return false;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return true; // Storage is full
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ==========================================
|
|
576
|
+
// PUBLIC INSTANCE METHODS - Event System
|
|
577
|
+
// ==========================================
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Add event listener
|
|
581
|
+
* @param {string} event - Event name
|
|
582
|
+
* @param {function} callback - Callback function
|
|
583
|
+
* @returns {function} - Unsubscribe function
|
|
584
|
+
*/
|
|
585
|
+
on(event, callback) {
|
|
586
|
+
if (typeof event !== 'string' || !event.trim()) {
|
|
587
|
+
throw new Error('Event name must be a non-empty string');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (typeof callback !== 'function') {
|
|
591
|
+
throw new Error('Callback must be a function');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (!this.__listeners.has(event)) {
|
|
595
|
+
this.__listeners.set(event, []);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this.__listeners.get(event).push(callback);
|
|
599
|
+
logger.log(`🎧 StorageService: Added listener for event: ${event}`);
|
|
600
|
+
|
|
601
|
+
// Return unsubscribe function
|
|
602
|
+
return () => this.off(event, callback);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Remove event listener
|
|
607
|
+
* @param {string} event - Event name
|
|
608
|
+
* @param {function} callback - Callback function
|
|
609
|
+
*/
|
|
610
|
+
off(event, callback) {
|
|
611
|
+
if (!this.__listeners.has(event)) return;
|
|
612
|
+
|
|
613
|
+
const listeners = this.__listeners.get(event);
|
|
614
|
+
const index = listeners.indexOf(callback);
|
|
615
|
+
if (index > -1) {
|
|
616
|
+
listeners.splice(index, 1);
|
|
617
|
+
logger.log(`🎧 StorageService: Removed listener for event: ${event}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Remove all listeners for an event
|
|
623
|
+
* @param {string} event - Event name
|
|
624
|
+
*/
|
|
625
|
+
removeAllListeners(event) {
|
|
626
|
+
if (event) {
|
|
627
|
+
this.__listeners.delete(event);
|
|
628
|
+
logger.log(`🎧 StorageService: Removed all listeners for event: ${event}`);
|
|
629
|
+
} else {
|
|
630
|
+
this.__listeners.clear();
|
|
631
|
+
logger.log('🎧 StorageService: Removed all listeners');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get all registered events
|
|
637
|
+
* @returns {string[]} - Array of event names
|
|
638
|
+
*/
|
|
639
|
+
getEvents() {
|
|
640
|
+
return Array.from(this.__listeners.keys());
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Get listener count for an event
|
|
645
|
+
* @param {string} event - Event name
|
|
646
|
+
* @returns {number} - Number of listeners
|
|
647
|
+
*/
|
|
648
|
+
getListenerCount(event) {
|
|
649
|
+
return this.__listeners.has(event) ? this.__listeners.get(event).length : 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ==========================================
|
|
653
|
+
// PRIVATE METHODS
|
|
654
|
+
// ==========================================
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Generate encryption key
|
|
658
|
+
* @returns {string} - Generated encryption key
|
|
659
|
+
*/
|
|
660
|
+
__generateEncryptionKey() {
|
|
661
|
+
const array = new Uint8Array(32);
|
|
662
|
+
crypto.getRandomValues(array);
|
|
663
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Encrypt data
|
|
668
|
+
* @param {string} data - Data to encrypt
|
|
669
|
+
* @returns {string} - Encrypted data
|
|
670
|
+
*/
|
|
671
|
+
__encrypt(data) {
|
|
672
|
+
if (!this.useEncryption || !this.encryptionKey) {
|
|
673
|
+
return data;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
// Simple XOR encryption (for development purposes)
|
|
678
|
+
// In production, use proper encryption like AES
|
|
679
|
+
let encrypted = '';
|
|
680
|
+
for (let i = 0; i < data.length; i++) {
|
|
681
|
+
const charCode = data.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length);
|
|
682
|
+
encrypted += String.fromCharCode(charCode);
|
|
683
|
+
}
|
|
684
|
+
return btoa(encrypted); // Base64 encode
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error('❌ StorageService: Encryption failed:', error);
|
|
687
|
+
return data;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Decrypt data
|
|
693
|
+
* @param {string} data - Data to decrypt
|
|
694
|
+
* @returns {string} - Decrypted data
|
|
695
|
+
*/
|
|
696
|
+
__decrypt(data) {
|
|
697
|
+
if (!this.useEncryption || !this.encryptionKey) {
|
|
698
|
+
return data;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
// Simple XOR decryption (for development purposes)
|
|
703
|
+
const decoded = atob(data); // Base64 decode
|
|
704
|
+
let decrypted = '';
|
|
705
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
706
|
+
const charCode = decoded.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length);
|
|
707
|
+
decrypted += String.fromCharCode(charCode);
|
|
708
|
+
}
|
|
709
|
+
return decrypted;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.error('❌ StorageService: Decryption failed:', error);
|
|
712
|
+
return data;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Check if data is expired
|
|
718
|
+
* @param {object} dataItem - Data item with timestamp and ttl
|
|
719
|
+
* @returns {boolean} - True if expired, false otherwise
|
|
720
|
+
*/
|
|
721
|
+
__isExpired(dataItem) {
|
|
722
|
+
if (!dataItem || !dataItem.timestamp || !dataItem.ttl) {
|
|
723
|
+
return false; // No TTL, not expired
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const now = Date.now();
|
|
727
|
+
const expiryTime = dataItem.timestamp + dataItem.ttl;
|
|
728
|
+
return now > expiryTime;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Clean expired data
|
|
733
|
+
* @returns {number} - Number of expired items removed
|
|
734
|
+
*/
|
|
735
|
+
__cleanExpiredData() {
|
|
736
|
+
let removed = 0;
|
|
737
|
+
const keysToRemove = [];
|
|
738
|
+
|
|
739
|
+
for (const [key, dataItem] of Object.entries(this.__data)) {
|
|
740
|
+
if (this.__isExpired(dataItem)) {
|
|
741
|
+
keysToRemove.push(key);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
for (const key of keysToRemove) {
|
|
746
|
+
delete this.__data[key];
|
|
747
|
+
removed++;
|
|
748
|
+
logger.log(`🗑️ StorageService: Removed expired key: ${key}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (removed > 0) {
|
|
752
|
+
this.__updateData();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return removed;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Load the data from the storage (private method)
|
|
760
|
+
*/
|
|
761
|
+
__loadData() {
|
|
762
|
+
if (!this.__isSupport) return;
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
const data = localStorage.getItem(this.__key);
|
|
766
|
+
if (data) {
|
|
767
|
+
const decryptedData = this.__decrypt(data);
|
|
768
|
+
this.__data = JSON.parse(decryptedData);
|
|
769
|
+
|
|
770
|
+
// Clean expired data on load
|
|
771
|
+
this.__cleanExpiredData();
|
|
772
|
+
}
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.error('❌ StorageService: Failed to load data:', error);
|
|
775
|
+
this.__data = {};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Update the data in the storage (private method)
|
|
781
|
+
*/
|
|
782
|
+
__updateData() {
|
|
783
|
+
if (!this.__isSupport || this.__isUpdating) return;
|
|
784
|
+
|
|
785
|
+
this.__isUpdating = true;
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
const jsonData = JSON.stringify(this.__data);
|
|
789
|
+
const encryptedData = this.__encrypt(jsonData);
|
|
790
|
+
localStorage.setItem(this.__key, encryptedData);
|
|
791
|
+
logger.log('💾 StorageService: Updated data for key:', this.__key);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
logger.error('❌ StorageService: Failed to update data:', error);
|
|
794
|
+
throw error;
|
|
795
|
+
} finally {
|
|
796
|
+
this.__isUpdating = false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Emit event to all listeners (private method)
|
|
802
|
+
* @param {string} event - Event name
|
|
803
|
+
* @param {any} data - Event data
|
|
804
|
+
*/
|
|
805
|
+
emit(event, data) {
|
|
806
|
+
if (!this.__listeners.has(event)) return;
|
|
807
|
+
|
|
808
|
+
const listeners = this.__listeners.get(event);
|
|
809
|
+
listeners.forEach(callback => {
|
|
810
|
+
try {
|
|
811
|
+
callback(data);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.error(`❌ StorageService: Error in event listener for ${event}:`, error);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Create dynamic property for direct access
|
|
820
|
+
* @param {string} key - The key
|
|
821
|
+
* @param {number|null} ttl - TTL value
|
|
822
|
+
*/
|
|
823
|
+
__createDynamicProperty(key, ttl) {
|
|
824
|
+
try {
|
|
825
|
+
if (!StorageService.privateProperties.includes(key) && !this.dynamicProperties.includes(key)) {
|
|
826
|
+
this.dynamicProperties.push(key);
|
|
827
|
+
Object.defineProperty(this, key, {
|
|
828
|
+
set: (value) => this.set(key, value, ttl),
|
|
829
|
+
get: () => this.get(key),
|
|
830
|
+
configurable: true
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
logger.error('❌ StorageService: Failed to create dynamic property:', error);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Remove dynamic property
|
|
840
|
+
* @param {string} key - The key to remove
|
|
841
|
+
*/
|
|
842
|
+
__removeDynamicProperty(key) {
|
|
843
|
+
try {
|
|
844
|
+
if (this.dynamicProperties && this.dynamicProperties.includes(key)) {
|
|
845
|
+
delete this[key];
|
|
846
|
+
this.dynamicProperties = this.dynamicProperties.filter(k => k !== key);
|
|
847
|
+
}
|
|
848
|
+
} catch (error) {
|
|
849
|
+
logger.error('❌ StorageService: Failed to remove dynamic property:', error);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Export singleton instance
|
|
855
|
+
const storage = StorageService.getInstance('onejs_storage');
|
|
856
|
+
export default storage;
|