Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/src/lib/libidbfs.js
6162 views
1
/**
2
* @license
3
* Copyright 2013 The Emscripten Authors
4
* SPDX-License-Identifier: MIT
5
*/
6
7
addToLibrary({
8
$IDBFS__deps: ['$FS', '$MEMFS', '$PATH'],
9
$IDBFS__postset: () => {
10
addAtExit('IDBFS.quit();');
11
return '';
12
},
13
$IDBFS: {
14
dbs: {},
15
indexedDB: () => {
16
#if ASSERTIONS
17
assert(typeof indexedDB != 'undefined', 'IDBFS used, but indexedDB not supported');
18
#endif
19
return indexedDB;
20
},
21
DB_VERSION: 21,
22
DB_STORE_NAME: 'FILE_DATA',
23
24
// Queues a new VFS -> IDBFS synchronization operation
25
queuePersist: (mount) => {
26
function onPersistComplete() {
27
if (mount.idbPersistState === 'again') startPersist(); // If a new sync request has appeared in between, kick off a new sync
28
else mount.idbPersistState = 0; // Otherwise reset sync state back to idle to wait for a new sync later
29
}
30
function startPersist() {
31
mount.idbPersistState = 'idb'; // Mark that we are currently running a sync operation
32
IDBFS.syncfs(mount, /*populate:*/false, onPersistComplete);
33
}
34
35
if (!mount.idbPersistState) {
36
// Programs typically write/copy/move multiple files in the in-memory
37
// filesystem within a single app frame, so when a filesystem sync
38
// command is triggered, do not start it immediately, but only after
39
// the current frame is finished. This way all the modified files
40
// inside the main loop tick will be batched up to the same sync.
41
mount.idbPersistState = setTimeout(startPersist, 0);
42
} else if (mount.idbPersistState === 'idb') {
43
// There is an active IndexedDB sync operation in-flight, but we now
44
// have accumulated more files to sync. We should therefore queue up
45
// a new sync after the current one finishes so that all writes
46
// will be properly persisted.
47
mount.idbPersistState = 'again';
48
}
49
},
50
51
mount: (mount) => {
52
// reuse core MEMFS functionality
53
var mnt = MEMFS.mount(mount);
54
// If the automatic IDBFS persistence option has been selected, then automatically persist
55
// all modifications to the filesystem as they occur.
56
if (mount?.opts?.autoPersist) {
57
mount.idbPersistState = 0; // IndexedDB sync starts in idle state
58
var memfs_node_ops = mnt.node_ops;
59
mnt.node_ops = {...mnt.node_ops}; // Clone node_ops to inject write tracking
60
mnt.node_ops.mknod = (parent, name, mode, dev) => {
61
var node = memfs_node_ops.mknod(parent, name, mode, dev);
62
// Propagate injected node_ops to the newly created child node
63
node.node_ops = mnt.node_ops;
64
// Remember for each IDBFS node which IDBFS mount point they came from so we know which mount to persist on modification.
65
node.idbfs_mount = mnt.mount;
66
// Remember original MEMFS stream_ops for this node
67
node.memfs_stream_ops = node.stream_ops;
68
// Clone stream_ops to inject write tracking
69
node.stream_ops = {...node.stream_ops};
70
71
// Track all file writes
72
node.stream_ops.write = (stream, buffer, offset, length, position, canOwn) => {
73
// This file has been modified, we must persist IndexedDB when this file closes
74
stream.node.isModified = true;
75
return node.memfs_stream_ops.write(stream, buffer, offset, length, position, canOwn);
76
};
77
78
// Persist IndexedDB on file close
79
node.stream_ops.close = (stream) => {
80
var n = stream.node;
81
if (n.isModified) {
82
IDBFS.queuePersist(n.idbfs_mount);
83
n.isModified = false;
84
}
85
if (n.memfs_stream_ops.close) return n.memfs_stream_ops.close(stream);
86
};
87
88
// Persist the node we just created to IndexedDB
89
IDBFS.queuePersist(mnt.mount);
90
91
return node;
92
};
93
// Also kick off persisting the filesystem on other operations that modify the filesystem.
94
mnt.node_ops.rmdir = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.rmdir(...args));
95
mnt.node_ops.symlink = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.symlink(...args));
96
mnt.node_ops.unlink = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.unlink(...args));
97
mnt.node_ops.rename = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.rename(...args));
98
}
99
return mnt;
100
},
101
102
syncfs: (mount, populate, callback) => {
103
IDBFS.getLocalSet(mount, (err, local) => {
104
if (err) return callback(err);
105
106
IDBFS.getRemoteSet(mount, (err, remote) => {
107
if (err) return callback(err);
108
109
var src = populate ? remote : local;
110
var dst = populate ? local : remote;
111
112
IDBFS.reconcile(src, dst, callback);
113
});
114
});
115
},
116
quit: () => {
117
for (var value of Object.values(IDBFS.dbs)) {
118
value.close()
119
}
120
IDBFS.dbs = {};
121
},
122
getDB: (name, callback) => {
123
// check the cache first
124
var db = IDBFS.dbs[name];
125
if (db) {
126
return callback(null, db);
127
}
128
129
var req;
130
try {
131
req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION);
132
} catch (e) {
133
return callback(e);
134
}
135
if (!req) {
136
return callback("Unable to connect to IndexedDB");
137
}
138
req.onupgradeneeded = (e) => {
139
var db = /** @type {IDBDatabase} */ (e.target.result);
140
var transaction = e.target.transaction;
141
142
var fileStore;
143
144
if (db.objectStoreNames.contains(IDBFS.DB_STORE_NAME)) {
145
fileStore = transaction.objectStore(IDBFS.DB_STORE_NAME);
146
} else {
147
fileStore = db.createObjectStore(IDBFS.DB_STORE_NAME);
148
}
149
150
if (!fileStore.indexNames.contains('timestamp')) {
151
fileStore.createIndex('timestamp', 'timestamp', { unique: false });
152
}
153
};
154
req.onsuccess = () => {
155
db = /** @type {IDBDatabase} */ (req.result);
156
157
// add to the cache
158
IDBFS.dbs[name] = db;
159
callback(null, db);
160
};
161
req.onerror = (e) => {
162
callback(e.target.error);
163
e.preventDefault();
164
};
165
},
166
getLocalSet: (mount, callback) => {
167
var entries = {};
168
169
function isRealDir(p) {
170
return p !== '.' && p !== '..';
171
};
172
function toAbsolute(root) {
173
return (p) => PATH.join2(root, p);
174
};
175
176
var check = FS.readdir(mount.mountpoint).filter(isRealDir).map(toAbsolute(mount.mountpoint));
177
178
while (check.length) {
179
var path = check.pop();
180
var stat;
181
182
try {
183
stat = FS.stat(path);
184
} catch (e) {
185
return callback(e);
186
}
187
188
if (FS.isDir(stat.mode)) {
189
check.push(...FS.readdir(path).filter(isRealDir).map(toAbsolute(path)));
190
}
191
192
entries[path] = { 'timestamp': stat.mtime };
193
}
194
195
return callback(null, { type: 'local', entries: entries });
196
},
197
getRemoteSet: (mount, callback) => {
198
var entries = {};
199
200
IDBFS.getDB(mount.mountpoint, (err, db) => {
201
if (err) return callback(err);
202
203
try {
204
var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readonly');
205
transaction.onerror = (e) => {
206
callback(e.target.error);
207
e.preventDefault();
208
};
209
210
var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
211
var index = store.index('timestamp');
212
213
index.openKeyCursor().onsuccess = (event) => {
214
var cursor = event.target.result;
215
216
if (!cursor) {
217
return callback(null, { type: 'remote', db, entries });
218
}
219
220
entries[cursor.primaryKey] = { 'timestamp': cursor.key };
221
222
cursor.continue();
223
};
224
} catch (e) {
225
return callback(e);
226
}
227
});
228
},
229
loadLocalEntry: (path, callback) => {
230
var stat, node;
231
232
try {
233
var lookup = FS.lookupPath(path);
234
node = lookup.node;
235
stat = FS.stat(path);
236
} catch (e) {
237
return callback(e);
238
}
239
240
if (FS.isDir(stat.mode)) {
241
return callback(null, { 'timestamp': stat.mtime, 'mode': stat.mode });
242
} else if (FS.isFile(stat.mode)) {
243
// Performance consideration: storing a normal JavaScript array to a IndexedDB is much slower than storing a typed array.
244
// Therefore always convert the file contents to a typed array first before writing the data to IndexedDB.
245
node.contents = MEMFS.getFileDataAsTypedArray(node);
246
return callback(null, { 'timestamp': stat.mtime, 'mode': stat.mode, 'contents': node.contents });
247
} else {
248
return callback(new Error('node type not supported'));
249
}
250
},
251
storeLocalEntry: (path, entry, callback) => {
252
try {
253
if (FS.isDir(entry['mode'])) {
254
FS.mkdirTree(path, entry['mode']);
255
} else if (FS.isFile(entry['mode'])) {
256
FS.writeFile(path, entry['contents'], { canOwn: true });
257
} else {
258
return callback(new Error('node type not supported'));
259
}
260
261
FS.chmod(path, entry['mode']);
262
FS.utime(path, entry['timestamp'], entry['timestamp']);
263
} catch (e) {
264
return callback(e);
265
}
266
267
callback(null);
268
},
269
removeLocalEntry: (path, callback) => {
270
try {
271
var stat = FS.stat(path);
272
273
if (FS.isDir(stat.mode)) {
274
FS.rmdir(path);
275
} else if (FS.isFile(stat.mode)) {
276
FS.unlink(path);
277
}
278
} catch (e) {
279
return callback(e);
280
}
281
282
callback(null);
283
},
284
loadRemoteEntry: (store, path, callback) => {
285
var req = store.get(path);
286
req.onsuccess = (event) => callback(null, event.target.result);
287
req.onerror = (e) => {
288
callback(e.target.error);
289
e.preventDefault();
290
};
291
},
292
storeRemoteEntry: (store, path, entry, callback) => {
293
try {
294
var req = store.put(entry, path);
295
} catch (e) {
296
callback(e);
297
return;
298
}
299
req.onsuccess = (event) => callback();
300
req.onerror = (e) => {
301
callback(e.target.error);
302
e.preventDefault();
303
};
304
},
305
removeRemoteEntry: (store, path, callback) => {
306
var req = store.delete(path);
307
req.onsuccess = (event) => callback();
308
req.onerror = (e) => {
309
callback(e.target.error);
310
e.preventDefault();
311
};
312
},
313
reconcile: (src, dst, callback) => {
314
var total = 0;
315
316
var create = [];
317
for (var [key, e] of Object.entries(src.entries)) {
318
var e2 = dst.entries[key];
319
if (!e2 || e['timestamp'].getTime() != e2['timestamp'].getTime()) {
320
create.push(key);
321
total++;
322
}
323
}
324
325
var remove = [];
326
for (var key of Object.keys(dst.entries)) {
327
if (!src.entries[key]) {
328
remove.push(key);
329
total++;
330
}
331
}
332
333
if (!total) {
334
return callback(null);
335
}
336
337
var errored = false;
338
var db = src.type === 'remote' ? src.db : dst.db;
339
var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readwrite');
340
var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
341
342
function done(err) {
343
if (err && !errored) {
344
errored = true;
345
return callback(err);
346
}
347
};
348
349
// transaction may abort if (for example) there is a QuotaExceededError
350
transaction.onerror = transaction.onabort = (e) => {
351
done(e.target.error);
352
e.preventDefault();
353
};
354
355
transaction.oncomplete = (e) => {
356
if (!errored) {
357
callback(null);
358
}
359
};
360
361
// sort paths in ascending order so directory entries are created
362
// before the files inside them
363
for (const path of create.sort()) {
364
if (dst.type === 'local') {
365
IDBFS.loadRemoteEntry(store, path, (err, entry) => {
366
if (err) return done(err);
367
IDBFS.storeLocalEntry(path, entry, done);
368
});
369
} else {
370
IDBFS.loadLocalEntry(path, (err, entry) => {
371
if (err) return done(err);
372
IDBFS.storeRemoteEntry(store, path, entry, done);
373
});
374
}
375
}
376
377
// sort paths in descending order so files are deleted before their
378
// parent directories
379
for (var path of remove.sort().reverse()) {
380
if (dst.type === 'local') {
381
IDBFS.removeLocalEntry(path, done);
382
} else {
383
IDBFS.removeRemoteEntry(store, path, done);
384
}
385
}
386
}
387
}
388
});
389
390
if (WASMFS) {
391
error("using -lidbfs is not currently supported in WasmFS.");
392
}
393
394