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.
Files changed (102) hide show
  1. package/Readme.md +113 -0
  2. package/android/src/main/java/com/strata/storage/EncryptedStorage.java +65 -0
  3. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +147 -0
  4. package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +74 -0
  5. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +256 -0
  6. package/dist/adapters/capacitor/FilesystemAdapter.d.ts +46 -0
  7. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -0
  8. package/dist/adapters/capacitor/FilesystemAdapter.js +162 -0
  9. package/dist/adapters/capacitor/PreferencesAdapter.d.ts +46 -0
  10. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -0
  11. package/dist/adapters/capacitor/PreferencesAdapter.js +162 -0
  12. package/dist/adapters/capacitor/SecureAdapter.d.ts +69 -0
  13. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -0
  14. package/dist/adapters/capacitor/SecureAdapter.js +214 -0
  15. package/dist/adapters/capacitor/SqliteAdapter.d.ts +68 -0
  16. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -0
  17. package/dist/adapters/capacitor/SqliteAdapter.js +277 -0
  18. package/dist/adapters/capacitor/index.d.ts +9 -0
  19. package/dist/adapters/capacitor/index.d.ts.map +1 -0
  20. package/dist/adapters/capacitor/index.js +8 -0
  21. package/dist/adapters/web/CacheAdapter.d.ts +91 -0
  22. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -0
  23. package/dist/adapters/web/CacheAdapter.js +291 -0
  24. package/dist/adapters/web/CookieAdapter.d.ts +77 -0
  25. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -0
  26. package/dist/adapters/web/CookieAdapter.js +260 -0
  27. package/dist/adapters/web/IndexedDBAdapter.d.ts +78 -0
  28. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -0
  29. package/dist/adapters/web/IndexedDBAdapter.js +371 -0
  30. package/dist/adapters/web/LocalStorageAdapter.d.ts +63 -0
  31. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -0
  32. package/dist/adapters/web/LocalStorageAdapter.js +238 -0
  33. package/dist/adapters/web/MemoryAdapter.d.ts +69 -0
  34. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -0
  35. package/dist/adapters/web/MemoryAdapter.js +165 -0
  36. package/dist/adapters/web/SessionStorageAdapter.d.ts +53 -0
  37. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -0
  38. package/dist/adapters/web/SessionStorageAdapter.js +180 -0
  39. package/dist/adapters/web/index.d.ts +10 -0
  40. package/dist/adapters/web/index.d.ts.map +1 -0
  41. package/dist/adapters/web/index.js +9 -0
  42. package/dist/core/AdapterRegistry.d.ts +52 -0
  43. package/dist/core/AdapterRegistry.d.ts.map +1 -0
  44. package/dist/core/AdapterRegistry.js +102 -0
  45. package/dist/core/BaseAdapter.d.ts +79 -0
  46. package/dist/core/BaseAdapter.d.ts.map +1 -0
  47. package/dist/core/BaseAdapter.js +197 -0
  48. package/dist/core/StorageStrategy.d.ts +55 -0
  49. package/dist/core/StorageStrategy.d.ts.map +1 -0
  50. package/dist/core/StorageStrategy.js +199 -0
  51. package/dist/core/Strata.d.ts +122 -0
  52. package/dist/core/Strata.d.ts.map +1 -0
  53. package/dist/core/Strata.js +568 -0
  54. package/dist/features/compression.d.ts +65 -0
  55. package/dist/features/compression.d.ts.map +1 -0
  56. package/dist/features/compression.js +205 -0
  57. package/dist/features/encryption.d.ts +68 -0
  58. package/dist/features/encryption.d.ts.map +1 -0
  59. package/dist/features/encryption.js +172 -0
  60. package/dist/features/migration.d.ts +17 -0
  61. package/dist/features/migration.d.ts.map +1 -0
  62. package/dist/features/migration.js +43 -0
  63. package/dist/features/query.d.ts +75 -0
  64. package/dist/features/query.d.ts.map +1 -0
  65. package/dist/features/query.js +305 -0
  66. package/dist/features/sync.d.ts +87 -0
  67. package/dist/features/sync.d.ts.map +1 -0
  68. package/dist/features/sync.js +233 -0
  69. package/dist/features/ttl.d.ts +124 -0
  70. package/dist/features/ttl.d.ts.map +1 -0
  71. package/dist/features/ttl.js +236 -0
  72. package/dist/index.d.ts +44 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +46 -0
  75. package/dist/package.json +60 -0
  76. package/dist/plugin/definitions.d.ts +219 -0
  77. package/dist/plugin/definitions.d.ts.map +1 -0
  78. package/dist/plugin/definitions.js +5 -0
  79. package/dist/plugin/index.d.ts +8 -0
  80. package/dist/plugin/index.d.ts.map +1 -0
  81. package/dist/plugin/index.js +27 -0
  82. package/dist/plugin/web.d.ts +24 -0
  83. package/dist/plugin/web.d.ts.map +1 -0
  84. package/dist/plugin/web.js +35 -0
  85. package/dist/types/index.d.ts +558 -0
  86. package/dist/types/index.d.ts.map +1 -0
  87. package/dist/types/index.js +14 -0
  88. package/dist/utils/errors.d.ts +92 -0
  89. package/dist/utils/errors.d.ts.map +1 -0
  90. package/dist/utils/errors.js +153 -0
  91. package/dist/utils/index.d.ts +105 -0
  92. package/dist/utils/index.d.ts.map +1 -0
  93. package/dist/utils/index.js +329 -0
  94. package/ios/Plugin/KeychainStorage.swift +87 -0
  95. package/ios/Plugin/SQLiteStorage.swift +167 -0
  96. package/ios/Plugin/StrataStoragePlugin.swift +204 -0
  97. package/ios/Plugin/UserDefaultsStorage.swift +44 -0
  98. package/package.json +126 -0
  99. package/scripts/build.js +52 -0
  100. package/scripts/cli.js +60 -0
  101. package/scripts/configure.js +444 -0
  102. 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
+ }