snapback4 0.0.1 → 0.0.2
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 +360 -15
- package/bin/snapback4.js +24 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.js +90 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +262 -0
- package/dist/local.d.ts +34 -0
- package/dist/local.js +358 -0
- package/dist/replica/cursor.d.ts +15 -0
- package/dist/replica/cursor.js +81 -0
- package/dist/replica/index.d.ts +3 -0
- package/dist/replica/index.js +6 -0
- package/dist/replica/interpreter.d.ts +106 -0
- package/dist/replica/interpreter.js +906 -0
- package/dist/replica/key.d.ts +9 -0
- package/dist/replica/key.js +107 -0
- package/dist/replica/replica.d.ts +58 -0
- package/dist/replica/replica.js +147 -0
- package/dist/replica/sqlite.d.ts +29 -0
- package/dist/replica/sqlite.js +151 -0
- package/dist/replica/store.d.ts +91 -0
- package/dist/replica/store.js +359 -0
- package/package.json +29 -6
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// The replica store: rows by (table, id) and one index entry per (index,
|
|
2
|
+
// key bytes), the server's layout on the device. Two stores share one
|
|
3
|
+
// interface — in memory for tests and the simulator's twin, IndexedDB on
|
|
4
|
+
// the web. Every read runs inside one transaction; every write commits as
|
|
5
|
+
// one batch, so a partition batch applies atomically and a watermark never
|
|
6
|
+
// gets ahead of its rows.
|
|
7
|
+
import { compareKeys, encodeKey, kindOf, upperBound } from "./key.js";
|
|
8
|
+
/** The index components' kinds for a table, from the schema. */
|
|
9
|
+
export function indexKinds(schema, table, index) {
|
|
10
|
+
const definition = schema.tables[table]?.indexes[index];
|
|
11
|
+
if (!definition)
|
|
12
|
+
throw new Error(`no index ${table}.${index}`);
|
|
13
|
+
const columns = definition.components.map((c) => ("Column" in c ? c.Column : "")).filter(Boolean);
|
|
14
|
+
const kinds = columns.map((column) => (column === "id" ? "id" : kindOf(schema.tables[table].columns[column])));
|
|
15
|
+
return { columns, kinds };
|
|
16
|
+
}
|
|
17
|
+
/** The stored key of one index entry: the components, then the id. */
|
|
18
|
+
export function indexKey(schema, table, index, row) {
|
|
19
|
+
const { columns, kinds } = indexKinds(schema, table, index);
|
|
20
|
+
const components = columns.map((column, i) => [kinds[i], row[column] ?? null]);
|
|
21
|
+
components.push(["id", row.id]);
|
|
22
|
+
return encodeKey(components);
|
|
23
|
+
}
|
|
24
|
+
/** The byte range of a scan: `[lo, hi)`, or null when empty. */
|
|
25
|
+
export function scanRange(schema, table, index, prefix, bounds, dir) {
|
|
26
|
+
const { kinds } = indexKinds(schema, table, index);
|
|
27
|
+
const enc = (values) => encodeKey(values.map((v, i) => [kinds[i] ?? "string", v]));
|
|
28
|
+
const base = enc(prefix);
|
|
29
|
+
let lo = base;
|
|
30
|
+
let hi = upperBound(base);
|
|
31
|
+
const withNext = (value) => enc([...prefix, value]);
|
|
32
|
+
const max = (a, b) => (compareKeys(a, b) >= 0 ? a : b);
|
|
33
|
+
const min = (a, b) => (compareKeys(a, b) <= 0 ? a : b);
|
|
34
|
+
if (bounds.gte !== undefined)
|
|
35
|
+
lo = max(lo, withNext(bounds.gte));
|
|
36
|
+
if (bounds.gt !== undefined)
|
|
37
|
+
lo = max(lo, upperBound(withNext(bounds.gt)));
|
|
38
|
+
if (bounds.lte !== undefined)
|
|
39
|
+
hi = min(hi, upperBound(withNext(bounds.lte)));
|
|
40
|
+
if (bounds.lt !== undefined)
|
|
41
|
+
hi = min(hi, withNext(bounds.lt));
|
|
42
|
+
if (bounds.after) {
|
|
43
|
+
const position = enc([...prefix, ...bounds.after]);
|
|
44
|
+
if (dir === "asc")
|
|
45
|
+
lo = max(lo, upperBound(position));
|
|
46
|
+
else
|
|
47
|
+
hi = min(hi, position);
|
|
48
|
+
}
|
|
49
|
+
if (bounds.before) {
|
|
50
|
+
const position = enc([...prefix, ...bounds.before]);
|
|
51
|
+
if (dir === "asc")
|
|
52
|
+
hi = min(hi, position);
|
|
53
|
+
else
|
|
54
|
+
lo = max(lo, upperBound(position));
|
|
55
|
+
}
|
|
56
|
+
return compareKeys(lo, hi) < 0 ? [lo, hi] : null;
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// In memory.
|
|
60
|
+
function lowerBoundIndex(entries, key) {
|
|
61
|
+
let lo = 0, hi = entries.length;
|
|
62
|
+
while (lo < hi) {
|
|
63
|
+
const mid = (lo + hi) >> 1;
|
|
64
|
+
if (compareKeys(entries[mid].key, key) < 0)
|
|
65
|
+
lo = mid + 1;
|
|
66
|
+
else
|
|
67
|
+
hi = mid;
|
|
68
|
+
}
|
|
69
|
+
return lo;
|
|
70
|
+
}
|
|
71
|
+
export class MemoryStore {
|
|
72
|
+
schema;
|
|
73
|
+
rows = new Map();
|
|
74
|
+
indexes = new Map();
|
|
75
|
+
meta = new Map();
|
|
76
|
+
sides = new Map();
|
|
77
|
+
constructor(schema) {
|
|
78
|
+
this.schema = schema;
|
|
79
|
+
}
|
|
80
|
+
tx() {
|
|
81
|
+
const self = this;
|
|
82
|
+
const entries = (table, index) => {
|
|
83
|
+
const name = `${table}.${index}`;
|
|
84
|
+
let list = self.indexes.get(name);
|
|
85
|
+
if (!list) {
|
|
86
|
+
list = [];
|
|
87
|
+
self.indexes.set(name, list);
|
|
88
|
+
}
|
|
89
|
+
return list;
|
|
90
|
+
};
|
|
91
|
+
const side = (name) => {
|
|
92
|
+
let map = self.sides.get(name);
|
|
93
|
+
if (!map) {
|
|
94
|
+
map = new Map();
|
|
95
|
+
self.sides.set(name, map);
|
|
96
|
+
}
|
|
97
|
+
const m = map;
|
|
98
|
+
return {
|
|
99
|
+
async get(key) { return m.get(key); },
|
|
100
|
+
async put(key, value) { m.set(key, structuredClone(value)); },
|
|
101
|
+
async delete(key) { m.delete(key); },
|
|
102
|
+
async all() { return [...m.entries()].map(([key, value]) => ({ key, value })); },
|
|
103
|
+
async clear() { m.clear(); },
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
async get(table, id) { return self.rows.get(table)?.get(id); },
|
|
108
|
+
async lookup(table, index, key) {
|
|
109
|
+
const { kinds } = indexKinds(self.schema, table, index);
|
|
110
|
+
const prefix = encodeKey(key.map((v, i) => [kinds[i] ?? "string", v]));
|
|
111
|
+
const list = entries(table, index);
|
|
112
|
+
const at = lowerBoundIndex(list, prefix);
|
|
113
|
+
const entry = list[at];
|
|
114
|
+
if (!entry || compareKeys(entry.key, upperBound(prefix)) >= 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
return self.rows.get(table)?.get(entry.id);
|
|
117
|
+
},
|
|
118
|
+
async scan(table, index, prefix, bounds, dir, limit) {
|
|
119
|
+
const range = scanRange(self.schema, table, index, prefix, bounds, dir);
|
|
120
|
+
if (!range)
|
|
121
|
+
return [];
|
|
122
|
+
const [lo, hi] = range;
|
|
123
|
+
const list = entries(table, index);
|
|
124
|
+
const start = lowerBoundIndex(list, lo);
|
|
125
|
+
const end = lowerBoundIndex(list, hi);
|
|
126
|
+
const out = [];
|
|
127
|
+
if (dir === "asc") {
|
|
128
|
+
for (let i = start; i < end && out.length < limit; i++) {
|
|
129
|
+
const row = self.rows.get(table)?.get(list[i].id);
|
|
130
|
+
if (row)
|
|
131
|
+
out.push({ row, key: list[i].key });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
for (let i = end - 1; i >= start && out.length < limit; i--) {
|
|
136
|
+
const row = self.rows.get(table)?.get(list[i].id);
|
|
137
|
+
if (row)
|
|
138
|
+
out.push({ row, key: list[i].key });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
},
|
|
143
|
+
async count(table, index, prefix, limit) {
|
|
144
|
+
const range = scanRange(self.schema, table, index, prefix, {}, "asc");
|
|
145
|
+
if (!range)
|
|
146
|
+
return 0;
|
|
147
|
+
const list = entries(table, index);
|
|
148
|
+
return Math.min(limit, lowerBoundIndex(list, range[1]) - lowerBoundIndex(list, range[0]));
|
|
149
|
+
},
|
|
150
|
+
async put(table, row) {
|
|
151
|
+
let rows = self.rows.get(table);
|
|
152
|
+
if (!rows) {
|
|
153
|
+
rows = new Map();
|
|
154
|
+
self.rows.set(table, rows);
|
|
155
|
+
}
|
|
156
|
+
const old = rows.get(row.id);
|
|
157
|
+
const definition = self.schema.tables[table];
|
|
158
|
+
if (!definition)
|
|
159
|
+
throw new Error(`unknown table ${table}`);
|
|
160
|
+
for (const index of Object.keys(definition.indexes)) {
|
|
161
|
+
const list = entries(table, index);
|
|
162
|
+
if (old) {
|
|
163
|
+
const before = indexKey(self.schema, table, index, old);
|
|
164
|
+
const at = lowerBoundIndex(list, before);
|
|
165
|
+
if (list[at] && compareKeys(list[at].key, before) === 0)
|
|
166
|
+
list.splice(at, 1);
|
|
167
|
+
}
|
|
168
|
+
const key = indexKey(self.schema, table, index, row);
|
|
169
|
+
list.splice(lowerBoundIndex(list, key), 0, { key, id: row.id });
|
|
170
|
+
}
|
|
171
|
+
rows.set(row.id, structuredClone(row));
|
|
172
|
+
},
|
|
173
|
+
async delete(table, id) {
|
|
174
|
+
const rows = self.rows.get(table);
|
|
175
|
+
const old = rows?.get(id);
|
|
176
|
+
if (!old)
|
|
177
|
+
return;
|
|
178
|
+
for (const index of Object.keys(self.schema.tables[table]?.indexes ?? {})) {
|
|
179
|
+
const list = entries(table, index);
|
|
180
|
+
const before = indexKey(self.schema, table, index, old);
|
|
181
|
+
const at = lowerBoundIndex(list, before);
|
|
182
|
+
if (list[at] && compareKeys(list[at].key, before) === 0)
|
|
183
|
+
list.splice(at, 1);
|
|
184
|
+
}
|
|
185
|
+
rows.delete(id);
|
|
186
|
+
},
|
|
187
|
+
async getMeta(key) { return self.meta.get(key); },
|
|
188
|
+
async setMeta(key, value) { self.meta.set(key, value); },
|
|
189
|
+
side,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async read(body) { return body(this.tx()); }
|
|
193
|
+
async write(body) { return body(this.tx()); }
|
|
194
|
+
async clearRows() { this.rows.clear(); this.indexes.clear(); }
|
|
195
|
+
async close() { }
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// IndexedDB.
|
|
199
|
+
const STORES = ["rows", "idx", "meta", "outbox", "predicted", "log"];
|
|
200
|
+
/** IndexedDB compares binary keys byte-wise; hand it a plain ArrayBuffer. */
|
|
201
|
+
function idbKey(bytes) {
|
|
202
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
203
|
+
}
|
|
204
|
+
function request(req) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
req.onsuccess = () => resolve(req.result);
|
|
207
|
+
req.onerror = () => reject(req.error);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
export async function openIndexedDb(name, schema) {
|
|
211
|
+
const db = await new Promise((resolve, reject) => {
|
|
212
|
+
const open = indexedDB.open(name, 1);
|
|
213
|
+
open.onupgradeneeded = () => {
|
|
214
|
+
const database = open.result;
|
|
215
|
+
for (const store of STORES)
|
|
216
|
+
if (!database.objectStoreNames.contains(store))
|
|
217
|
+
database.createObjectStore(store);
|
|
218
|
+
};
|
|
219
|
+
open.onsuccess = () => resolve(open.result);
|
|
220
|
+
open.onerror = () => reject(open.error);
|
|
221
|
+
});
|
|
222
|
+
return new IndexedDbStore(db, schema);
|
|
223
|
+
}
|
|
224
|
+
class IndexedDbStore {
|
|
225
|
+
db;
|
|
226
|
+
schema;
|
|
227
|
+
constructor(db, schema) {
|
|
228
|
+
this.db = db;
|
|
229
|
+
this.schema = schema;
|
|
230
|
+
}
|
|
231
|
+
tx(mode) {
|
|
232
|
+
const transaction = this.db.transaction([...STORES], mode);
|
|
233
|
+
const done = new Promise((resolve, reject) => {
|
|
234
|
+
transaction.oncomplete = () => resolve();
|
|
235
|
+
transaction.onerror = () => reject(transaction.error);
|
|
236
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("aborted"));
|
|
237
|
+
});
|
|
238
|
+
const rows = transaction.objectStore("rows");
|
|
239
|
+
const idx = transaction.objectStore("idx");
|
|
240
|
+
const meta = transaction.objectStore("meta");
|
|
241
|
+
const schema = this.schema;
|
|
242
|
+
const side = (name) => {
|
|
243
|
+
const store = transaction.objectStore(name);
|
|
244
|
+
return {
|
|
245
|
+
get: (key) => request(store.get(key)),
|
|
246
|
+
put: async (key, value) => { await request(store.put(value, key)); },
|
|
247
|
+
delete: async (key) => { await request(store.delete(key)); },
|
|
248
|
+
all: async () => {
|
|
249
|
+
const keys = await request(store.getAllKeys());
|
|
250
|
+
const values = await request(store.getAll());
|
|
251
|
+
return keys.map((key, i) => ({ key: String(key), value: values[i] }));
|
|
252
|
+
},
|
|
253
|
+
clear: async () => { await request(store.clear()); },
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
const api = {
|
|
257
|
+
get: (table, id) => request(rows.get([table, id])),
|
|
258
|
+
async lookup(table, index, key) {
|
|
259
|
+
const { kinds } = indexKinds(schema, table, index);
|
|
260
|
+
const prefix = encodeKey(key.map((v, i) => [kinds[i] ?? "string", v]));
|
|
261
|
+
const range = IDBKeyRange.bound([`${table}.${index}`, idbKey(prefix)], [`${table}.${index}`, idbKey(upperBound(prefix))], false, true);
|
|
262
|
+
const id = await request(idx.get(range));
|
|
263
|
+
return id === undefined ? undefined : request(rows.get([table, id]));
|
|
264
|
+
},
|
|
265
|
+
async scan(table, index, prefix, bounds, dir, limit) {
|
|
266
|
+
const range = scanRange(schema, table, index, prefix, bounds, dir);
|
|
267
|
+
if (!range || limit <= 0)
|
|
268
|
+
return [];
|
|
269
|
+
const name = `${table}.${index}`;
|
|
270
|
+
const keyRange = IDBKeyRange.bound([name, idbKey(range[0])], [name, idbKey(range[1])], false, true);
|
|
271
|
+
const out = [];
|
|
272
|
+
await new Promise((resolve, reject) => {
|
|
273
|
+
const cursor = idx.openCursor(keyRange, dir === "asc" ? "next" : "prev");
|
|
274
|
+
cursor.onerror = () => reject(cursor.error);
|
|
275
|
+
cursor.onsuccess = () => {
|
|
276
|
+
const c = cursor.result;
|
|
277
|
+
if (!c || out.length >= limit) {
|
|
278
|
+
resolve();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const id = c.value;
|
|
282
|
+
const key = new Uint8Array(c.key[1]);
|
|
283
|
+
const get = rows.get([table, id]);
|
|
284
|
+
get.onsuccess = () => {
|
|
285
|
+
if (get.result)
|
|
286
|
+
out.push({ row: get.result, key });
|
|
287
|
+
if (out.length >= limit)
|
|
288
|
+
resolve();
|
|
289
|
+
else
|
|
290
|
+
c.continue();
|
|
291
|
+
};
|
|
292
|
+
get.onerror = () => reject(get.error);
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
return out;
|
|
296
|
+
},
|
|
297
|
+
async count(table, index, prefix, limit) {
|
|
298
|
+
const range = scanRange(schema, table, index, prefix, {}, "asc");
|
|
299
|
+
if (!range)
|
|
300
|
+
return 0;
|
|
301
|
+
const name = `${table}.${index}`;
|
|
302
|
+
const n = await request(idx.count(IDBKeyRange.bound([name, idbKey(range[0])], [name, idbKey(range[1])], false, true)));
|
|
303
|
+
return Math.min(n, limit);
|
|
304
|
+
},
|
|
305
|
+
async put(table, row) {
|
|
306
|
+
const old = (await request(rows.get([table, row.id])));
|
|
307
|
+
const definition = schema.tables[table];
|
|
308
|
+
if (!definition)
|
|
309
|
+
throw new Error(`unknown table ${table}`);
|
|
310
|
+
for (const index of Object.keys(definition.indexes)) {
|
|
311
|
+
const name = `${table}.${index}`;
|
|
312
|
+
if (old)
|
|
313
|
+
await request(idx.delete([name, idbKey(indexKey(schema, table, index, old))]));
|
|
314
|
+
await request(idx.put(row.id, [name, idbKey(indexKey(schema, table, index, row))]));
|
|
315
|
+
}
|
|
316
|
+
await request(rows.put(row, [table, row.id]));
|
|
317
|
+
},
|
|
318
|
+
async delete(table, id) {
|
|
319
|
+
const old = (await request(rows.get([table, id])));
|
|
320
|
+
if (!old)
|
|
321
|
+
return;
|
|
322
|
+
for (const index of Object.keys(schema.tables[table]?.indexes ?? {})) {
|
|
323
|
+
await request(idx.delete([`${table}.${index}`, idbKey(indexKey(schema, table, index, old))]));
|
|
324
|
+
}
|
|
325
|
+
await request(rows.delete([table, id]));
|
|
326
|
+
},
|
|
327
|
+
getMeta: (key) => request(meta.get(key)),
|
|
328
|
+
setMeta: async (key, value) => { await request(meta.put(value, key)); },
|
|
329
|
+
side,
|
|
330
|
+
};
|
|
331
|
+
return { transaction, api, done };
|
|
332
|
+
}
|
|
333
|
+
async read(body) {
|
|
334
|
+
const { api, done } = this.tx("readonly");
|
|
335
|
+
const result = await body(api);
|
|
336
|
+
await done;
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
async write(body) {
|
|
340
|
+
const { transaction, api, done } = this.tx("readwrite");
|
|
341
|
+
try {
|
|
342
|
+
const result = await body(api);
|
|
343
|
+
await done;
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
try {
|
|
348
|
+
transaction.abort();
|
|
349
|
+
}
|
|
350
|
+
catch { /* already done */ }
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async clearRows() {
|
|
355
|
+
const transaction = this.db.transaction(["rows", "idx", "predicted"], "readwrite");
|
|
356
|
+
await Promise.all([request(transaction.objectStore("rows").clear()), request(transaction.objectStore("idx").clear()), request(transaction.objectStore("predicted").clear())]);
|
|
357
|
+
}
|
|
358
|
+
async close() { this.db.close(); }
|
|
359
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapback4",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Snapback 4
|
|
5
|
+
"description": "Snapback 4: the card (contract), the online and local-first clients (IndexedDB, SQLite), React hooks, sign-in, a labelled mock, and the `snapback4` CLI. LLP 3000.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"snapback4": "bin/snapback4.js"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
10
|
+
"./auth": {
|
|
11
|
+
"types": "./dist/auth.d.ts",
|
|
12
|
+
"import": "./dist/auth.js"
|
|
13
|
+
},
|
|
14
|
+
"./client": {
|
|
15
|
+
"types": "./dist/client.d.ts",
|
|
16
|
+
"import": "./dist/client.js"
|
|
17
|
+
},
|
|
7
18
|
"./contract": {
|
|
8
19
|
"types": "./dist/contract.d.ts",
|
|
9
20
|
"import": "./dist/contract.js"
|
|
10
21
|
},
|
|
22
|
+
"./local": {
|
|
23
|
+
"types": "./dist/local.d.ts",
|
|
24
|
+
"import": "./dist/local.js"
|
|
25
|
+
},
|
|
11
26
|
"./mock": {
|
|
12
27
|
"types": "./dist/mock.d.ts",
|
|
13
28
|
"import": "./dist/mock.js"
|
|
@@ -15,8 +30,20 @@
|
|
|
15
30
|
"./react": {
|
|
16
31
|
"types": "./dist/react.d.ts",
|
|
17
32
|
"import": "./dist/react.js"
|
|
33
|
+
},
|
|
34
|
+
"./replica": {
|
|
35
|
+
"types": "./dist/replica/index.d.ts",
|
|
36
|
+
"import": "./dist/replica/index.js"
|
|
18
37
|
}
|
|
19
38
|
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"bin",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"optionalDependencies": {
|
|
45
|
+
"snapback4-darwin-arm64": "0.0.2"
|
|
46
|
+
},
|
|
20
47
|
"scripts": {
|
|
21
48
|
"build": "tsc -p tsconfig.build.json",
|
|
22
49
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
@@ -37,10 +64,6 @@
|
|
|
37
64
|
"url": "https://github.com/expo/snapback.git",
|
|
38
65
|
"directory": "snapback4/packages/snapback4"
|
|
39
66
|
},
|
|
40
|
-
"files": [
|
|
41
|
-
"dist",
|
|
42
|
-
"README.md"
|
|
43
|
-
],
|
|
44
67
|
"peerDependenciesMeta": {
|
|
45
68
|
"react": {
|
|
46
69
|
"optional": true
|