strata-storage 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-INTEGRATION-GUIDE.md +46 -2
- package/LICENSE +21 -35
- package/README.md +15 -13
- package/dist/LICENSE +21 -35
- package/dist/README.md +15 -13
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts +0 -1
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts +0 -1
- package/dist/adapters/capacitor/SecureAdapter.d.ts +0 -1
- package/dist/adapters/capacitor/SqliteAdapter.d.ts +0 -1
- package/dist/adapters/capacitor/index.d.ts +0 -1
- package/dist/adapters/web/CacheAdapter.d.ts +0 -1
- package/dist/adapters/web/CacheAdapter.js +20 -5
- package/dist/adapters/web/CookieAdapter.d.ts +0 -1
- package/dist/adapters/web/CookieAdapter.js +27 -3
- package/dist/adapters/web/IndexedDBAdapter.d.ts +0 -1
- package/dist/adapters/web/IndexedDBAdapter.js +5 -1
- package/dist/adapters/web/LocalStorageAdapter.d.ts +0 -1
- package/dist/adapters/web/LocalStorageAdapter.js +15 -1
- package/dist/adapters/web/MemoryAdapter.d.ts +7 -1
- package/dist/adapters/web/MemoryAdapter.js +22 -5
- package/dist/adapters/web/SessionStorageAdapter.d.ts +0 -1
- package/dist/adapters/web/SessionStorageAdapter.js +14 -1
- package/dist/adapters/web/URLAdapter.d.ts +0 -1
- package/dist/adapters/web/index.d.ts +0 -1
- package/dist/capacitor.d.ts +0 -1
- package/dist/config/support.d.ts +0 -1
- package/dist/core/AdapterRegistry.d.ts +0 -1
- package/dist/core/BaseAdapter.d.ts +0 -1
- package/dist/core/Strata.d.ts +12 -1
- package/dist/core/Strata.js +119 -21
- package/dist/features/compression/index.d.ts +0 -1
- package/dist/features/compression.d.ts +23 -9
- package/dist/features/compression.js +164 -77
- package/dist/features/encryption/index.d.ts +0 -1
- package/dist/features/encryption.d.ts +0 -1
- package/dist/features/integrity.d.ts +0 -1
- package/dist/features/migration.d.ts +0 -1
- package/dist/features/migration.js +7 -2
- package/dist/features/observer/index.d.ts +0 -1
- package/dist/features/observer.d.ts +0 -1
- package/dist/features/query/index.d.ts +0 -1
- package/dist/features/query.d.ts +1 -1
- package/dist/features/query.js +29 -6
- package/dist/features/sync/index.d.ts +0 -1
- package/dist/features/sync.d.ts +0 -1
- package/dist/features/sync.js +18 -3
- package/dist/features/ttl/index.d.ts +0 -1
- package/dist/features/ttl.d.ts +0 -1
- package/dist/features/ttl.js +4 -2
- package/dist/firebase.d.ts +0 -1
- package/dist/firebase.js +39 -2
- package/dist/index.d.ts +2 -3
- package/dist/integrations/angular/index.d.ts +0 -1
- package/dist/integrations/index.d.ts +0 -1
- package/dist/integrations/react/index.d.ts +0 -1
- package/dist/integrations/vue/index.d.ts +0 -1
- package/dist/package.json +9 -4
- package/dist/plugin/definitions.d.ts +0 -1
- package/dist/plugin/index.d.ts +0 -1
- package/dist/plugin/web.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/utils/errors.d.ts +0 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/logger.d.ts +0 -1
- package/package.json +15 -5
- package/scripts/build.js +30 -13
- package/android/AGENTS.md +0 -66
- package/android/CLAUDE.md +0 -122
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +0 -1
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +0 -1
- package/dist/adapters/capacitor/SecureAdapter.d.ts.map +0 -1
- package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +0 -1
- package/dist/adapters/capacitor/index.d.ts.map +0 -1
- package/dist/adapters/web/CacheAdapter.d.ts.map +0 -1
- package/dist/adapters/web/CookieAdapter.d.ts.map +0 -1
- package/dist/adapters/web/IndexedDBAdapter.d.ts.map +0 -1
- package/dist/adapters/web/LocalStorageAdapter.d.ts.map +0 -1
- package/dist/adapters/web/MemoryAdapter.d.ts.map +0 -1
- package/dist/adapters/web/SessionStorageAdapter.d.ts.map +0 -1
- package/dist/adapters/web/URLAdapter.d.ts.map +0 -1
- package/dist/adapters/web/index.d.ts.map +0 -1
- package/dist/capacitor.d.ts.map +0 -1
- package/dist/config/support.d.ts.map +0 -1
- package/dist/core/AdapterRegistry.d.ts.map +0 -1
- package/dist/core/BaseAdapter.d.ts.map +0 -1
- package/dist/core/Strata.d.ts.map +0 -1
- package/dist/features/compression/index.d.ts.map +0 -1
- package/dist/features/compression.d.ts.map +0 -1
- package/dist/features/encryption/index.d.ts.map +0 -1
- package/dist/features/encryption.d.ts.map +0 -1
- package/dist/features/integrity.d.ts.map +0 -1
- package/dist/features/migration.d.ts.map +0 -1
- package/dist/features/observer/index.d.ts.map +0 -1
- package/dist/features/observer.d.ts.map +0 -1
- package/dist/features/query/index.d.ts.map +0 -1
- package/dist/features/query.d.ts.map +0 -1
- package/dist/features/sync/index.d.ts.map +0 -1
- package/dist/features/sync.d.ts.map +0 -1
- package/dist/features/ttl/index.d.ts.map +0 -1
- package/dist/features/ttl.d.ts.map +0 -1
- package/dist/firebase.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/integrations/angular/index.d.ts.map +0 -1
- package/dist/integrations/index.d.ts.map +0 -1
- package/dist/integrations/react/index.d.ts.map +0 -1
- package/dist/integrations/vue/index.d.ts.map +0 -1
- package/dist/plugin/definitions.d.ts.map +0 -1
- package/dist/plugin/index.d.ts.map +0 -1
- package/dist/plugin/web.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/ios/AGENTS.md +0 -59
- package/ios/CLAUDE.md +0 -106
|
@@ -34,8 +34,12 @@ export class CompressionManager {
|
|
|
34
34
|
return data;
|
|
35
35
|
}
|
|
36
36
|
return {
|
|
37
|
+
// Tag the on-disk format so decompress() can pick the right decoder.
|
|
38
|
+
// Older releases wrote `algorithm: 'lz'` with a UTF-16-char codec that
|
|
39
|
+
// corrupted non-Latin1 data; the current byte-LZW format is tagged
|
|
40
|
+
// distinctly and old 'lz' payloads still read via the legacy path.
|
|
37
41
|
data: compressed,
|
|
38
|
-
algorithm:
|
|
42
|
+
algorithm: CompressionManager.FORMAT,
|
|
39
43
|
originalSize,
|
|
40
44
|
compressedSize,
|
|
41
45
|
};
|
|
@@ -52,7 +56,12 @@ export class CompressionManager {
|
|
|
52
56
|
if (!this.isCompressedData(compressedData)) {
|
|
53
57
|
return compressedData;
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
// Route by stored format tag: the current byte-LZW format vs. the legacy
|
|
60
|
+
// UTF-16-char codec written by <= 2.7.0 (algorithm 'lz'), so existing
|
|
61
|
+
// compressed-at-rest values keep decoding after upgrade.
|
|
62
|
+
const decompressed = compressedData.algorithm === CompressionManager.FORMAT
|
|
63
|
+
? this.lzDecompress(compressedData.data)
|
|
64
|
+
: this.legacyLzDecompress(compressedData.data);
|
|
56
65
|
return JSON.parse(decompressed);
|
|
57
66
|
}
|
|
58
67
|
catch (error) {
|
|
@@ -70,120 +79,198 @@ export class CompressionManager {
|
|
|
70
79
|
'originalSize' in data &&
|
|
71
80
|
'compressedSize' in data);
|
|
72
81
|
}
|
|
82
|
+
// Format tag stored in CompressedData.algorithm for the current byte-LZW
|
|
83
|
+
// codec. Payloads written by <= 2.7.0 are tagged 'lz' and decode via the
|
|
84
|
+
// legacy path; anything tagged FORMAT uses the current decoder.
|
|
85
|
+
static FORMAT = 'lz-u8';
|
|
86
|
+
// Freeze the LZW dictionary at 16 bits so every token serializes to exactly
|
|
87
|
+
// two bytes; both encoder and decoder stop growing the dictionary at the same
|
|
88
|
+
// point, staying in sync. 0xFFFF entries is ample for storage-sized values;
|
|
89
|
+
// beyond it, already-learned phrases still compress, just no new ones.
|
|
90
|
+
static LZW_MAX_CODE = 0xffff;
|
|
73
91
|
/**
|
|
74
|
-
* LZ
|
|
92
|
+
* LZ compression over UTF-8 bytes.
|
|
93
|
+
*
|
|
94
|
+
* The alphabet is bytes (0–255), so the literal-vs-dictionary-code
|
|
95
|
+
* discriminator (`< 256`) on decompress is correct for ALL input — including
|
|
96
|
+
* CJK / emoji / symbols whose code points are ≥ 256. (Operating on raw UTF-16
|
|
97
|
+
* chars previously misclassified any such literal as a dictionary code and
|
|
98
|
+
* silently corrupted the stored value, or threw on read.)
|
|
75
99
|
*/
|
|
76
100
|
lzCompress(uncompressed) {
|
|
77
|
-
|
|
101
|
+
const bytes = new TextEncoder().encode(uncompressed);
|
|
102
|
+
if (bytes.length === 0)
|
|
78
103
|
return '';
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let phrase =
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
phrase
|
|
104
|
+
const dictionary = new Map();
|
|
105
|
+
let nextCode = 256;
|
|
106
|
+
const tokens = [];
|
|
107
|
+
let phrase = String.fromCharCode(bytes[0]); // byte-string (each char 0–255)
|
|
108
|
+
for (let i = 1; i < bytes.length; i++) {
|
|
109
|
+
const current = String.fromCharCode(bytes[i]);
|
|
110
|
+
const combined = phrase + current;
|
|
111
|
+
if (dictionary.has(combined)) {
|
|
112
|
+
phrase = combined;
|
|
88
113
|
}
|
|
89
114
|
else {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
// `phrase` is always either a single byte (literal) or an existing
|
|
116
|
+
// dictionary entry, so the lookup below is always defined.
|
|
117
|
+
tokens.push(phrase.length === 1 ? phrase.charCodeAt(0) : dictionary.get(phrase));
|
|
118
|
+
if (nextCode <= CompressionManager.LZW_MAX_CODE) {
|
|
119
|
+
dictionary.set(combined, nextCode++);
|
|
95
120
|
}
|
|
96
|
-
|
|
97
|
-
code++;
|
|
98
|
-
phrase = currentChar;
|
|
121
|
+
phrase = current;
|
|
99
122
|
}
|
|
100
123
|
}
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
tokens.push(phrase.length === 1 ? phrase.charCodeAt(0) : dictionary.get(phrase));
|
|
125
|
+
// Serialize each token as a 16-bit big-endian value, then base64.
|
|
126
|
+
const out = new Uint8Array(tokens.length * 2);
|
|
127
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
128
|
+
out[i * 2] = (tokens[i] >> 8) & 0xff;
|
|
129
|
+
out[i * 2 + 1] = tokens[i] & 0xff;
|
|
103
130
|
}
|
|
104
|
-
|
|
105
|
-
out.push(phrase);
|
|
106
|
-
}
|
|
107
|
-
return this.encodeToBase64(out.join(''));
|
|
131
|
+
return this.bytesToBase64(out);
|
|
108
132
|
}
|
|
109
133
|
/**
|
|
110
|
-
* LZ
|
|
134
|
+
* LZ decompression — inverse of {@link lzCompress}, reconstructing UTF-8 bytes
|
|
135
|
+
* then decoding them back to the original string.
|
|
111
136
|
*/
|
|
112
137
|
lzDecompress(compressed) {
|
|
113
|
-
if (compressed
|
|
138
|
+
if (!compressed)
|
|
114
139
|
return '';
|
|
115
|
-
|
|
140
|
+
const inBytes = this.base64ToBytes(compressed);
|
|
141
|
+
const tokenCount = inBytes.length >> 1;
|
|
142
|
+
if (tokenCount === 0)
|
|
116
143
|
return '';
|
|
117
|
-
const decoded = this.decodeFromBase64(compressed);
|
|
118
144
|
const dictionary = new Map();
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let
|
|
123
|
-
|
|
124
|
-
for (let i = 1; i <
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
let nextCode = 256;
|
|
146
|
+
const readToken = (i) => (inBytes[i * 2] << 8) | inBytes[i * 2 + 1];
|
|
147
|
+
// The first token is always a literal byte (the dictionary starts empty).
|
|
148
|
+
let oldPhrase = String.fromCharCode(readToken(0));
|
|
149
|
+
const out = [oldPhrase];
|
|
150
|
+
for (let i = 1; i < tokenCount; i++) {
|
|
151
|
+
const code = readToken(i);
|
|
152
|
+
let phrase;
|
|
153
|
+
if (code < 256) {
|
|
154
|
+
phrase = String.fromCharCode(code);
|
|
155
|
+
}
|
|
156
|
+
else if (dictionary.has(code)) {
|
|
157
|
+
phrase = dictionary.get(code);
|
|
128
158
|
}
|
|
129
159
|
else {
|
|
130
|
-
|
|
160
|
+
// Classic LZW edge: the code equals the entry we are about to define.
|
|
161
|
+
phrase = oldPhrase + oldPhrase.charAt(0);
|
|
131
162
|
}
|
|
132
163
|
out.push(phrase);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
if (nextCode <= CompressionManager.LZW_MAX_CODE) {
|
|
165
|
+
dictionary.set(nextCode++, oldPhrase + phrase.charAt(0));
|
|
166
|
+
}
|
|
136
167
|
oldPhrase = phrase;
|
|
137
168
|
}
|
|
138
|
-
|
|
169
|
+
// `out` holds byte-strings; rebuild the byte sequence and UTF-8 decode it.
|
|
170
|
+
const joined = out.join('');
|
|
171
|
+
const bytes = new Uint8Array(joined.length);
|
|
172
|
+
for (let i = 0; i < joined.length; i++) {
|
|
173
|
+
bytes[i] = joined.charCodeAt(i) & 0xff;
|
|
174
|
+
}
|
|
175
|
+
return new TextDecoder().decode(bytes);
|
|
139
176
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
encodeToBase64(input) {
|
|
177
|
+
static B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
178
|
+
/** Base64-encode raw bytes (btoa when available, else a pure-JS fallback). */
|
|
179
|
+
bytesToBase64(bytes) {
|
|
144
180
|
if (typeof btoa !== 'undefined') {
|
|
145
|
-
|
|
146
|
-
|
|
181
|
+
let binary = '';
|
|
182
|
+
const chunk = 0x8000;
|
|
183
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
184
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
185
|
+
}
|
|
186
|
+
return btoa(binary);
|
|
147
187
|
}
|
|
148
|
-
|
|
149
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
188
|
+
const chars = CompressionManager.B64_CHARS;
|
|
150
189
|
let result = '';
|
|
151
|
-
let i = 0;
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const c = i < input.length ? input.charCodeAt(i++) : 0;
|
|
190
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
191
|
+
const a = bytes[i];
|
|
192
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
193
|
+
const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
156
194
|
const bitmap = (a << 16) | (b << 8) | c;
|
|
157
195
|
result += chars.charAt((bitmap >> 18) & 63);
|
|
158
196
|
result += chars.charAt((bitmap >> 12) & 63);
|
|
159
|
-
result += i
|
|
160
|
-
result += i
|
|
197
|
+
result += i + 1 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
|
|
198
|
+
result += i + 2 < bytes.length ? chars.charAt(bitmap & 63) : '=';
|
|
161
199
|
}
|
|
162
200
|
return result;
|
|
163
201
|
}
|
|
202
|
+
/** Decode a base64 string back to raw bytes (atob when available, else pure-JS). */
|
|
203
|
+
base64ToBytes(input) {
|
|
204
|
+
if (typeof atob !== 'undefined') {
|
|
205
|
+
const binary = atob(input);
|
|
206
|
+
const bytes = new Uint8Array(binary.length);
|
|
207
|
+
for (let i = 0; i < binary.length; i++) {
|
|
208
|
+
bytes[i] = binary.charCodeAt(i) & 0xff;
|
|
209
|
+
}
|
|
210
|
+
return bytes;
|
|
211
|
+
}
|
|
212
|
+
const chars = CompressionManager.B64_CHARS;
|
|
213
|
+
const clean = input.replace(/[^A-Za-z0-9+/]/g, '');
|
|
214
|
+
const bytes = [];
|
|
215
|
+
for (let i = 0; i < clean.length; i += 4) {
|
|
216
|
+
const e1 = chars.indexOf(clean.charAt(i));
|
|
217
|
+
const e2 = chars.indexOf(clean.charAt(i + 1));
|
|
218
|
+
const e3 = chars.indexOf(clean.charAt(i + 2));
|
|
219
|
+
const e4 = chars.indexOf(clean.charAt(i + 3));
|
|
220
|
+
const bitmap = (e1 << 18) | (Math.max(e2, 0) << 12) | (Math.max(e3, 0) << 6) | Math.max(e4, 0);
|
|
221
|
+
bytes.push((bitmap >> 16) & 255);
|
|
222
|
+
if (e3 !== -1)
|
|
223
|
+
bytes.push((bitmap >> 8) & 255);
|
|
224
|
+
if (e4 !== -1)
|
|
225
|
+
bytes.push(bitmap & 255);
|
|
226
|
+
}
|
|
227
|
+
return new Uint8Array(bytes);
|
|
228
|
+
}
|
|
164
229
|
/**
|
|
165
|
-
*
|
|
230
|
+
* Legacy decoder for payloads written by <= 2.7.0 (algorithm 'lz'): the
|
|
231
|
+
* original UTF-16-char LZW over the raw string with a `decodeURIComponent`
|
|
232
|
+
* base64 layer. Kept read-only so existing compressed-at-rest values still
|
|
233
|
+
* decode after upgrading to the current byte-LZW codec. (That codec corrupted
|
|
234
|
+
* non-Latin1 input, so legacy payloads are Latin1 in practice.)
|
|
166
235
|
*/
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
legacyLzDecompress(compressed) {
|
|
237
|
+
if (!compressed)
|
|
238
|
+
return '';
|
|
239
|
+
const decoded = typeof atob !== 'undefined'
|
|
240
|
+
? decodeURIComponent(escape(atob(compressed)))
|
|
241
|
+
: this.legacyDecodeFromBase64(compressed);
|
|
242
|
+
const dictionary = new Map();
|
|
243
|
+
let currentChar = decoded.charAt(0);
|
|
244
|
+
let oldPhrase = currentChar;
|
|
245
|
+
const out = [currentChar];
|
|
246
|
+
let code = 256;
|
|
247
|
+
for (let i = 1; i < decoded.length; i++) {
|
|
248
|
+
const currentCode = decoded.charCodeAt(i);
|
|
249
|
+
const phrase = currentCode < 256
|
|
250
|
+
? decoded.charAt(i)
|
|
251
|
+
: dictionary.get(currentCode) || oldPhrase + currentChar;
|
|
252
|
+
out.push(phrase);
|
|
253
|
+
currentChar = phrase.charAt(0);
|
|
254
|
+
dictionary.set(code, oldPhrase + currentChar);
|
|
255
|
+
code++;
|
|
256
|
+
oldPhrase = phrase;
|
|
171
257
|
}
|
|
172
|
-
|
|
173
|
-
|
|
258
|
+
return out.join('');
|
|
259
|
+
}
|
|
260
|
+
legacyDecodeFromBase64(input) {
|
|
261
|
+
const chars = `${CompressionManager.B64_CHARS}=`;
|
|
174
262
|
let result = '';
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const bitmap = (encoded1 << 18) | (encoded2 << 12) | (encoded3 << 6) | encoded4;
|
|
263
|
+
const clean = input.replace(/[^A-Za-z0-9+/]/g, '');
|
|
264
|
+
for (let i = 0; i < clean.length; i += 4) {
|
|
265
|
+
const e1 = chars.indexOf(clean.charAt(i));
|
|
266
|
+
const e2 = chars.indexOf(clean.charAt(i + 1));
|
|
267
|
+
const e3 = chars.indexOf(clean.charAt(i + 2));
|
|
268
|
+
const e4 = chars.indexOf(clean.charAt(i + 3));
|
|
269
|
+
const bitmap = (e1 << 18) | (e2 << 12) | (e3 << 6) | e4;
|
|
183
270
|
result += String.fromCharCode((bitmap >> 16) & 255);
|
|
184
|
-
if (
|
|
271
|
+
if (e3 !== 64 && e3 !== -1)
|
|
185
272
|
result += String.fromCharCode((bitmap >> 8) & 255);
|
|
186
|
-
if (
|
|
273
|
+
if (e4 !== 64 && e4 !== -1)
|
|
187
274
|
result += String.fromCharCode(bitmap & 255);
|
|
188
275
|
}
|
|
189
276
|
return result;
|
|
@@ -13,4 +13,3 @@ export declare function computeChecksum(value: unknown): string;
|
|
|
13
13
|
* stored (nothing to verify) so this is safe to call unconditionally.
|
|
14
14
|
*/
|
|
15
15
|
export declare function verifyChecksum(value: unknown, expected: string | undefined): boolean;
|
|
16
|
-
//# sourceMappingURL=integrity.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Migration utilities for storage upgrades
|
|
3
3
|
*/
|
|
4
|
+
import { MigrationError } from "../utils/errors.js";
|
|
4
5
|
export class MigrationManager {
|
|
5
6
|
migrations = [];
|
|
6
7
|
register(migration) {
|
|
@@ -22,9 +23,13 @@ export class MigrationManager {
|
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
25
|
for (const migration of migrationsToRun.reverse()) {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// An irreversible migration must NOT silently lower the stored version
|
|
27
|
+
// (that would claim a rollback that never reverted the data). Fail
|
|
28
|
+
// before touching anything so the version stays consistent with data.
|
|
29
|
+
if (!migration.down) {
|
|
30
|
+
throw new MigrationError(`Cannot roll back migration ${migration.version}: it has no down() and is irreversible.`);
|
|
27
31
|
}
|
|
32
|
+
await migration.down(adapter);
|
|
28
33
|
await this.setVersion(adapter, migration.version - 1);
|
|
29
34
|
}
|
|
30
35
|
}
|
package/dist/features/query.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare class QueryEngine {
|
|
|
68
68
|
/**
|
|
69
69
|
* Set nested value in object using dot notation
|
|
70
70
|
*/
|
|
71
|
+
private static readonly FORBIDDEN_KEYS;
|
|
71
72
|
private setNestedValue;
|
|
72
73
|
/**
|
|
73
74
|
* Delete nested value from object using dot notation
|
|
@@ -78,4 +79,3 @@ export declare class QueryEngine {
|
|
|
78
79
|
* Create a query engine instance
|
|
79
80
|
*/
|
|
80
81
|
export declare function createQueryEngine(): QueryEngine;
|
|
81
|
-
//# sourceMappingURL=query.d.ts.map
|
package/dist/features/query.js
CHANGED
|
@@ -73,7 +73,15 @@ export class QueryEngine {
|
|
|
73
73
|
// Otherwise check if value equals any operand value
|
|
74
74
|
return operand.some((v) => this.equals(value, v));
|
|
75
75
|
case '$nin':
|
|
76
|
-
|
|
76
|
+
if (!Array.isArray(operand))
|
|
77
|
+
return false;
|
|
78
|
+
// Mirror $in: for an array field, "not in" means none of its items
|
|
79
|
+
// match any operand value (the previous code compared the whole array
|
|
80
|
+
// to each scalar operand, giving wrong results for array fields).
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
return !operand.some((v) => value.some((item) => this.equals(item, v)));
|
|
83
|
+
}
|
|
84
|
+
return !operand.some((v) => this.equals(value, v));
|
|
77
85
|
case '$regex':
|
|
78
86
|
return this.matchesRegex(value, operand);
|
|
79
87
|
case '$exists':
|
|
@@ -272,8 +280,10 @@ export class QueryEngine {
|
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
else {
|
|
275
|
-
// Exclusion mode - include all except specified fields
|
|
276
|
-
result
|
|
283
|
+
// Exclusion mode - include all except specified fields. Spread the item's
|
|
284
|
+
// own fields directly onto result (NOT under a `value` wrapper, which left
|
|
285
|
+
// the excluded keys unreachable and returned the whole item unchanged).
|
|
286
|
+
Object.assign(result, item);
|
|
277
287
|
for (const [key, exclude] of Object.entries(projection)) {
|
|
278
288
|
if (exclude === 0 || exclude === false) {
|
|
279
289
|
this.deleteNestedValue(result, key);
|
|
@@ -285,17 +295,28 @@ export class QueryEngine {
|
|
|
285
295
|
/**
|
|
286
296
|
* Set nested value in object using dot notation
|
|
287
297
|
*/
|
|
298
|
+
static FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
288
299
|
setNestedValue(obj, path, value) {
|
|
289
300
|
const parts = path.split('.');
|
|
290
301
|
let current = obj;
|
|
291
302
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
292
303
|
const part = parts[i];
|
|
293
|
-
|
|
304
|
+
// Refuse prototype-polluting paths and use own-property checks only — `in`
|
|
305
|
+
// walks the prototype chain, so a '__proto__.x' path could reach
|
|
306
|
+
// Object.prototype.
|
|
307
|
+
if (QueryEngine.FORBIDDEN_KEYS.has(part))
|
|
308
|
+
return;
|
|
309
|
+
if (!Object.prototype.hasOwnProperty.call(current, part) ||
|
|
310
|
+
typeof current[part] !== 'object' ||
|
|
311
|
+
current[part] === null) {
|
|
294
312
|
current[part] = {};
|
|
295
313
|
}
|
|
296
314
|
current = current[part];
|
|
297
315
|
}
|
|
298
|
-
|
|
316
|
+
const last = parts[parts.length - 1];
|
|
317
|
+
if (QueryEngine.FORBIDDEN_KEYS.has(last))
|
|
318
|
+
return;
|
|
319
|
+
current[last] = value;
|
|
299
320
|
}
|
|
300
321
|
/**
|
|
301
322
|
* Delete nested value from object using dot notation
|
|
@@ -305,7 +326,9 @@ export class QueryEngine {
|
|
|
305
326
|
let current = obj;
|
|
306
327
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
307
328
|
const part = parts[i];
|
|
308
|
-
if (!(
|
|
329
|
+
if (!Object.prototype.hasOwnProperty.call(current, part) ||
|
|
330
|
+
typeof current[part] !== 'object' ||
|
|
331
|
+
current[part] === null) {
|
|
309
332
|
return;
|
|
310
333
|
}
|
|
311
334
|
current = current[part];
|
package/dist/features/sync.d.ts
CHANGED
package/dist/features/sync.js
CHANGED
|
@@ -247,9 +247,24 @@ export class SyncManager extends EventEmitter {
|
|
|
247
247
|
return this.config.conflictResolution(values);
|
|
248
248
|
}
|
|
249
249
|
switch (this.config.conflictResolution) {
|
|
250
|
-
case 'latest':
|
|
251
|
-
//
|
|
252
|
-
|
|
250
|
+
case 'latest': {
|
|
251
|
+
// Most-recent-wins by the StorageValue `updated` timestamp so two tabs
|
|
252
|
+
// with concurrent same-key writes converge on the genuinely newest
|
|
253
|
+
// value, instead of each applying the other's (last-received-wins, which
|
|
254
|
+
// diverges and lets a delayed older message clobber a newer local
|
|
255
|
+
// write). Falls back to positional-last when values carry no timestamp.
|
|
256
|
+
let newest = values[values.length - 1];
|
|
257
|
+
let newestTime = isStorageValue(newest) ? newest.updated : undefined;
|
|
258
|
+
for (const candidate of values) {
|
|
259
|
+
if (!isStorageValue(candidate))
|
|
260
|
+
continue;
|
|
261
|
+
if (typeof newestTime !== 'number' || candidate.updated >= newestTime) {
|
|
262
|
+
newest = candidate;
|
|
263
|
+
newestTime = candidate.updated;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return newest;
|
|
267
|
+
}
|
|
253
268
|
case 'merge':
|
|
254
269
|
if (values.every(isStorageValue)) {
|
|
255
270
|
const wrappers = values;
|
package/dist/features/ttl.d.ts
CHANGED
package/dist/features/ttl.js
CHANGED
|
@@ -32,10 +32,12 @@ export class TTLManager extends EventEmitter {
|
|
|
32
32
|
const time = options.expireAt instanceof Date ? options.expireAt.getTime() : options.expireAt;
|
|
33
33
|
return time > Date.now() ? time : undefined;
|
|
34
34
|
}
|
|
35
|
-
// Expire after a certain date
|
|
35
|
+
// Expire after a certain date — documented as an alias of expireAt, so it
|
|
36
|
+
// gets the same past-guard (a past time yields no expiry rather than an
|
|
37
|
+
// entry that is dead on arrival).
|
|
36
38
|
if (options.expireAfter) {
|
|
37
39
|
const afterTime = options.expireAfter instanceof Date ? options.expireAfter.getTime() : options.expireAfter;
|
|
38
|
-
return afterTime;
|
|
40
|
+
return afterTime > Date.now() ? afterTime : undefined;
|
|
39
41
|
}
|
|
40
42
|
// TTL from now
|
|
41
43
|
if (options.ttl) {
|
package/dist/firebase.d.ts
CHANGED
package/dist/firebase.js
CHANGED
|
@@ -6,6 +6,35 @@ function isStorageValue(value) {
|
|
|
6
6
|
'created' in value &&
|
|
7
7
|
'updated' in value);
|
|
8
8
|
}
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
if (typeof value !== 'object' || value === null)
|
|
11
|
+
return false;
|
|
12
|
+
const proto = Object.getPrototypeOf(value);
|
|
13
|
+
return proto === Object.prototype || proto === null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Recursively drop `undefined`-valued keys. Firestore `setDoc()` and Realtime
|
|
17
|
+
* Database `set()` both throw on `undefined` field values, and the StorageValue
|
|
18
|
+
* wrapper Strata builds always carries `expires`/`tags`/`metadata` keys that are
|
|
19
|
+
* `undefined` for a plain write — so writing it verbatim breaks the happy path.
|
|
20
|
+
* Only plain objects/arrays are traversed, so Dates and other class instances
|
|
21
|
+
* are preserved as-is.
|
|
22
|
+
*/
|
|
23
|
+
function stripUndefined(input) {
|
|
24
|
+
if (Array.isArray(input)) {
|
|
25
|
+
return input.map((item) => stripUndefined(item));
|
|
26
|
+
}
|
|
27
|
+
if (isPlainObject(input)) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const [key, val] of Object.entries(input)) {
|
|
30
|
+
if (val === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
out[key] = stripUndefined(val);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
return input;
|
|
37
|
+
}
|
|
9
38
|
function normalizeRemoteValue(value) {
|
|
10
39
|
if (!value)
|
|
11
40
|
return null;
|
|
@@ -71,7 +100,7 @@ export async function enableFirebaseSync(storage, config) {
|
|
|
71
100
|
},
|
|
72
101
|
async set(key, value) {
|
|
73
102
|
const docRef = doc(db, collectionName, key);
|
|
74
|
-
await setDoc(docRef, value);
|
|
103
|
+
await setDoc(docRef, stripUndefined(value));
|
|
75
104
|
},
|
|
76
105
|
async remove(key) {
|
|
77
106
|
const docRef = doc(db, collectionName, key);
|
|
@@ -168,7 +197,7 @@ export async function enableFirebaseSync(storage, config) {
|
|
|
168
197
|
return snapshot.exists() ? normalizeRemoteValue(snapshot.val()) : null;
|
|
169
198
|
},
|
|
170
199
|
async set(key, value) {
|
|
171
|
-
await set(ref(db, `strata-storage/${key}`), value);
|
|
200
|
+
await set(ref(db, `strata-storage/${key}`), stripUndefined(value));
|
|
172
201
|
},
|
|
173
202
|
async remove(key) {
|
|
174
203
|
await remove(ref(db, `strata-storage/${key}`));
|
|
@@ -230,6 +259,14 @@ export async function enableFirebaseSync(storage, config) {
|
|
|
230
259
|
// Cast to unknown first to bypass type checking for custom adapter
|
|
231
260
|
storage.registerAdapter(realtimeAdapter);
|
|
232
261
|
}
|
|
262
|
+
// Attach the freshly registered adapters to the active set so they join
|
|
263
|
+
// multi-adapter operations (subscribe/keys/clear/size) immediately — not
|
|
264
|
+
// only after an explicit `{ storage: 'firestore' }` call. Mirrors
|
|
265
|
+
// registerCapacitorAdapters(). Without this, a top-level subscribe() issued
|
|
266
|
+
// before any firestore op never wires up the onSnapshot listener.
|
|
267
|
+
if (config.firestore || config.realtimeDatabase) {
|
|
268
|
+
await storage.refreshAdapters();
|
|
269
|
+
}
|
|
233
270
|
// Firebase sync enabled successfully
|
|
234
271
|
}
|
|
235
272
|
catch (error) {
|
package/dist/index.d.ts
CHANGED
|
@@ -18,10 +18,10 @@ export { computeChecksum, verifyChecksum } from "./features/integrity.js";
|
|
|
18
18
|
export { MigrationManager } from "./features/migration.js";
|
|
19
19
|
export type { Migration } from "./features/migration.js";
|
|
20
20
|
export { StrataError, StorageError, IntegrityError, QuotaExceededError, EncryptionError, CompressionError, SerializationError, ValidationError, NotSupportedError, AdapterNotAvailableError, } from "./utils/errors.js";
|
|
21
|
-
export type { StorageType, StorageOptions, StorageValue, StorageAdapter, AdapterConfig, QueryOptions, SyncConfig, EncryptionConfig, CompressionConfig, ObserverCallback, StorageEvent, StorageCapabilities, StorageMetadata, TTLConfig, StrataConfig, StorageChange, SubscriptionCallback, UnsubscribeFunction, } from "./types.js";
|
|
21
|
+
export type { StorageType, StorageOptions, StorageValue, StorageAdapter, AdapterConfig, QueryOptions, SyncConfig, EncryptionConfig, CompressionConfig, ObserverCallback, StorageEvent, StorageCapabilities, StorageMetadata, TTLConfig, StrataConfig, StorageChange, SubscriptionCallback, UnsubscribeFunction, } from "./types/index.js";
|
|
22
22
|
export { isValidKey, isValidValue, serializeValue, deserializeValue, generateId, createError, retry, debounce, throttle, } from "./utils/index.js";
|
|
23
23
|
import { Strata } from "./core/Strata.js";
|
|
24
|
-
import type { StrataConfig } from "./types.js";
|
|
24
|
+
import type { StrataConfig } from "./types/index.js";
|
|
25
25
|
/**
|
|
26
26
|
* Register the standard web storage adapters on a Strata instance: memory,
|
|
27
27
|
* localStorage, sessionStorage, indexedDB, cookies, and cache. Exported so any
|
|
@@ -55,4 +55,3 @@ declare const storage: Strata;
|
|
|
55
55
|
declare function ensureInitialized(): Promise<void>;
|
|
56
56
|
export { storage, ensureInitialized };
|
|
57
57
|
export default storage;
|
|
58
|
-
//# sourceMappingURL=index.d.ts.map
|