react-native-nitro-storage 0.7.0 → 0.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/CHANGELOG.md +32 -0
- package/README.md +20 -9
- package/SECURITY.md +2 -2
- package/docs/api-reference.md +11 -1
- package/docs/batch-transactions-migrations.md +8 -2
- package/docs/benchmarks.md +19 -15
- package/docs/web-backends.md +18 -1
- package/ios/IOSStorageAdapterCpp.mm +162 -17
- package/lib/commonjs/capabilities.js +26 -0
- package/lib/commonjs/capabilities.js.map +1 -0
- package/lib/commonjs/core/durability.js +137 -0
- package/lib/commonjs/core/durability.js.map +1 -0
- package/lib/commonjs/core/metrics.js +91 -0
- package/lib/commonjs/core/metrics.js.map +1 -0
- package/lib/commonjs/index.js +47 -22
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +118 -66
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +73 -23
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/internal.js +27 -4
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +2 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +217 -233
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-events.js.map +1 -1
- package/lib/commonjs/storage-platform.js +6 -0
- package/lib/commonjs/storage-platform.js.map +1 -0
- package/lib/commonjs/storage-runtime.js +3 -21
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +19 -4
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/commonjs/web-backend-contract.js +27 -0
- package/lib/commonjs/web-backend-contract.js.map +1 -0
- package/lib/module/capabilities.js +21 -0
- package/lib/module/capabilities.js.map +1 -0
- package/lib/module/core/durability.js +133 -0
- package/lib/module/core/durability.js.map +1 -0
- package/lib/module/core/metrics.js +87 -0
- package/lib/module/core/metrics.js.map +1 -0
- package/lib/module/index.js +48 -23
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +103 -64
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +73 -23
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/internal.js +24 -3
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +2 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +218 -234
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-events.js.map +1 -1
- package/lib/module/storage-platform.js +4 -0
- package/lib/module/storage-platform.js.map +1 -0
- package/lib/module/storage-runtime.js +3 -21
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +5 -3
- package/lib/module/testing.js.map +1 -1
- package/lib/module/web-backend-contract.js +22 -0
- package/lib/module/web-backend-contract.js.map +1 -0
- package/lib/typescript/capabilities.d.ts +11 -0
- package/lib/typescript/capabilities.d.ts.map +1 -0
- package/lib/typescript/core/durability.d.ts +27 -0
- package/lib/typescript/core/durability.d.ts.map +1 -0
- package/lib/typescript/core/metrics.d.ts +12 -0
- package/lib/typescript/core/metrics.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +5 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +8 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +3 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +2 -1
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +3 -3
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-events.d.ts +2 -2
- package/lib/typescript/storage-events.d.ts.map +1 -1
- package/lib/typescript/storage-platform.d.ts +12 -0
- package/lib/typescript/storage-platform.d.ts.map +1 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +5 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/lib/typescript/web-backend-contract.d.ts +10 -0
- package/lib/typescript/web-backend-contract.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +1 -1
- package/lib/typescript/web-storage-backend.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/capabilities.ts +36 -0
- package/src/core/durability.ts +170 -0
- package/src/core/metrics.ts +133 -0
- package/src/index.ts +66 -40
- package/src/index.web.ts +158 -86
- package/src/indexeddb-backend.ts +91 -20
- package/src/internal.ts +50 -4
- package/src/shared.ts +7 -6
- package/src/storage-core.ts +317 -285
- package/src/storage-events.ts +2 -1
- package/src/storage-platform.ts +16 -0
- package/src/storage-runtime.ts +13 -47
- package/src/testing.ts +14 -7
- package/src/web-backend-contract.ts +34 -0
- package/src/web-storage-backend.ts +1 -1
package/src/indexeddb-backend.ts
CHANGED
|
@@ -19,7 +19,11 @@ export type IndexedDBBackendOptions = {
|
|
|
19
19
|
function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
|
|
20
20
|
return new Promise((resolve, reject) => {
|
|
21
21
|
if (typeof indexedDB === "undefined") {
|
|
22
|
-
reject(
|
|
22
|
+
reject(
|
|
23
|
+
new Error(
|
|
24
|
+
"[nitro-error:unsupported] IndexedDB is not available in this environment.",
|
|
25
|
+
),
|
|
26
|
+
);
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -32,9 +36,12 @@ function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
|
|
|
32
36
|
}
|
|
33
37
|
};
|
|
34
38
|
|
|
35
|
-
request.onsuccess = () =>
|
|
36
|
-
|
|
39
|
+
request.onsuccess = () => {
|
|
40
|
+
resolve(request.result);
|
|
41
|
+
};
|
|
42
|
+
request.onerror = () => {
|
|
37
43
|
reject(request.error ?? new Error("Failed to open IndexedDB database."));
|
|
44
|
+
};
|
|
38
45
|
});
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -72,7 +79,7 @@ export async function createIndexedDBBackend(
|
|
|
72
79
|
const db = await openDB(dbName, storeName);
|
|
73
80
|
const cache = new Map<string, string>();
|
|
74
81
|
const pendingWrites = new Set<Promise<void>>();
|
|
75
|
-
const pendingErrors: Error[] = [];
|
|
82
|
+
const pendingErrors: { keys: string[] | null; error: Error }[] = [];
|
|
76
83
|
const subscribers = new Set<(event: WebStorageChangeEvent) => void>();
|
|
77
84
|
let closed = false;
|
|
78
85
|
const sourceId = `nitro-storage-${Math.random().toString(36).slice(2)}`;
|
|
@@ -95,15 +102,51 @@ export async function createIndexedDBBackend(
|
|
|
95
102
|
});
|
|
96
103
|
}
|
|
97
104
|
|
|
98
|
-
function handleAsyncError(error: unknown): void {
|
|
105
|
+
function handleAsyncError(keys: string[] | null, error: unknown): void {
|
|
99
106
|
const normalized =
|
|
100
107
|
error instanceof Error
|
|
101
108
|
? error
|
|
102
109
|
: new Error(String(error ?? "Unknown IndexedDB error"));
|
|
103
|
-
pendingErrors.push(normalized);
|
|
110
|
+
pendingErrors.push({ keys, error: normalized });
|
|
104
111
|
options.onError?.(normalized);
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
function handleLifecycleFlush(): void {
|
|
115
|
+
if (closed || pendingWrites.size === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
void Promise.all(Array.from(pendingWrites)).catch(() => {
|
|
119
|
+
// Errors are surfaced through flush() and onError; the page may be
|
|
120
|
+
// hidden, so the flush result is intentionally not awaited here.
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function installLifecycleFlush(): () => void {
|
|
125
|
+
if (typeof globalThis === "undefined" || !globalThis.addEventListener) {
|
|
126
|
+
return () => {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const onPageHide = () => {
|
|
130
|
+
handleLifecycleFlush();
|
|
131
|
+
};
|
|
132
|
+
const onVisibilityChange = () => {
|
|
133
|
+
if (
|
|
134
|
+
typeof document !== "undefined" &&
|
|
135
|
+
document.visibilityState === "hidden"
|
|
136
|
+
) {
|
|
137
|
+
handleLifecycleFlush();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
globalThis.addEventListener("pagehide", onPageHide);
|
|
141
|
+
globalThis.addEventListener("visibilitychange", onVisibilityChange);
|
|
142
|
+
return () => {
|
|
143
|
+
globalThis.removeEventListener("pagehide", onPageHide);
|
|
144
|
+
globalThis.removeEventListener("visibilitychange", onVisibilityChange);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const removeLifecycleFlush = installLifecycleFlush();
|
|
149
|
+
|
|
107
150
|
channel?.addEventListener("message", (event: MessageEvent) => {
|
|
108
151
|
if (closed) {
|
|
109
152
|
return;
|
|
@@ -153,22 +196,29 @@ export async function createIndexedDBBackend(
|
|
|
153
196
|
}
|
|
154
197
|
};
|
|
155
198
|
|
|
156
|
-
tx.oncomplete = () =>
|
|
157
|
-
|
|
199
|
+
tx.oncomplete = () => {
|
|
200
|
+
resolve();
|
|
201
|
+
};
|
|
202
|
+
tx.onerror = () => {
|
|
158
203
|
reject(tx.error ?? new Error("Failed to load IndexedDB entries."));
|
|
204
|
+
};
|
|
159
205
|
});
|
|
160
206
|
|
|
161
|
-
function trackWrite(tx: IDBTransaction): void {
|
|
207
|
+
function trackWrite(tx: IDBTransaction, keys: string[] | null): void {
|
|
162
208
|
const pending = new Promise<void>((resolve) => {
|
|
163
|
-
tx.oncomplete = () =>
|
|
209
|
+
tx.oncomplete = () => {
|
|
210
|
+
resolve();
|
|
211
|
+
};
|
|
164
212
|
tx.onerror = () => {
|
|
165
213
|
handleAsyncError(
|
|
214
|
+
keys,
|
|
166
215
|
tx.error ?? new Error("Failed to persist IndexedDB transaction."),
|
|
167
216
|
);
|
|
168
217
|
resolve();
|
|
169
218
|
};
|
|
170
219
|
tx.onabort = () => {
|
|
171
220
|
handleAsyncError(
|
|
221
|
+
keys,
|
|
172
222
|
tx.error ?? new Error("IndexedDB transaction was aborted."),
|
|
173
223
|
);
|
|
174
224
|
resolve();
|
|
@@ -186,9 +236,10 @@ export async function createIndexedDBBackend(
|
|
|
186
236
|
try {
|
|
187
237
|
const tx = db.transaction(storeName, "readwrite");
|
|
188
238
|
tx.objectStore(storeName).put(value, key);
|
|
189
|
-
trackWrite(tx);
|
|
239
|
+
trackWrite(tx, [key]);
|
|
190
240
|
} catch {
|
|
191
241
|
handleAsyncError(
|
|
242
|
+
[key],
|
|
192
243
|
new Error(`Failed to queue IndexedDB write for "${key}".`),
|
|
193
244
|
);
|
|
194
245
|
}
|
|
@@ -198,9 +249,10 @@ export async function createIndexedDBBackend(
|
|
|
198
249
|
try {
|
|
199
250
|
const tx = db.transaction(storeName, "readwrite");
|
|
200
251
|
tx.objectStore(storeName).delete(key);
|
|
201
|
-
trackWrite(tx);
|
|
252
|
+
trackWrite(tx, [key]);
|
|
202
253
|
} catch {
|
|
203
254
|
handleAsyncError(
|
|
255
|
+
[key],
|
|
204
256
|
new Error(`Failed to queue IndexedDB delete for "${key}".`),
|
|
205
257
|
);
|
|
206
258
|
}
|
|
@@ -210,9 +262,9 @@ export async function createIndexedDBBackend(
|
|
|
210
262
|
try {
|
|
211
263
|
const tx = db.transaction(storeName, "readwrite");
|
|
212
264
|
tx.objectStore(storeName).clear();
|
|
213
|
-
trackWrite(tx);
|
|
265
|
+
trackWrite(tx, null);
|
|
214
266
|
} catch {
|
|
215
|
-
handleAsyncError(new Error("Failed to queue IndexedDB clear."));
|
|
267
|
+
handleAsyncError(null, new Error("Failed to queue IndexedDB clear."));
|
|
216
268
|
}
|
|
217
269
|
}
|
|
218
270
|
|
|
@@ -254,8 +306,10 @@ export async function createIndexedDBBackend(
|
|
|
254
306
|
},
|
|
255
307
|
setMany(entries): void {
|
|
256
308
|
assertOpen();
|
|
309
|
+
const keys: string[] = [];
|
|
257
310
|
entries.forEach(([key, value]) => {
|
|
258
311
|
cache.set(key, value);
|
|
312
|
+
keys.push(key);
|
|
259
313
|
});
|
|
260
314
|
try {
|
|
261
315
|
const tx = db.transaction(storeName, "readwrite");
|
|
@@ -264,9 +318,12 @@ export async function createIndexedDBBackend(
|
|
|
264
318
|
store.put(value, key);
|
|
265
319
|
publish({ key, newValue: value });
|
|
266
320
|
});
|
|
267
|
-
trackWrite(tx);
|
|
321
|
+
trackWrite(tx, keys);
|
|
268
322
|
} catch {
|
|
269
|
-
handleAsyncError(
|
|
323
|
+
handleAsyncError(
|
|
324
|
+
null,
|
|
325
|
+
new Error("Failed to queue IndexedDB batch write."),
|
|
326
|
+
);
|
|
270
327
|
}
|
|
271
328
|
},
|
|
272
329
|
removeMany(keys: string[]): void {
|
|
@@ -281,9 +338,12 @@ export async function createIndexedDBBackend(
|
|
|
281
338
|
store.delete(key);
|
|
282
339
|
publish({ key, newValue: null });
|
|
283
340
|
});
|
|
284
|
-
trackWrite(tx);
|
|
341
|
+
trackWrite(tx, keys);
|
|
285
342
|
} catch {
|
|
286
|
-
handleAsyncError(
|
|
343
|
+
handleAsyncError(
|
|
344
|
+
null,
|
|
345
|
+
new Error("Failed to queue IndexedDB batch delete."),
|
|
346
|
+
);
|
|
287
347
|
}
|
|
288
348
|
},
|
|
289
349
|
size(): number {
|
|
@@ -303,14 +363,25 @@ export async function createIndexedDBBackend(
|
|
|
303
363
|
return;
|
|
304
364
|
}
|
|
305
365
|
|
|
306
|
-
const
|
|
307
|
-
|
|
366
|
+
const failures = pendingErrors.splice(0);
|
|
367
|
+
const failedKeys = failures
|
|
368
|
+
.flatMap(({ keys }) => keys ?? [])
|
|
369
|
+
.filter((key, index, all) => all.indexOf(key) === index);
|
|
370
|
+
const keyDetail =
|
|
371
|
+
failedKeys.length > 0
|
|
372
|
+
? ` Affected keys: ${failedKeys.join(", ")}.`
|
|
373
|
+
: "";
|
|
374
|
+
const [first] = failures;
|
|
375
|
+
throw new Error(
|
|
376
|
+
`NitroStorage: IndexedDB persistence failed for ${failures.length} write(s).${keyDetail} ${first?.error.message ?? ""}`,
|
|
377
|
+
);
|
|
308
378
|
},
|
|
309
379
|
close(): void {
|
|
310
380
|
if (closed) {
|
|
311
381
|
return;
|
|
312
382
|
}
|
|
313
383
|
closed = true;
|
|
384
|
+
removeLifecycleFlush();
|
|
314
385
|
subscribers.clear();
|
|
315
386
|
channel?.close();
|
|
316
387
|
db.close();
|
package/src/internal.ts
CHANGED
|
@@ -16,6 +16,36 @@ const PRIM_NAN = "__nitro_storage_primitive__:n:NaN";
|
|
|
16
16
|
const NAMESPACE_SEPARATOR = ":";
|
|
17
17
|
const VERSION_TOKEN_PREFIX = "__nitro_storage_version__:";
|
|
18
18
|
|
|
19
|
+
export const ESCAPE_PREFIX = "__nitro_storage_escaped__:";
|
|
20
|
+
|
|
21
|
+
const RESERVED_RAW_TOKENS = new Set<string>([
|
|
22
|
+
PRIM_NULL,
|
|
23
|
+
PRIM_UNDEFINED,
|
|
24
|
+
PRIM_TRUE,
|
|
25
|
+
PRIM_FALSE,
|
|
26
|
+
PRIM_INFINITY,
|
|
27
|
+
PRIM_NEG_INFINITY,
|
|
28
|
+
PRIM_NAN,
|
|
29
|
+
NATIVE_BATCH_MISSING_SENTINEL,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export function escapeCollidingRawValue(value: string): string {
|
|
33
|
+
if (
|
|
34
|
+
RESERVED_RAW_TOKENS.has(value) ||
|
|
35
|
+
value.startsWith(PRIMITIVE_FAST_PATH_PREFIX) ||
|
|
36
|
+
value.startsWith(ESCAPE_PREFIX)
|
|
37
|
+
) {
|
|
38
|
+
return `${ESCAPE_PREFIX}${value}`;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function unescapeCollidingRawValue(value: string): string {
|
|
44
|
+
return value.startsWith(ESCAPE_PREFIX)
|
|
45
|
+
? value.slice(ESCAPE_PREFIX.length)
|
|
46
|
+
: value;
|
|
47
|
+
}
|
|
48
|
+
|
|
19
49
|
export type StoredEnvelope = {
|
|
20
50
|
__nitroStorageEnvelope: true;
|
|
21
51
|
expiresAt: number;
|
|
@@ -71,7 +101,7 @@ export function assertBatchScope(
|
|
|
71
101
|
export function decodeNativeBatchValue(
|
|
72
102
|
value: string | undefined,
|
|
73
103
|
): string | undefined {
|
|
74
|
-
if (value === NATIVE_BATCH_MISSING_SENTINEL) {
|
|
104
|
+
if (value === undefined || value === NATIVE_BATCH_MISSING_SENTINEL) {
|
|
75
105
|
return undefined;
|
|
76
106
|
}
|
|
77
107
|
|
|
@@ -93,8 +123,17 @@ export function serializeWithPrimitiveFastPath<T>(value: T): string {
|
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
switch (typeof value) {
|
|
96
|
-
case "string":
|
|
97
|
-
|
|
126
|
+
case "string": {
|
|
127
|
+
const stringValue = value as string;
|
|
128
|
+
if (
|
|
129
|
+
stringValue === NATIVE_BATCH_MISSING_SENTINEL ||
|
|
130
|
+
stringValue.startsWith(PRIMITIVE_FAST_PATH_PREFIX) ||
|
|
131
|
+
stringValue.startsWith(ESCAPE_PREFIX)
|
|
132
|
+
) {
|
|
133
|
+
return `${PRIM_STRING_PREFIX}${ESCAPE_PREFIX}${stringValue}`;
|
|
134
|
+
}
|
|
135
|
+
return PRIM_STRING_PREFIX + stringValue;
|
|
136
|
+
}
|
|
98
137
|
case "number":
|
|
99
138
|
if (Number.isFinite(value)) {
|
|
100
139
|
return PRIM_NUMBER_PREFIX + String(value);
|
|
@@ -134,6 +173,10 @@ const CHAR_B = 98; // 'b'
|
|
|
134
173
|
const CHAR_N = 110; // 'n'
|
|
135
174
|
|
|
136
175
|
export function deserializeWithPrimitiveFastPath<T>(value: string): T {
|
|
176
|
+
if (value.startsWith(ESCAPE_PREFIX)) {
|
|
177
|
+
return value.slice(ESCAPE_PREFIX.length) as T;
|
|
178
|
+
}
|
|
179
|
+
|
|
137
180
|
if (value.startsWith(PRIMITIVE_FAST_PATH_PREFIX)) {
|
|
138
181
|
const prefixLen = PRIMITIVE_FAST_PATH_PREFIX.length;
|
|
139
182
|
const tagChar = value.charCodeAt(prefixLen);
|
|
@@ -149,7 +192,10 @@ export function deserializeWithPrimitiveFastPath<T>(value: string): T {
|
|
|
149
192
|
const payload = value.slice(prefixLen + 2);
|
|
150
193
|
|
|
151
194
|
if (tagChar === CHAR_S) {
|
|
152
|
-
|
|
195
|
+
const unescapedPayload = payload.startsWith(ESCAPE_PREFIX)
|
|
196
|
+
? payload.slice(ESCAPE_PREFIX.length)
|
|
197
|
+
: payload;
|
|
198
|
+
return unescapedPayload as T;
|
|
153
199
|
}
|
|
154
200
|
if (tagChar === CHAR_B) {
|
|
155
201
|
return (payload === "1") as T;
|
package/src/shared.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
2
1
|
import {
|
|
3
2
|
serializeWithPrimitiveFastPath,
|
|
4
3
|
deserializeWithPrimitiveFastPath,
|
|
5
4
|
} from "./internal";
|
|
6
|
-
import {
|
|
7
|
-
getStorageErrorCode,
|
|
8
|
-
isLockedStorageErrorCode,
|
|
9
|
-
} from "./storage-runtime";
|
|
10
5
|
import type {
|
|
11
6
|
StorageChangeOperation,
|
|
12
7
|
StorageChangeSource,
|
|
13
8
|
StorageKeyChangeEvent,
|
|
14
9
|
} from "./storage-events";
|
|
10
|
+
import {
|
|
11
|
+
getStorageErrorCode,
|
|
12
|
+
isLockedStorageErrorCode,
|
|
13
|
+
} from "./storage-runtime";
|
|
14
|
+
import { StorageScope } from "./Storage.types";
|
|
15
|
+
import type { AccessControl, BiometricLevel } from "./Storage.types";
|
|
15
16
|
|
|
16
17
|
export type Validator<T> = (value: unknown) => value is T;
|
|
17
18
|
|
|
@@ -144,7 +145,7 @@ export const runMicrotask =
|
|
|
144
145
|
typeof queueMicrotask === "function"
|
|
145
146
|
? queueMicrotask
|
|
146
147
|
: (task: () => void) => {
|
|
147
|
-
Promise.resolve().then(task);
|
|
148
|
+
void Promise.resolve().then(task);
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
export const now =
|