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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "origin-snapshot",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Export and restore a browser origin's localStorage, sessionStorage, IndexedDB, and OPFS as a single gzip-compressed snapshot file.",
5
5
  "keywords": [
6
6
  "localstorage",
@@ -1,4 +1,4 @@
1
- import { serializeValue, deserializeValue } from '../serialize.js';
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
- 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;
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
- 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 { name: iname, keyPath: ix.keyPath, unique: ix.unique, multiEntry: ix.multiEntry };
49
- })
50
- };
51
-
52
- const outOfLine = store.keyPath === null;
53
- const values = await request(store.getAll());
54
- const keys = outOfLine ? await request(store.getAllKeys()) : null;
55
-
56
- const records = values.map((value, i) => {
57
- const rec = { value: serializeValue(value, bin) };
58
- if (outOfLine) rec.key = serializeValue(keys[i], bin);
59
- return rec;
60
- });
61
-
62
- await transactionDone(tx);
63
- return { ...schema, records };
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
- for (const dbInfo of dump) {
73
- if (clear) await deleteDatabase(dbInfo.name);
74
- const db = await openForRestore(dbInfo, clear);
75
- try {
76
- for (const storeInfo of dbInfo.stores) {
77
- await restoreStore(db, storeInfo, bin);
78
- }
79
- } finally {
80
- db.close();
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
- const tx = db.transaction(storeInfo.name, 'readwrite');
92
- const store = tx.objectStore(storeInfo.name);
93
- const inline = storeInfo.keyPath !== null;
96
+ const tx = db.transaction(storeInfo.name, "readwrite");
97
+ const store = tx.objectStore(storeInfo.name);
98
+ const inline = storeInfo.keyPath !== null;
94
99
 
95
- for (const rec of storeInfo.records) {
96
- const value = deserializeValue(rec.value, bin);
97
- if (inline) store.put(value);
98
- else store.put(value, deserializeValue(rec.key, bin));
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
- await transactionDone(tx);
106
+ await transactionDone(tx);
102
107
  }
103
108
 
104
109
  /** @param {string} name */
105
110
  function openExisting(name) {
106
- return new Promise((resolve, reject) => {
107
- const open = indexedDB.open(name);
108
- open.onsuccess = () => {
109
- const db = open.result;
110
- db.onversionchange = () => db.close();
111
- resolve(db);
112
- };
113
- open.onerror = () => reject(open.error);
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
- if (wasCleared) {
125
- return openAndUpgrade(dbInfo.name, dbInfo.version || 1, dbInfo.stores);
126
- }
129
+ if (wasCleared) {
130
+ return openAndUpgrade(dbInfo.name, dbInfo.version || 1, dbInfo.stores);
131
+ }
127
132
 
128
- const current = await openExisting(dbInfo.name);
129
- const needsUpgrade = dbInfo.stores.some((s) => !hasSchema(current, s));
130
- if (!needsUpgrade) return current;
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
- const targetVersion = Math.max(current.version + 1, dbInfo.version || 1);
133
- current.close();
134
- return openAndUpgrade(dbInfo.name, targetVersion, dbInfo.stores);
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
- if (!db.objectStoreNames.contains(storeInfo.name)) return false;
143
- if (storeInfo.indexes.length === 0) return true;
144
- const store = db.transaction(storeInfo.name, 'readonly').objectStore(storeInfo.name);
145
- return storeInfo.indexes.every((ix) => store.indexNames.contains(ix.name));
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
- return new Promise((resolve, reject) => {
156
- const open = indexedDB.open(name, version);
157
- open.onupgradeneeded = () => {
158
- const db = open.result;
159
- const tx = open.transaction;
160
- for (const s of stores) {
161
- const store = db.objectStoreNames.contains(s.name)
162
- ? tx.objectStore(s.name)
163
- : db.createObjectStore(s.name, { keyPath: s.keyPath, autoIncrement: s.autoIncrement });
164
- for (const ix of s.indexes) {
165
- if (!store.indexNames.contains(ix.name)) {
166
- store.createIndex(ix.name, ix.keyPath, {
167
- unique: ix.unique,
168
- multiEntry: ix.multiEntry
169
- });
170
- }
171
- }
172
- }
173
- };
174
- open.onsuccess = () => resolve(open.result);
175
- open.onerror = () => reject(open.er open.onblocked = () => reject(blockedError(name));
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
- return new Promise((resolve, reject) => {
182
- const req = indexedDB.deleteDatabase(name);
183
- req.onsuccess = () => resolve(undefined);
184
- req.onerror = () => reject(req.error);
185
- req.onblocked = () => reject(blockedError(name));
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
- return new DOMException(
192
- `IndexedDB "${name}" is blocked by another open connection — close other tabs on this origin and retry`,
193
- 'InvalidStateError'
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
- return new Promise((resolve, reject) => {
200
- req.onsuccess = () => resolve(req.result);
201
- req.onerror = () => reject(req.error);
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
- return new Promise((resolve, reject) => {
208
- tx.oncomplete = () => resolve(undefined);
209
- tx.onabort = tx.onerror = () => reject(tx.error);
210
- });
219
+ return new Promise((resolve, reject) => {
220
+ tx.oncomplete = () => resolve(undefined);
221
+ tx.onabort = tx.onerror = () => reject(tx.error);
222
+ });
211
223
  }