forked from codesandbox/codesandbox-client
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIndexedDB.ts
More file actions
251 lines (229 loc) · 8.08 KB
/
IndexedDB.ts
File metadata and controls
251 lines (229 loc) · 8.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import {BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system';
import {AsyncKeyValueROTransaction, AsyncKeyValueRWTransaction, AsyncKeyValueStore, AsyncKeyValueFileSystem} from '../generic/key_value_filesystem';
import {ApiError, ErrorCode} from '../core/api_error';
import global from '../core/global';
import {arrayBuffer2Buffer, buffer2ArrayBuffer} from '../core/util';
/**
* Get the indexedDB constructor for the current browser.
* @hidden
*/
const indexedDB: IDBFactory = global.indexedDB ||
(<any> global).mozIndexedDB ||
(<any> global).webkitIndexedDB ||
global.msIndexedDB;
/**
* Converts a DOMException or a DOMError from an IndexedDB event into a
* standardized BrowserFS API error.
* @hidden
*/
function convertError(e: {name: string}, message: string = e.toString()): ApiError {
switch (e.name) {
case "NotFoundError":
return new ApiError(ErrorCode.ENOENT, message);
case "QuotaExceededError":
return new ApiError(ErrorCode.ENOSPC, message);
default:
// The rest do not seem to map cleanly to standard error codes.
return new ApiError(ErrorCode.EIO, message);
}
}
/**
* Produces a new onerror handler for IDB. Our errors are always fatal, so we
* handle them generically: Call the user-supplied callback with a translated
* version of the error, and let the error bubble up.
* @hidden
*/
function onErrorHandler(cb: (e: ApiError) => void, code: ErrorCode = ErrorCode.EIO, message: string | null = null): (e?: any) => void {
return function(e?: any): void {
// Prevent the error from canceling the transaction.
e.preventDefault();
cb(new ApiError(code, message !== null ? message : undefined));
};
}
/**
* @hidden
*/
export class IndexedDBROTransaction implements AsyncKeyValueROTransaction {
constructor(public tx: IDBTransaction, public store: IDBObjectStore) { }
public get(key: string, cb: BFSCallback<Buffer>): void {
try {
const r: IDBRequest = this.store.get(key);
r.onerror = onErrorHandler(cb);
r.onsuccess = (event) => {
// IDB returns the value 'undefined' when you try to get keys that
// don't exist. The caller expects this behavior.
const result: any = (<any> event.target).result;
if (result === undefined) {
cb(null, result);
} else {
// IDB data is stored as an ArrayBuffer
cb(null, arrayBuffer2Buffer(result));
}
};
} catch (e) {
cb(convertError(e));
}
}
}
/**
* @hidden
*/
export class IndexedDBRWTransaction extends IndexedDBROTransaction implements AsyncKeyValueRWTransaction, AsyncKeyValueROTransaction {
constructor(tx: IDBTransaction, store: IDBObjectStore) {
super(tx, store);
}
public put(key: string, data: Buffer, overwrite: boolean, cb: BFSCallback<boolean>): void {
try {
const arraybuffer = buffer2ArrayBuffer(data);
let r: IDBRequest;
// Note: 'add' will never overwrite an existing key.
r = overwrite ? this.store.put(arraybuffer, key) : this.store.add(arraybuffer, key);
// XXX: NEED TO RETURN FALSE WHEN ADD HAS A KEY CONFLICT. NO ERROR.
r.onerror = onErrorHandler(cb);
r.onsuccess = (event) => {
cb(null, true);
};
} catch (e) {
cb(convertError(e));
}
}
public del(key: string, cb: BFSOneArgCallback): void {
try {
// NOTE: IE8 has a bug with identifiers named 'delete' unless used as a string
// like this.
// http://stackoverflow.com/a/26479152
const r: IDBRequest = this.store['delete'](key);
r.onerror = onErrorHandler(cb);
r.onsuccess = (event) => {
cb();
};
} catch (e) {
cb(convertError(e));
}
}
public commit(cb: BFSOneArgCallback): void {
// Return to the event loop to commit the transaction.
setTimeout(cb, 0);
}
public abort(cb: BFSOneArgCallback): void {
let _e: ApiError | null = null;
try {
this.tx.abort();
} catch (e) {
_e = convertError(e);
} finally {
cb(_e);
}
}
}
export class IndexedDBStore implements AsyncKeyValueStore {
public static Create(storeName: string, cb: BFSCallback<IndexedDBStore>): void {
const openReq: IDBOpenDBRequest = indexedDB.open(storeName, 1);
openReq.onupgradeneeded = (event) => {
const db: IDBDatabase = (<any> event.target).result;
// Huh. This should never happen; we're at version 1. Why does another
// database exist?
if (db.objectStoreNames.contains(storeName)) {
db.deleteObjectStore(storeName);
}
db.createObjectStore(storeName);
};
openReq.onsuccess = (event) => {
cb(null, new IndexedDBStore((<any> event.target).result, storeName));
};
openReq.onerror = onErrorHandler(cb, ErrorCode.EACCES);
}
constructor(private db: IDBDatabase, private storeName: string) {
}
public name(): string {
return IndexedDBFileSystem.Name + " - " + this.storeName;
}
public clear(cb: BFSOneArgCallback): void {
try {
const tx = this.db.transaction(this.storeName, 'readwrite'),
objectStore = tx.objectStore(this.storeName),
r: IDBRequest = objectStore.clear();
r.onsuccess = (event) => {
// Use setTimeout to commit transaction.
setTimeout(cb, 0);
};
r.onerror = onErrorHandler(cb);
} catch (e) {
cb(convertError(e));
}
}
public beginTransaction(type: 'readonly'): AsyncKeyValueROTransaction;
public beginTransaction(type: 'readwrite'): AsyncKeyValueRWTransaction;
public beginTransaction(type: 'readonly' | 'readwrite' = 'readonly'): AsyncKeyValueROTransaction {
const tx = this.db.transaction(this.storeName, type),
objectStore = tx.objectStore(this.storeName);
if (type === 'readwrite') {
return new IndexedDBRWTransaction(tx, objectStore);
} else if (type === 'readonly') {
return new IndexedDBROTransaction(tx, objectStore);
} else {
throw new ApiError(ErrorCode.EINVAL, 'Invalid transaction type.');
}
}
}
/**
* Configuration options for the IndexedDB file system.
*/
export interface IndexedDBFileSystemOptions {
// The name of this file system. You can have multiple IndexedDB file systems operating
// at once, but each must have a different name.
storeName?: string;
// The size of the inode cache. Defaults to 100. A size of 0 or below disables caching.
cacheSize?: number;
}
/**
* A file system that uses the IndexedDB key value file system.
*/
export default class IndexedDBFileSystem extends AsyncKeyValueFileSystem {
public static readonly Name = "IndexedDB";
public static readonly Options: FileSystemOptions = {
storeName: {
type: "string",
optional: true,
description: "The name of this file system. You can have multiple IndexedDB file systems operating at once, but each must have a different name."
},
cacheSize: {
type: "number",
optional: true,
description: "The size of the inode cache. Defaults to 100. A size of 0 or below disables caching."
}
};
/**
* Constructs an IndexedDB file system with the given options.
*/
public static Create(opts: IndexedDBFileSystemOptions, cb: BFSCallback<IndexedDBFileSystem>): void {
IndexedDBStore.Create(opts.storeName ? opts.storeName : 'browserfs', (e, store?) => {
if (store) {
const idbfs = new IndexedDBFileSystem(typeof(opts.cacheSize) === 'number' ? opts.cacheSize : 100);
idbfs.init(store, (e) => {
if (e) {
cb(e);
} else {
cb(null, idbfs);
}
});
} else {
cb(e);
}
});
}
public static isAvailable(): boolean {
// In Safari's private browsing mode, indexedDB.open returns NULL.
// In Firefox, it throws an exception.
// In Chrome, it "just works", and clears the database when you leave the page.
// Untested: Opera, IE.
try {
return typeof indexedDB !== 'undefined' && null !== indexedDB.open("__browserfs_test__");
} catch (e) {
return false;
}
}
private constructor(cacheSize: number) {
super(cacheSize);
}
}