Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/src/lib/libidbfs.js
4150 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
Object.values(IDBFS.dbs).forEach((value) => value.close());
118
IDBFS.dbs = {};
119
},
120
getDB: (name, callback) => {
121
// check the cache first
122
var db = IDBFS.dbs[name];
123
if (db) {
124
return callback(null, db);
125
}
126
127
var req;
128
try {
129
req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION);
130
} catch (e) {
131
return callback(e);
132
}
133
if (!req) {
134
return callback("Unable to connect to IndexedDB");
135
}
136
req.onupgradeneeded = (e) => {
137
var db = /** @type {IDBDatabase} */ (e.target.result);
138
var transaction = e.target.transaction;
139
140
var fileStore;
141
142
if (db.objectStoreNames.contains(IDBFS.DB_STORE_NAME)) {
143
fileStore = transaction.objectStore(IDBFS.DB_STORE_NAME);
144
} else {
145
fileStore = db.createObjectStore(IDBFS.DB_STORE_NAME);
146
}
147
148
if (!fileStore.indexNames.contains('timestamp')) {
149
fileStore.createIndex('timestamp', 'timestamp', { unique: false });
150
}
151
};
152
req.onsuccess = () => {
153
db = /** @type {IDBDatabase} */ (req.result);
154
155
// add to the cache
156
IDBFS.dbs[name] = db;
157
callback(null, db);
158
};
159
req.onerror = (e) => {
160
callback(e.target.error);
161
e.preventDefault();
162
};
163
},
164
getLocalSet: (mount, callback) => {
165
var entries = {};
166
167
function isRealDir(p) {
168
return p !== '.' && p !== '..';
169
};
170
function toAbsolute(root) {
171
return (p) => PATH.join2(root, p);
172
};
173
174
var check = FS.readdir(mount.mountpoint).filter(isRealDir).map(toAbsolute(mount.mountpoint));
175
176
while (check.length) {
177
var path = check.pop();
178
var stat;
179
180
try {
181
stat = FS.stat(path);
182
} catch (e) {
183
return callback(e);
184
}
185
186
if (FS.isDir(stat.mode)) {
187
check.push(...FS.readdir(path).filter(isRealDir).map(toAbsolute(path)));
188
}
189
190
entries[path] = { 'timestamp': stat.mtime };
191
}
192
193
return callback(null, { type: 'local', entries: entries });
194
},
195
getRemoteSet: (mount, callback) => {
196
var entries = {};
197
198
IDBFS.getDB(mount.mountpoint, (err, db) => {
199
if (err) return callback(err);
200
201
try {
202
var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readonly');
203
transaction.onerror = (e) => {
204
callback(e.target.error);
205
e.preventDefault();
206
};
207
208
var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
209
var index = store.index('timestamp');
210
211
index.openKeyCursor().onsuccess = (event) => {
212
var cursor = event.target.result;
213
214
if (!cursor) {
215
return callback(null, { type: 'remote', db, entries });
216
}
217
218
entries[cursor.primaryKey] = { 'timestamp': cursor.key };
219
220
cursor.continue();
221
};
222
} catch (e) {
223
return callback(e);
224
}
225
});
226
},
227
loadLocalEntry: (path, callback) => {
228
var stat, node;
229
230
try {
231
var lookup = FS.lookupPath(path);
232
node = lookup.node;
233
stat = FS.stat(path);
234
} catch (e) {
235
return callback(e);
236
}
237
238
if (FS.isDir(stat.mode)) {
239
return callback(null, { 'timestamp': stat.mtime, 'mode': stat.mode });
240
} else if (FS.isFile(stat.mode)) {
241
// Performance consideration: storing a normal JavaScript array to a IndexedDB is much slower than storing a typed array.
242
// Therefore always convert the file contents to a typed array first before writing the data to IndexedDB.
243
node.contents = MEMFS.getFileDataAsTypedArray(node);
244
return callback(null, { 'timestamp': stat.mtime, 'mode': stat.mode, 'contents': node.contents });
245
} else {
246
return callback(new Error('node type not supported'));
247
}
248
},
249
storeLocalEntry: (path, entry, callback) => {
250
try {
251
if (FS.isDir(entry['mode'])) {
252
FS.mkdirTree(path, entry['mode']);
253
} else if (FS.isFile(entry['mode'])) {
254
FS.writeFile(path, entry['contents'], { canOwn: true });
255
} else {
256
return callback(new Error('node type not supported'));
257
}
258
259
FS.chmod(path, entry['mode']);
260
FS.utime(path, entry['timestamp'], entry['timestamp']);
261
} catch (e) {
262
return callback(e);
263
}
264
265
callback(null);
266
},
267
removeLocalEntry: (path, callback) => {
268
try {
269
var stat = FS.stat(path);
270
271
if (FS.isDir(stat.mode)) {
272
FS.rmdir(path);
273
} else if (FS.isFile(stat.mode)) {
274
FS.unlink(path);
275
}
276
} catch (e) {
277
return callback(e);
278
}
279
280
callback(null);
281
},
282
loadRemoteEntry: (store, path, callback) => {
283
var req = store.get(path);
284
req.onsuccess = (event) => callback(null, event.target.result);
285
req.onerror = (e) => {
286
callback(e.target.error);
287
e.preventDefault();
288
};
289
},
290
storeRemoteEntry: (store, path, entry, callback) => {
291
try {
292
var req = store.put(entry, path);
293
} catch (e) {
294
callback(e);
295
return;
296
}
297
req.onsuccess = (event) => callback();
298
req.onerror = (e) => {
299
callback(e.target.error);
300
e.preventDefault();
301
};
302
},
303
removeRemoteEntry: (store, path, callback) => {
304
var req = store.delete(path);
305
req.onsuccess = (event) => callback();
306
req.onerror = (e) => {
307
callback(e.target.error);
308
e.preventDefault();
309
};
310
},
311
reconcile: (src, dst, callback) => {
312
var total = 0;
313
314
var create = [];
315
Object.keys(src.entries).forEach((key) => {
316
var e = src.entries[key];
317
var e2 = dst.entries[key];
318
if (!e2 || e['timestamp'].getTime() != e2['timestamp'].getTime()) {
319
create.push(key);
320
total++;
321
}
322
});
323
324
var remove = [];
325
Object.keys(dst.entries).forEach((key) => {
326
if (!src.entries[key]) {
327
remove.push(key);
328
total++;
329
}
330
});
331
332
if (!total) {
333
return callback(null);
334
}
335
336
var errored = false;
337
var db = src.type === 'remote' ? src.db : dst.db;
338
var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readwrite');
339
var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
340
341
function done(err) {
342
if (err && !errored) {
343
errored = true;
344
return callback(err);
345
}
346
};
347
348
// transaction may abort if (for example) there is a QuotaExceededError
349
transaction.onerror = transaction.onabort = (e) => {
350
done(e.target.error);
351
e.preventDefault();
352
};
353
354
transaction.oncomplete = (e) => {
355
if (!errored) {
356
callback(null);
357
}
358
};
359
360
// sort paths in ascending order so directory entries are created
361
// before the files inside them
362
create.sort().forEach((path) => {
363
if (dst.type === 'local') {
364
IDBFS.loadRemoteEntry(store, path, (err, entry) => {
365
if (err) return done(err);
366
IDBFS.storeLocalEntry(path, entry, done);
367
});
368
} else {
369
IDBFS.loadLocalEntry(path, (err, entry) => {
370
if (err) return done(err);
371
IDBFS.storeRemoteEntry(store, path, entry, done);
372
});
373
}
374
});
375
376
// sort paths in descending order so files are deleted before their
377
// parent directories
378
remove.sort().reverse().forEach((path) => {
379
if (dst.type === 'local') {
380
IDBFS.removeLocalEntry(path, done);
381
} else {
382
IDBFS.removeRemoteEntry(store, path, done);
383
}
384
});
385
}
386
}
387
});
388
389
if (WASMFS) {
390
error("using -lidbfs is not currently supported in WasmFS.");
391
}
392
393