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,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Engine Feature
|
|
3
|
+
* Zero-dependency implementation of MongoDB-like query operators
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Query engine for advanced data filtering
|
|
7
|
+
*/
|
|
8
|
+
export class QueryEngine {
|
|
9
|
+
/**
|
|
10
|
+
* Check if a value matches a query condition
|
|
11
|
+
*/
|
|
12
|
+
matches(value, condition) {
|
|
13
|
+
// Handle null/undefined values
|
|
14
|
+
if (value === null || value === undefined) {
|
|
15
|
+
return this.matchesNull(value, condition);
|
|
16
|
+
}
|
|
17
|
+
// If condition is not an object, use direct equality
|
|
18
|
+
if (typeof condition !== 'object' || condition === null) {
|
|
19
|
+
return this.equals(value, condition);
|
|
20
|
+
}
|
|
21
|
+
// Handle special operators
|
|
22
|
+
if (this.hasOperators(condition)) {
|
|
23
|
+
return this.matchesOperators(value, condition);
|
|
24
|
+
}
|
|
25
|
+
// Handle nested object matching
|
|
26
|
+
if (typeof value === 'object' && value !== null) {
|
|
27
|
+
return this.matchesObject(value, condition);
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if object has query operators
|
|
33
|
+
*/
|
|
34
|
+
hasOperators(obj) {
|
|
35
|
+
return Object.keys(obj).some((key) => key.startsWith('$'));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Match value against operators
|
|
39
|
+
*/
|
|
40
|
+
matchesOperators(value, operators) {
|
|
41
|
+
for (const [op, operand] of Object.entries(operators)) {
|
|
42
|
+
if (!this.matchesOperator(value, op, operand)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Match single operator
|
|
50
|
+
*/
|
|
51
|
+
matchesOperator(value, operator, operand) {
|
|
52
|
+
switch (operator) {
|
|
53
|
+
case '$eq':
|
|
54
|
+
return this.equals(value, operand);
|
|
55
|
+
case '$ne':
|
|
56
|
+
return !this.equals(value, operand);
|
|
57
|
+
case '$gt':
|
|
58
|
+
return this.compare(value, operand) > 0;
|
|
59
|
+
case '$gte':
|
|
60
|
+
return this.compare(value, operand) >= 0;
|
|
61
|
+
case '$lt':
|
|
62
|
+
return this.compare(value, operand) < 0;
|
|
63
|
+
case '$lte':
|
|
64
|
+
return this.compare(value, operand) <= 0;
|
|
65
|
+
case '$in':
|
|
66
|
+
return Array.isArray(operand) && operand.some((v) => this.equals(value, v));
|
|
67
|
+
case '$nin':
|
|
68
|
+
return Array.isArray(operand) && !operand.some((v) => this.equals(value, v));
|
|
69
|
+
case '$regex':
|
|
70
|
+
return this.matchesRegex(value, operand);
|
|
71
|
+
case '$exists':
|
|
72
|
+
return (value !== undefined) === Boolean(operand);
|
|
73
|
+
case '$type':
|
|
74
|
+
return this.getType(value) === operand;
|
|
75
|
+
case '$and':
|
|
76
|
+
return Array.isArray(operand) && operand.every((cond) => this.matches(value, cond));
|
|
77
|
+
case '$or':
|
|
78
|
+
return Array.isArray(operand) && operand.some((cond) => this.matches(value, cond));
|
|
79
|
+
case '$not':
|
|
80
|
+
return !this.matches(value, operand);
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Match object against condition
|
|
87
|
+
*/
|
|
88
|
+
matchesObject(obj, condition) {
|
|
89
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
90
|
+
if (key.startsWith('$')) {
|
|
91
|
+
// Top-level operator
|
|
92
|
+
if (!this.matchesOperator(obj, key, value)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Property match
|
|
98
|
+
const objValue = this.getNestedValue(obj, key);
|
|
99
|
+
if (!this.matches(objValue, value)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get nested value from object using dot notation
|
|
108
|
+
*/
|
|
109
|
+
getNestedValue(obj, path) {
|
|
110
|
+
const parts = path.split('.');
|
|
111
|
+
let current = obj;
|
|
112
|
+
for (const part of parts) {
|
|
113
|
+
if (current === null || current === undefined) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (typeof current === 'object' && part in current) {
|
|
117
|
+
current = current[part];
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check equality with proper type handling
|
|
127
|
+
*/
|
|
128
|
+
equals(a, b) {
|
|
129
|
+
// Handle null/undefined
|
|
130
|
+
if (a === b)
|
|
131
|
+
return true;
|
|
132
|
+
if (a === null || b === null)
|
|
133
|
+
return false;
|
|
134
|
+
if (a === undefined || b === undefined)
|
|
135
|
+
return false;
|
|
136
|
+
// Handle dates
|
|
137
|
+
if (a instanceof Date && b instanceof Date) {
|
|
138
|
+
return a.getTime() === b.getTime();
|
|
139
|
+
}
|
|
140
|
+
// Handle arrays
|
|
141
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
142
|
+
if (a.length !== b.length)
|
|
143
|
+
return false;
|
|
144
|
+
return a.every((val, i) => this.equals(val, b[i]));
|
|
145
|
+
}
|
|
146
|
+
// Handle objects
|
|
147
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
148
|
+
const aKeys = Object.keys(a);
|
|
149
|
+
const bKeys = Object.keys(b);
|
|
150
|
+
if (aKeys.length !== bKeys.length)
|
|
151
|
+
return false;
|
|
152
|
+
return aKeys.every((key) => this.equals(a[key], b[key]));
|
|
153
|
+
}
|
|
154
|
+
// Default comparison
|
|
155
|
+
return a === b;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Compare values
|
|
159
|
+
*/
|
|
160
|
+
compare(a, b) {
|
|
161
|
+
// Handle null/undefined
|
|
162
|
+
if (a === b)
|
|
163
|
+
return 0;
|
|
164
|
+
if (a === null || a === undefined)
|
|
165
|
+
return -1;
|
|
166
|
+
if (b === null || b === undefined)
|
|
167
|
+
return 1;
|
|
168
|
+
// Handle dates
|
|
169
|
+
if (a instanceof Date && b instanceof Date) {
|
|
170
|
+
return a.getTime() - b.getTime();
|
|
171
|
+
}
|
|
172
|
+
// Handle numbers
|
|
173
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
174
|
+
return a - b;
|
|
175
|
+
}
|
|
176
|
+
// Handle strings
|
|
177
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
178
|
+
return a.localeCompare(b);
|
|
179
|
+
}
|
|
180
|
+
// Type mismatch - convert to string for comparison
|
|
181
|
+
return String(a).localeCompare(String(b));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Match regex pattern
|
|
185
|
+
*/
|
|
186
|
+
matchesRegex(value, pattern) {
|
|
187
|
+
if (typeof value !== 'string')
|
|
188
|
+
return false;
|
|
189
|
+
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
190
|
+
return regex.test(value);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get JavaScript type of value
|
|
194
|
+
*/
|
|
195
|
+
getType(value) {
|
|
196
|
+
if (value === null)
|
|
197
|
+
return 'null';
|
|
198
|
+
if (value === undefined)
|
|
199
|
+
return 'undefined';
|
|
200
|
+
if (Array.isArray(value))
|
|
201
|
+
return 'array';
|
|
202
|
+
if (value instanceof Date)
|
|
203
|
+
return 'date';
|
|
204
|
+
if (value instanceof RegExp)
|
|
205
|
+
return 'regexp';
|
|
206
|
+
return typeof value;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Handle null/undefined matching
|
|
210
|
+
*/
|
|
211
|
+
matchesNull(value, condition) {
|
|
212
|
+
// Direct null/undefined comparison
|
|
213
|
+
if (condition === null || condition === undefined) {
|
|
214
|
+
return value === condition;
|
|
215
|
+
}
|
|
216
|
+
// Handle operators
|
|
217
|
+
if (typeof condition === 'object' && this.hasOperators(condition)) {
|
|
218
|
+
return this.matchesOperators(value, condition);
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Sort array of items by multiple fields
|
|
224
|
+
*/
|
|
225
|
+
sort(items, sortBy) {
|
|
226
|
+
const sorted = [...items];
|
|
227
|
+
const sortKeys = Object.entries(sortBy);
|
|
228
|
+
sorted.sort((a, b) => {
|
|
229
|
+
for (const [key, direction] of sortKeys) {
|
|
230
|
+
const aVal = this.getNestedValue(a, key);
|
|
231
|
+
const bVal = this.getNestedValue(b, key);
|
|
232
|
+
const comparison = this.compare(aVal, bVal);
|
|
233
|
+
if (comparison !== 0) {
|
|
234
|
+
return comparison * direction;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
});
|
|
239
|
+
return sorted;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Project/transform objects based on projection spec
|
|
243
|
+
*/
|
|
244
|
+
project(item, projection) {
|
|
245
|
+
const result = {};
|
|
246
|
+
const isInclusion = Object.values(projection).some((v) => v === 1 || v === true);
|
|
247
|
+
if (isInclusion) {
|
|
248
|
+
// Inclusion mode - only include specified fields
|
|
249
|
+
for (const [key, include] of Object.entries(projection)) {
|
|
250
|
+
if (include === 1 || include === true) {
|
|
251
|
+
const value = this.getNestedValue(item, key);
|
|
252
|
+
if (value !== undefined) {
|
|
253
|
+
this.setNestedValue(result, key, value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Exclusion mode - include all except specified fields
|
|
260
|
+
result.value = { ...item };
|
|
261
|
+
for (const [key, exclude] of Object.entries(projection)) {
|
|
262
|
+
if (exclude === 0 || exclude === false) {
|
|
263
|
+
this.deleteNestedValue(result, key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Set nested value in object using dot notation
|
|
271
|
+
*/
|
|
272
|
+
setNestedValue(obj, path, value) {
|
|
273
|
+
const parts = path.split('.');
|
|
274
|
+
let current = obj;
|
|
275
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
276
|
+
const part = parts[i];
|
|
277
|
+
if (!(part in current) || typeof current[part] !== 'object') {
|
|
278
|
+
current[part] = {};
|
|
279
|
+
}
|
|
280
|
+
current = current[part];
|
|
281
|
+
}
|
|
282
|
+
current[parts[parts.length - 1]] = value;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Delete nested value from object using dot notation
|
|
286
|
+
*/
|
|
287
|
+
deleteNestedValue(obj, path) {
|
|
288
|
+
const parts = path.split('.');
|
|
289
|
+
let current = obj;
|
|
290
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
291
|
+
const part = parts[i];
|
|
292
|
+
if (!(part in current) || typeof current[part] !== 'object') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
current = current[part];
|
|
296
|
+
}
|
|
297
|
+
delete current[parts[parts.length - 1]];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Create a query engine instance
|
|
302
|
+
*/
|
|
303
|
+
export function createQueryEngine() {
|
|
304
|
+
return new QueryEngine();
|
|
305
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tab Synchronization Feature
|
|
3
|
+
* Zero-dependency implementation using BroadcastChannel and storage events
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from '@/utils';
|
|
6
|
+
import type { StorageType, SubscriptionCallback, UnsubscribeFunction } from '@/types';
|
|
7
|
+
/**
|
|
8
|
+
* Sync configuration
|
|
9
|
+
*/
|
|
10
|
+
export interface SyncConfig {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
channelName?: string;
|
|
13
|
+
storages?: StorageType[];
|
|
14
|
+
conflictResolution?: 'latest' | 'merge' | ((conflicts: unknown[]) => unknown);
|
|
15
|
+
debounceMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sync message structure
|
|
19
|
+
*/
|
|
20
|
+
export interface SyncMessage {
|
|
21
|
+
type: 'set' | 'remove' | 'clear';
|
|
22
|
+
key?: string;
|
|
23
|
+
value?: unknown;
|
|
24
|
+
storage: StorageType;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
origin: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cross-tab synchronization manager
|
|
30
|
+
*/
|
|
31
|
+
export declare class SyncManager extends EventEmitter {
|
|
32
|
+
private config;
|
|
33
|
+
private channel?;
|
|
34
|
+
private origin;
|
|
35
|
+
private listeners;
|
|
36
|
+
private debounceTimers;
|
|
37
|
+
constructor(config?: SyncConfig);
|
|
38
|
+
/**
|
|
39
|
+
* Initialize sync manager
|
|
40
|
+
*/
|
|
41
|
+
initialize(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Broadcast a change to other tabs
|
|
44
|
+
*/
|
|
45
|
+
broadcast(message: Omit<SyncMessage, 'origin'>): void;
|
|
46
|
+
/**
|
|
47
|
+
* Subscribe to sync events
|
|
48
|
+
*/
|
|
49
|
+
subscribe(callback: SubscriptionCallback): UnsubscribeFunction;
|
|
50
|
+
/**
|
|
51
|
+
* Close sync manager
|
|
52
|
+
*/
|
|
53
|
+
close(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Check if BroadcastChannel is available
|
|
56
|
+
*/
|
|
57
|
+
private isBroadcastChannelAvailable;
|
|
58
|
+
/**
|
|
59
|
+
* Set up BroadcastChannel for cross-tab communication
|
|
60
|
+
*/
|
|
61
|
+
private setupBroadcastChannel;
|
|
62
|
+
/**
|
|
63
|
+
* Set up storage event listeners for fallback sync
|
|
64
|
+
*/
|
|
65
|
+
private setupStorageEvents;
|
|
66
|
+
/**
|
|
67
|
+
* Send message through available channels
|
|
68
|
+
*/
|
|
69
|
+
private sendMessage;
|
|
70
|
+
/**
|
|
71
|
+
* Emit a change event
|
|
72
|
+
*/
|
|
73
|
+
private emitChange;
|
|
74
|
+
/**
|
|
75
|
+
* Generate unique origin ID
|
|
76
|
+
*/
|
|
77
|
+
private generateOrigin;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve conflicts between values
|
|
80
|
+
*/
|
|
81
|
+
resolveConflict(values: unknown[]): unknown;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a sync manager instance
|
|
85
|
+
*/
|
|
86
|
+
export declare function createSyncManager(config?: SyncConfig): SyncManager;
|
|
87
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/features/sync.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAEV,WAAW,EACX,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAC;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAC,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAuE;IACxF,OAAO,CAAC,cAAc,CAA0C;gBAEpD,MAAM,GAAE,UAAe;IAYnC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,IAAI;IAuBrD;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,mBAAmB;IAY9D;;OAEG;IACH,KAAK,IAAI,IAAI;IAuBb;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAInC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA6B7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA+C1B;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,cAAc;IAItB;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;CAyB5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,WAAW,CAElE"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tab Synchronization Feature
|
|
3
|
+
* Zero-dependency implementation using BroadcastChannel and storage events
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from '@/utils';
|
|
6
|
+
/**
|
|
7
|
+
* Cross-tab synchronization manager
|
|
8
|
+
*/
|
|
9
|
+
export class SyncManager extends EventEmitter {
|
|
10
|
+
config;
|
|
11
|
+
channel;
|
|
12
|
+
origin;
|
|
13
|
+
listeners = new Map();
|
|
14
|
+
debounceTimers = new Map();
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = {
|
|
18
|
+
enabled: config.enabled ?? true,
|
|
19
|
+
channelName: config.channelName || 'strata-sync',
|
|
20
|
+
storages: config.storages || ['localStorage', 'sessionStorage'],
|
|
21
|
+
conflictResolution: config.conflictResolution || 'latest',
|
|
22
|
+
debounceMs: config.debounceMs || 50,
|
|
23
|
+
};
|
|
24
|
+
this.origin = this.generateOrigin();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Initialize sync manager
|
|
28
|
+
*/
|
|
29
|
+
async initialize() {
|
|
30
|
+
if (!this.config.enabled)
|
|
31
|
+
return;
|
|
32
|
+
// Set up BroadcastChannel if available
|
|
33
|
+
if (this.isBroadcastChannelAvailable()) {
|
|
34
|
+
this.setupBroadcastChannel();
|
|
35
|
+
}
|
|
36
|
+
// Set up storage event listeners
|
|
37
|
+
this.setupStorageEvents();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Broadcast a change to other tabs
|
|
41
|
+
*/
|
|
42
|
+
broadcast(message) {
|
|
43
|
+
if (!this.config.enabled)
|
|
44
|
+
return;
|
|
45
|
+
const fullMessage = {
|
|
46
|
+
...message,
|
|
47
|
+
origin: this.origin,
|
|
48
|
+
};
|
|
49
|
+
// Debounce broadcasts
|
|
50
|
+
const debounceKey = `${message.type}:${message.key || '*'}`;
|
|
51
|
+
if (this.debounceTimers.has(debounceKey)) {
|
|
52
|
+
clearTimeout(this.debounceTimers.get(debounceKey));
|
|
53
|
+
}
|
|
54
|
+
this.debounceTimers.set(debounceKey, setTimeout(() => {
|
|
55
|
+
this.sendMessage(fullMessage);
|
|
56
|
+
this.debounceTimers.delete(debounceKey);
|
|
57
|
+
}, this.config.debounceMs));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Subscribe to sync events
|
|
61
|
+
*/
|
|
62
|
+
subscribe(callback) {
|
|
63
|
+
const handler = (change) => {
|
|
64
|
+
callback(change);
|
|
65
|
+
};
|
|
66
|
+
this.on('change', handler);
|
|
67
|
+
return () => {
|
|
68
|
+
this.off('change', handler);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Close sync manager
|
|
73
|
+
*/
|
|
74
|
+
close() {
|
|
75
|
+
// Clear all debounce timers
|
|
76
|
+
for (const timer of this.debounceTimers.values()) {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
this.debounceTimers.clear();
|
|
80
|
+
// Close BroadcastChannel
|
|
81
|
+
if (this.channel) {
|
|
82
|
+
this.channel.close();
|
|
83
|
+
this.channel = undefined;
|
|
84
|
+
}
|
|
85
|
+
// Remove storage event listeners
|
|
86
|
+
this.listeners.forEach((listener) => {
|
|
87
|
+
window.removeEventListener('storage', listener);
|
|
88
|
+
});
|
|
89
|
+
this.listeners.clear();
|
|
90
|
+
// Remove all event listeners
|
|
91
|
+
this.removeAllListeners();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if BroadcastChannel is available
|
|
95
|
+
*/
|
|
96
|
+
isBroadcastChannelAvailable() {
|
|
97
|
+
return typeof window !== 'undefined' && 'BroadcastChannel' in window;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Set up BroadcastChannel for cross-tab communication
|
|
101
|
+
*/
|
|
102
|
+
setupBroadcastChannel() {
|
|
103
|
+
try {
|
|
104
|
+
this.channel = new BroadcastChannel(this.config.channelName);
|
|
105
|
+
this.channel.onmessage = (event) => {
|
|
106
|
+
const message = event.data;
|
|
107
|
+
// Ignore messages from self
|
|
108
|
+
if (message.origin === this.origin)
|
|
109
|
+
return;
|
|
110
|
+
// Emit change event
|
|
111
|
+
this.emitChange({
|
|
112
|
+
key: message.key || '*',
|
|
113
|
+
oldValue: undefined,
|
|
114
|
+
newValue: message.value,
|
|
115
|
+
source: 'remote',
|
|
116
|
+
storage: message.storage,
|
|
117
|
+
timestamp: message.timestamp,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
this.channel.onmessageerror = (event) => {
|
|
121
|
+
console.error('Sync message error:', event);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.warn('Failed to set up BroadcastChannel:', error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Set up storage event listeners for fallback sync
|
|
130
|
+
*/
|
|
131
|
+
setupStorageEvents() {
|
|
132
|
+
if (typeof window === 'undefined')
|
|
133
|
+
return;
|
|
134
|
+
const listener = (event) => {
|
|
135
|
+
// Only process events from other windows
|
|
136
|
+
if (event.storageArea !== window.localStorage)
|
|
137
|
+
return;
|
|
138
|
+
// Parse the storage type from key prefix
|
|
139
|
+
let storageType = 'localStorage';
|
|
140
|
+
if (event.key?.startsWith('strata:session:')) {
|
|
141
|
+
storageType = 'sessionStorage';
|
|
142
|
+
}
|
|
143
|
+
// Check if this storage type is enabled for sync
|
|
144
|
+
if (!this.config.storages.includes(storageType))
|
|
145
|
+
return;
|
|
146
|
+
let oldValue;
|
|
147
|
+
let newValue;
|
|
148
|
+
try {
|
|
149
|
+
oldValue = event.oldValue ? JSON.parse(event.oldValue) : undefined;
|
|
150
|
+
newValue = event.newValue ? JSON.parse(event.newValue) : undefined;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// If parsing fails, use raw values
|
|
154
|
+
oldValue = event.oldValue;
|
|
155
|
+
newValue = event.newValue;
|
|
156
|
+
}
|
|
157
|
+
this.emitChange({
|
|
158
|
+
key: event.key || '*',
|
|
159
|
+
oldValue,
|
|
160
|
+
newValue,
|
|
161
|
+
source: 'remote',
|
|
162
|
+
storage: storageType,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
window.addEventListener('storage', listener);
|
|
167
|
+
// Create a SubscriptionCallback wrapper for the storage event listener
|
|
168
|
+
const subscriptionCallback = (_change) => {
|
|
169
|
+
// This is handled by the listener itself
|
|
170
|
+
};
|
|
171
|
+
this.listeners.set(subscriptionCallback, listener);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Send message through available channels
|
|
175
|
+
*/
|
|
176
|
+
sendMessage(message) {
|
|
177
|
+
// Send through BroadcastChannel if available
|
|
178
|
+
if (this.channel) {
|
|
179
|
+
try {
|
|
180
|
+
this.channel.postMessage(message);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error('Failed to send sync message:', error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Storage events are automatic when using localStorage
|
|
187
|
+
// No need to manually trigger them
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Emit a change event
|
|
191
|
+
*/
|
|
192
|
+
emitChange(change) {
|
|
193
|
+
this.emit('change', change);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Generate unique origin ID
|
|
197
|
+
*/
|
|
198
|
+
generateOrigin() {
|
|
199
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Resolve conflicts between values
|
|
203
|
+
*/
|
|
204
|
+
resolveConflict(values) {
|
|
205
|
+
if (values.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
if (values.length === 1)
|
|
208
|
+
return values[0];
|
|
209
|
+
if (typeof this.config.conflictResolution === 'function') {
|
|
210
|
+
return this.config.conflictResolution(values);
|
|
211
|
+
}
|
|
212
|
+
switch (this.config.conflictResolution) {
|
|
213
|
+
case 'latest':
|
|
214
|
+
// Return the last value (most recent)
|
|
215
|
+
return values[values.length - 1];
|
|
216
|
+
case 'merge':
|
|
217
|
+
// Simple merge for objects
|
|
218
|
+
if (values.every((v) => typeof v === 'object' && v !== null && !Array.isArray(v))) {
|
|
219
|
+
return Object.assign({}, ...values);
|
|
220
|
+
}
|
|
221
|
+
// For non-objects, use latest
|
|
222
|
+
return values[values.length - 1];
|
|
223
|
+
default:
|
|
224
|
+
return values[values.length - 1];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Create a sync manager instance
|
|
230
|
+
*/
|
|
231
|
+
export function createSyncManager(config) {
|
|
232
|
+
return new SyncManager(config);
|
|
233
|
+
}
|