origin-snapshot 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/storage/indexeddb.js +144 -132
package/package.json
CHANGED
package/src/storage/indexeddb.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { serializeValue, deserializeValue } from
|
|
1
|
+
import { serializeValue, deserializeValue } from "../serialize.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @param {Array<Blob | Uint8Array>} bin
|
|
@@ -6,28 +6,28 @@ import { serializeValue, deserializeValue } from '../serialize.js';
|
|
|
6
6
|
* @returns {Promise<object[]>}
|
|
7
7
|
*/
|
|
8
8
|
export async function dumpDatabases(bin, names) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
9
|
+
let list = names;
|
|
10
|
+
if (!list) {
|
|
11
|
+
list =
|
|
12
|
+
typeof indexedDB.databases === "function"
|
|
13
|
+
? (await indexedDB.databases()).map((d) => d.name).filter(Boolean)
|
|
14
|
+
: [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dump = [];
|
|
18
|
+
for (const name of list) {
|
|
19
|
+
const db = await openExisting(name);
|
|
20
|
+
try {
|
|
21
|
+
const stores = [];
|
|
22
|
+
for (const storeName of [...db.objectStoreNames]) {
|
|
23
|
+
stores.push(await dumpStore(db, storeName, bin));
|
|
24
|
+
}
|
|
25
|
+
dump.push({ name: db.name, version: db.version, stores });
|
|
26
|
+
} finally {
|
|
27
|
+
db.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return dump;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -36,31 +36,36 @@ export async function dumpDatabases(bin, names) {
|
|
|
36
36
|
* @param {Array<Blob | Uint8Array>} bin
|
|
37
37
|
*/
|
|
38
38
|
async function dumpStore(db, storeName, bin) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
const tx = db.transaction(storeName, "readonly");
|
|
40
|
+
const store = tx.objectStore(storeName);
|
|
41
|
+
|
|
42
|
+
const schema = {
|
|
43
|
+
name: storeName,
|
|
44
|
+
keyPath: store.keyPath,
|
|
45
|
+
autoIncrement: store.autoIncrement,
|
|
46
|
+
indexes: [...store.indexNames].map((iname) => {
|
|
47
|
+
const ix = store.index(iname);
|
|
48
|
+
return {
|
|
49
|
+
name: iname,
|
|
50
|
+
keyPath: ix.keyPath,
|
|
51
|
+
unique: ix.unique,
|
|
52
|
+
multiEntry: ix.multiEntry,
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const outOfLine = store.keyPath === null;
|
|
58
|
+
const values = await request(store.getAll());
|
|
59
|
+
const keys = outOfLine ? await request(store.getAllKeys()) : null;
|
|
60
|
+
|
|
61
|
+
const records = values.map((value, i) => {
|
|
62
|
+
const rec = { value: serializeValue(value, bin) };
|
|
63
|
+
if (outOfLine) rec.key = serializeValue(keys[i], bin);
|
|
64
|
+
return rec;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await transactionDone(tx);
|
|
68
|
+
return { ...schema, records };
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
/**
|
|
@@ -69,17 +74,17 @@ async function dumpStore(db, storeName, bin) {
|
|
|
69
74
|
* @param {{ clear?: boolean }} [options]
|
|
70
75
|
*/
|
|
71
76
|
export async function restoreDatabases(dump, bin, { clear = true } = {}) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
for (const dbInfo of dump) {
|
|
78
|
+
if (clear) await deleteDatabase(dbInfo.name);
|
|
79
|
+
const db = await openForRestore(dbInfo, clear);
|
|
80
|
+
try {
|
|
81
|
+
for (const storeInfo of dbInfo.stores) {
|
|
82
|
+
await restoreStore(db, storeInfo, bin);
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
db.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -88,50 +93,50 @@ export async function restoreDatabases(dump, bin, { clear = true } = {}) {
|
|
|
88
93
|
* @param {Uint8Array[]} bin
|
|
89
94
|
*/
|
|
90
95
|
async function restoreStore(db, storeInfo, bin) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
const tx = db.transaction(storeInfo.name, "readwrite");
|
|
97
|
+
const store = tx.objectStore(storeInfo.name);
|
|
98
|
+
const inline = storeInfo.keyPath !== null;
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
for (const rec of storeInfo.records) {
|
|
101
|
+
const value = deserializeValue(rec.value, bin);
|
|
102
|
+
if (inline) store.put(value);
|
|
103
|
+
else store.put(value, deserializeValue(rec.key, bin));
|
|
104
|
+
}
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
await transactionDone(tx);
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
/** @param {string} name */
|
|
105
110
|
function openExisting(name) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const open = indexedDB.open(name);
|
|
113
|
+
open.onsuccess = () => {
|
|
114
|
+
const db = open.result;
|
|
115
|
+
db.onversionchange = () => db.close();
|
|
116
|
+
resolve(db);
|
|
117
|
+
};
|
|
118
|
+
open.onerror = () => reject(open.error);
|
|
119
|
+
});
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
118
123
|
*
|
|
119
124
|
* @param {any} dbInfo
|
|
120
|
-
* @param {boolean} wasCleared
|
|
125
|
+
* @param {boolean} wasCleared
|
|
121
126
|
* @returns {Promise<IDBDatabase>}
|
|
122
127
|
*/
|
|
123
128
|
async function openForRestore(dbInfo, wasCleared) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
if (wasCleared) {
|
|
130
|
+
return openAndUpgrade(dbInfo.name, dbInfo.version || 1, dbInfo.stores);
|
|
131
|
+
}
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
const current = await openExisting(dbInfo.name);
|
|
134
|
+
const needsUpgrade = dbInfo.stores.some((s) => !hasSchema(current, s));
|
|
135
|
+
if (!needsUpgrade) return current;
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
const targetVersion = Math.max(current.version + 1, dbInfo.version || 1);
|
|
138
|
+
current.close();
|
|
139
|
+
return openAndUpgrade(dbInfo.name, targetVersion, dbInfo.stores);
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
/**
|
|
@@ -139,10 +144,12 @@ async function openForRestore(dbInfo, wasCleared) {
|
|
|
139
144
|
* @param {any} storeInfo
|
|
140
145
|
*/
|
|
141
146
|
function hasSchema(db, storeInfo) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
if (!db.objectStoreNames.contains(storeInfo.name)) return false;
|
|
148
|
+
if (storeInfo.indexes.length === 0) return true;
|
|
149
|
+
const store = db
|
|
150
|
+
.transaction(storeInfo.name, "readonly")
|
|
151
|
+
.objectStore(storeInfo.name);
|
|
152
|
+
return storeInfo.indexes.every((ix) => store.indexNames.contains(ix.name));
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
/**
|
|
@@ -152,60 +159,65 @@ function hasSchema(db, storeInfo) {
|
|
|
152
159
|
* @returns {Promise<IDBDatabase>}
|
|
153
160
|
*/
|
|
154
161
|
function openAndUpgrade(name, version, stores) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const open = indexedDB.open(name, version);
|
|
164
|
+
open.onupgradeneeded = () => {
|
|
165
|
+
const db = open.result;
|
|
166
|
+
const tx = open.transaction;
|
|
167
|
+
for (const s of stores) {
|
|
168
|
+
const store = db.objectStoreNames.contains(s.name)
|
|
169
|
+
? tx.objectStore(s.name)
|
|
170
|
+
: db.createObjectStore(s.name, {
|
|
171
|
+
keyPath: s.keyPath,
|
|
172
|
+
autoIncrement: s.autoIncrement,
|
|
173
|
+
});
|
|
174
|
+
for (const ix of s.indexes) {
|
|
175
|
+
if (!store.indexNames.contains(ix.name)) {
|
|
176
|
+
store.createIndex(ix.name, ix.keyPath, {
|
|
177
|
+
unique: ix.unique,
|
|
178
|
+
multiEntry: ix.multiEntry,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
open.onsuccess = () => resolve(open.result);
|
|
185
|
+
open.onerror = () => reject(open.error);
|
|
186
|
+
// A blocked upgrade never fires success/error on its own — fail fast with an
|
|
187
|
+
// actionable error instead of leaving the restore hung forever.
|
|
188
|
+
open.onblocked = () => reject(blockedError(name));
|
|
189
|
+
});
|
|
177
190
|
}
|
|
178
|
-
|
|
179
191
|
/** @param {string} name */
|
|
180
192
|
function deleteDatabase(name) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const req = indexedDB.deleteDatabase(name);
|
|
195
|
+
req.onsuccess = () => resolve(undefined);
|
|
196
|
+
req.onerror = () => reject(req.error);
|
|
197
|
+
req.onblocked = () => reject(blockedError(name));
|
|
198
|
+
});
|
|
187
199
|
}
|
|
188
200
|
|
|
189
201
|
/** @param {string} name */
|
|
190
202
|
function blockedError(name) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
return new DOMException(
|
|
204
|
+
`IndexedDB "${name}" is blocked by another open connection — close other tabs on this origin and retry`,
|
|
205
|
+
"InvalidStateError",
|
|
206
|
+
);
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
/** @param {IDBRequest} req */
|
|
198
210
|
function request(req) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
req.onsuccess = () => resolve(req.result);
|
|
213
|
+
req.onerror = () => reject(req.error);
|
|
214
|
+
});
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
/** @param {IDBTransaction} tx */
|
|
206
218
|
function transactionDone(tx) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
tx.oncomplete = () => resolve(undefined);
|
|
221
|
+
tx.onabort = tx.onerror = () => reject(tx.error);
|
|
222
|
+
});
|
|
211
223
|
}
|