@vanillaspa/sqlite-database 1.0.0 → 1.2.0
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/index.js +71 -121
- package/package.json +4 -2
- package/sqliteWorker.js +93 -83
package/index.js
CHANGED
|
@@ -1,153 +1,103 @@
|
|
|
1
1
|
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
|
2
|
+
import { addEventListener, dispatchEvent } from '@vanillaspa/event-bus';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
//
|
|
4
|
+
if (!window.Worker) throw new Error(`Your browser doesn't support web workers.`);
|
|
5
|
+
export const name = "sqlite"; // module name
|
|
6
|
+
try {
|
|
7
|
+
const sqlite3 = await sqlite3InitModule({ print: console.log, printErr: console.error });
|
|
8
|
+
console.log('SQLite3 version:', sqlite3.version.libVersion);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
console.error('Initialization error:', err.name, err.message);
|
|
11
|
+
}
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
const workers = new Map();
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
function getWorker(name = 'default') {
|
|
16
|
+
const worker = workers.get(name);
|
|
17
|
+
if (!worker) throw new Error(`No worker for "${name}"`);
|
|
18
|
+
return worker;
|
|
19
|
+
}
|
|
9
20
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
worker.terminate();
|
|
15
|
-
} else {
|
|
16
|
-
workers[name] = worker;
|
|
17
|
-
}
|
|
21
|
+
function initializeWorker(name) {
|
|
22
|
+
if (workers.has(name)) throw new Error(`Worker "${name}" already exists.`);
|
|
23
|
+
const worker = new Worker(new URL('./sqliteWorker.js', import.meta.url), { type: 'module' });
|
|
24
|
+
workers.set(name, worker);
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
function enqueue(worker, payload) {
|
|
28
|
+
const { port1, port2 } = new MessageChannel();
|
|
21
29
|
return new Promise((resolve, reject) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
reject(new Error(error));
|
|
30
|
+
port1.onmessage = ({ data }) => {
|
|
31
|
+
port1.close();
|
|
32
|
+
data.type === 'error'
|
|
33
|
+
? reject(new Error(data.message))
|
|
34
|
+
: resolve(data.result);
|
|
35
|
+
};
|
|
36
|
+
port1.onmessageerror = () => {
|
|
37
|
+
port1.close();
|
|
38
|
+
reject(new Error('MessageChannel deserialization error'));
|
|
32
39
|
};
|
|
33
|
-
worker.postMessage(
|
|
34
|
-
})
|
|
40
|
+
worker.postMessage(payload, [port2]);
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Public API
|
|
45
|
+
export function createDB(name = 'default') {
|
|
46
|
+
initializeWorker(name);
|
|
47
|
+
return enqueue(getWorker(name), { action: 'createDB', name });
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
export async function deleteAndTerminateDB(name) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log("Removing...", fileSystemFileHandle);
|
|
46
|
-
await fileSystemFileHandle.remove();
|
|
47
|
-
await worker.terminate();
|
|
48
|
-
}
|
|
49
|
-
delete workers[name];
|
|
50
|
-
}
|
|
51
|
-
worker.postMessage({ action: 'closeDB' });
|
|
52
|
-
}
|
|
51
|
+
const worker = getWorker(name);
|
|
52
|
+
await enqueue(worker, { action: 'closeDB' })
|
|
53
|
+
const root = await navigator.storage.getDirectory();
|
|
54
|
+
const fileHandle = await root.getFileHandle(`${name}.sqlite3`).catch(() => null);
|
|
55
|
+
if (fileHandle) await fileHandle.remove();
|
|
56
|
+
worker.terminate();
|
|
57
|
+
workers.delete(name);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function downloadDB(name = 'default') {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const { type } = data;
|
|
60
|
-
if (type === 'application/vnd.sqlite3') {
|
|
61
|
-
let downloadChannel = new BroadcastChannel("download_channel");
|
|
62
|
-
downloadChannel.postMessage(data);
|
|
63
|
-
downloadChannel.close();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
worker.postMessage({ action: 'downloadDB' });
|
|
67
|
-
}
|
|
61
|
+
enqueue(getWorker(name), { action: 'downloadDB' }).then(blob => {
|
|
62
|
+
dispatchEvent(new CustomEvent('sqlite:download', { detail: { blob, name } }))
|
|
63
|
+
})
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
export function executeQuery(sql, name = 'default') {
|
|
71
|
-
return
|
|
72
|
-
let worker = getWorker(name);
|
|
73
|
-
if (worker) {
|
|
74
|
-
worker.onmessage = function ({ data }) {
|
|
75
|
-
const { type } = data;
|
|
76
|
-
if (type === 'application/json') {
|
|
77
|
-
const { result } = data;
|
|
78
|
-
resolve(result);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
worker.onerror = (error) => {
|
|
82
|
-
reject(error);
|
|
83
|
-
};
|
|
84
|
-
worker.postMessage({ action: "executeQuery", sql });
|
|
85
|
-
} else {
|
|
86
|
-
reject(new Error("No worker"));
|
|
87
|
-
}
|
|
88
|
-
});
|
|
67
|
+
return enqueue(getWorker(name), { action: "executeQuery", sql });
|
|
89
68
|
}
|
|
90
69
|
|
|
91
70
|
export function executeStatement({ sql, values, name = "default" }) {
|
|
92
|
-
return
|
|
93
|
-
let worker = getWorker(name);
|
|
94
|
-
if (worker) {
|
|
95
|
-
worker.onmessage = function ({ data }) {
|
|
96
|
-
const { type } = data;
|
|
97
|
-
if (type === 'application/json') {
|
|
98
|
-
const { result } = data;
|
|
99
|
-
resolve(result);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
worker.onerror = (error) => {
|
|
103
|
-
reject(error);
|
|
104
|
-
};
|
|
105
|
-
worker.postMessage({ action: "prepareStatement", sql, values });
|
|
106
|
-
} else {
|
|
107
|
-
reject(new Error("No worker"));
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function getWorker(name = 'default') {
|
|
113
|
-
let worker = workers[name];
|
|
114
|
-
return worker ? worker : undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function getWorkers() {
|
|
118
|
-
return workers;
|
|
71
|
+
return enqueue(getWorker(name), { action: "prepareStatement", sql, values });
|
|
119
72
|
}
|
|
120
73
|
|
|
121
74
|
export function uploadDB(fileName, arrayBuffer) {
|
|
122
|
-
|
|
123
|
-
if (['sqlite', 'sqlite3'].includes(extension)) {
|
|
124
|
-
|
|
125
|
-
if (!worker) {
|
|
126
|
-
initalizeWorker(name);
|
|
127
|
-
worker = getWorker(name);
|
|
128
|
-
console.log({worker})
|
|
129
|
-
} // TODO: allow overwrite
|
|
130
|
-
worker.postMessage({ action: 'uploadDB', name, arrayBuffer });
|
|
131
|
-
} else {
|
|
132
|
-
throw new Error({ name: "UnsupportedError", message: "Unsupported extension" });
|
|
75
|
+
const [name, extension] = fileName.split(".");
|
|
76
|
+
if (!['sqlite', 'sqlite3'].includes(extension)) {
|
|
77
|
+
throw new Error(`UnsupportedError: Unsupported extension ".${extension}"`);
|
|
133
78
|
}
|
|
79
|
+
if (!workers.has(name)) initializeWorker(name);
|
|
80
|
+
return enqueue(workers.get(name), { action: 'uploadDB', name, arrayBuffer });
|
|
134
81
|
}
|
|
135
82
|
|
|
136
83
|
export function terminate(name = 'default') {
|
|
137
|
-
|
|
84
|
+
const worker = workers.get(name);
|
|
138
85
|
if (worker) {
|
|
139
|
-
worker.
|
|
140
|
-
|
|
86
|
+
worker.terminate();
|
|
87
|
+
workers.delete(name);
|
|
88
|
+
}
|
|
141
89
|
}
|
|
142
90
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// instantiation test
|
|
146
|
-
const sqlite3 = await sqlite3InitModule({ print: console.log, printErr: console.error });
|
|
147
|
-
console.log('Running SQLite3 version', sqlite3.version.libVersion);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
console.error('Initialization error:', err.name, err.message);
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
console.error('Your browser doesn\'t support web workers.');
|
|
91
|
+
export function getWorkers() {
|
|
92
|
+
return workers;
|
|
153
93
|
}
|
|
94
|
+
|
|
95
|
+
addEventListener('sqlite:download', (event) => {
|
|
96
|
+
const { blob, name } = event.detail;
|
|
97
|
+
const url = URL.createObjectURL(blob);
|
|
98
|
+
const a = document.createElement('a');
|
|
99
|
+
a.href = url;
|
|
100
|
+
a.download = `${name}.sqlite3`;
|
|
101
|
+
a.click();
|
|
102
|
+
URL.revokeObjectURL(url);
|
|
103
|
+
});
|
package/package.json
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
},
|
|
6
6
|
"description": "A serverless SQLite database inside your browser.",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@sqlite.org/sqlite-wasm":"^3.51.2-build6"
|
|
8
|
+
"@sqlite.org/sqlite-wasm":"^3.51.2-build6",
|
|
9
|
+
"@vanillaspa/event-bus": "1.1.0"
|
|
9
10
|
},
|
|
10
11
|
"homepage": "https://github.com/vanillaspa/sqlite-database#readme",
|
|
11
12
|
"keywords": [
|
|
12
13
|
"Database",
|
|
14
|
+
"EventBus",
|
|
13
15
|
"JavaScript",
|
|
14
16
|
"OPFS",
|
|
15
17
|
"SPA",
|
|
@@ -28,5 +30,5 @@
|
|
|
28
30
|
"url": "git+https://github.com/vanillaspa/sqlite-database.git"
|
|
29
31
|
},
|
|
30
32
|
"type": "module",
|
|
31
|
-
"version": "1.
|
|
33
|
+
"version": "1.2.0"
|
|
32
34
|
}
|
package/sqliteWorker.js
CHANGED
|
@@ -1,31 +1,71 @@
|
|
|
1
1
|
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
|
2
|
+
import { dispatchEvent } from '@vanillaspa/event-bus';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
let db = null;
|
|
5
|
+
let sqlite3 = null;
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
async function getInstance() {
|
|
8
|
+
if (!sqlite3) {
|
|
9
|
+
sqlite3 = await sqlite3InitModule({ print: console.log, printErr: console.error });
|
|
10
|
+
}
|
|
11
|
+
return sqlite3;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function reply(port, result) {
|
|
15
|
+
port.postMessage({ type: 'application/json', result });
|
|
16
|
+
port.close();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function replyError(message) {
|
|
20
|
+
port.postMessage({ type: 'error', message });
|
|
21
|
+
port.close();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleSQLiteError(port, sql, e) {
|
|
25
|
+
if (e.message.includes('SQLITE_CANTOPEN')) {
|
|
26
|
+
console.info("Info: No SQLite database available. Upload a new database or reload the page.");
|
|
27
|
+
} else if (e.message.includes('SQLITE_CONSTRAINT_UNIQUE')) {
|
|
28
|
+
console.error('Unique constraint violation:', sql, e.message);
|
|
29
|
+
} else {
|
|
30
|
+
console.error("Error executing SQL_", sql, e.message);
|
|
31
|
+
}
|
|
32
|
+
replyError(port, e.message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onmessage = async function ({ data, ports }) {
|
|
7
36
|
const { action } = data;
|
|
37
|
+
const port = ports[0] ?? null;
|
|
38
|
+
|
|
8
39
|
switch (action) {
|
|
9
40
|
case 'createDB': {
|
|
10
41
|
const { name } = data;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
try {
|
|
43
|
+
const { newDB, message } = await createDatabase(name)
|
|
44
|
+
db = newDB;
|
|
45
|
+
reply(port, message);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
replyError(port, e.message)
|
|
48
|
+
}
|
|
14
49
|
break;
|
|
15
50
|
}
|
|
51
|
+
case 'downloadDB': {
|
|
52
|
+
try {
|
|
53
|
+
const byteArray = sqlite3.capi.sqlite3_js_db_export(db);
|
|
54
|
+
const blob = new Blob([byteArray.buffer], { type: "application/vnd.sqlite3" });
|
|
55
|
+
reply(port, blob);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
replyError(port, e.message);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
case 'executeQuery': {
|
|
17
63
|
const { sql } = data;
|
|
18
64
|
try {
|
|
19
|
-
const result =
|
|
20
|
-
|
|
21
|
-
postMessage({ result, type: "application/json" });
|
|
65
|
+
const result = db.exec({ sql, returnValue: "resultRows" });
|
|
66
|
+
reply(port, result);
|
|
22
67
|
} catch (e) {
|
|
23
|
-
|
|
24
|
-
console.info("Info: Currently no SQLite database available for this worker. Upload a new database or reload the page.");
|
|
25
|
-
}
|
|
26
|
-
if (e.message.indexOf("SQLITE_CONSTRAINT_UNIQUE") != -1) {
|
|
27
|
-
console.error("Error executing SQL statement", sql, e.message);
|
|
28
|
-
}
|
|
68
|
+
handleSQLiteError(port, sql, e)
|
|
29
69
|
}
|
|
30
70
|
break;
|
|
31
71
|
}
|
|
@@ -33,102 +73,72 @@ onmessage = async function ({ data }) {
|
|
|
33
73
|
const { sql, values } = data;
|
|
34
74
|
let stmt;
|
|
35
75
|
try {
|
|
36
|
-
|
|
37
|
-
stmt = await db.prepare(sql, values);
|
|
38
|
-
const columns = stmt.getColumnNames();
|
|
39
|
-
// console.debug("columns", columns);
|
|
76
|
+
stmt = db.prepare(sql);
|
|
40
77
|
stmt.bind(values);
|
|
41
|
-
|
|
78
|
+
const columns = stmt.getColumnNames();
|
|
42
79
|
const result = [];
|
|
43
80
|
while (stmt.step()) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return [columnName, row[index]];
|
|
47
|
-
});
|
|
48
|
-
let obj = Object.fromEntries(zipped);
|
|
49
|
-
result.push(obj);
|
|
81
|
+
const row = stmt.get([]);
|
|
82
|
+
result.push(Object.fromEntries(columns.map((columnName, index) => [columnName, row[index]])));
|
|
50
83
|
}
|
|
51
|
-
|
|
52
|
-
postMessage({ result, type: "application/json" });
|
|
84
|
+
reply(port, result);
|
|
53
85
|
} catch (e) {
|
|
54
|
-
|
|
55
|
-
console.info("Info: Currently no SQLite database available for this worker. Upload a new database or reload the page.");
|
|
56
|
-
} else if (e.message.indexOf("SQLITE_CONSTRAINT_UNIQUE") != -1) {
|
|
57
|
-
console.error("Error executing SQL statement", sql, e.message);
|
|
58
|
-
} else {
|
|
59
|
-
console.error("Error executing SQL statement", sql, e.message);
|
|
60
|
-
}
|
|
86
|
+
handleSQLiteError(port, sql, e);
|
|
61
87
|
} finally {
|
|
62
|
-
stmt
|
|
88
|
+
stmt?.finalize();
|
|
63
89
|
}
|
|
64
90
|
break;
|
|
65
91
|
}
|
|
66
|
-
case 'uploadDB':
|
|
92
|
+
case 'uploadDB': {
|
|
67
93
|
const { name, arrayBuffer } = data;
|
|
68
|
-
const { message } = await uploadDatabase(name, arrayBuffer)
|
|
69
|
-
console.log(message, db);
|
|
70
|
-
break;
|
|
71
|
-
case 'downloadDB':
|
|
72
94
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
postMessage(blob); // send the database Blob to the API
|
|
95
|
+
const message = await uploadDatabase(name, arrayBuffer)
|
|
96
|
+
reply(port, message);
|
|
76
97
|
} catch (e) {
|
|
77
|
-
|
|
78
|
-
postMessage({ type: "application/vnd.sqlite3", error: "SQLITE_NOMEM" });
|
|
79
|
-
else
|
|
80
|
-
console.error(e);
|
|
98
|
+
replyError(port, e.message);
|
|
81
99
|
}
|
|
82
100
|
break;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
}
|
|
102
|
+
case 'closeDB': {
|
|
103
|
+
try {
|
|
104
|
+
closeDB();
|
|
105
|
+
reply(port, null);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
replyError(port, e.message);
|
|
108
|
+
}
|
|
86
109
|
break;
|
|
110
|
+
}
|
|
87
111
|
default:
|
|
88
|
-
console.
|
|
112
|
+
console.warn('Unknown action:', data)
|
|
89
113
|
}
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
async function createDatabase(name) {
|
|
93
|
-
const
|
|
94
|
-
return 'opfs' in
|
|
95
|
-
? {
|
|
96
|
-
|
|
117
|
+
const instance = await getInstance();
|
|
118
|
+
return 'opfs' in instance
|
|
119
|
+
? {
|
|
120
|
+
newDB: new instance.oo1.OpfsDb(`/${name}.sqlite3`),
|
|
121
|
+
message: `OPFS is available, created persisted database at /${name}.sqlite3`
|
|
122
|
+
}
|
|
123
|
+
: {
|
|
124
|
+
newDB: new instance.oo1.DB(`/${name}.sqlite3`, 'ct'),
|
|
125
|
+
message: `OPFS is not available, created transient database /${name}.sqlite3`
|
|
126
|
+
};
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
async function uploadDatabase(name, arrayBuffer) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { message: `New DB imported as ${name}.sqlite3. (${arrayBuffer.byteLength} Bytes)` }
|
|
107
|
-
} else {
|
|
108
|
-
throw new Error({ name: "ImportError", message: "Empty size" })
|
|
109
|
-
}
|
|
110
|
-
} else { // TODO allow alternative
|
|
111
|
-
throw new Error({ name: "OPFSMissingError", message: "Unsupported operation due to missing OPFS support." });
|
|
112
|
-
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.error(err.name, err.message);
|
|
115
|
-
}
|
|
130
|
+
const instance = await getInstance();
|
|
131
|
+
if (!('opfs' in instance)) throw new Error('OPFSMissingError: Unsupported operation due to missing OPFS support.');
|
|
132
|
+
const size = await instance.oo1.OpfsDb.importDb(`${name}.sqlite3`, arrayBuffer);
|
|
133
|
+
if (!size) throw new Error('ImportError: Empty size after import.');
|
|
134
|
+
db = new instance.oo1.OpfsDb(`/${name}.sqlite3`);
|
|
135
|
+
return `New DB imported as ${name}.sqlite3. (${arrayBuffer.byteLength} Bytes)`;
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
function closeDB() {
|
|
119
139
|
if (db) {
|
|
120
140
|
console.log("Closing...", db);
|
|
121
141
|
db.close();
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function getInstance() {
|
|
126
|
-
try {
|
|
127
|
-
if (!sqlite3) {
|
|
128
|
-
sqlite3 = await sqlite3InitModule({ print: console.log, printErr: console.error });
|
|
129
|
-
}
|
|
130
|
-
return sqlite3;
|
|
131
|
-
} catch (err) {
|
|
132
|
-
console.error(err.name, err.message);
|
|
142
|
+
db = null;
|
|
133
143
|
}
|
|
134
144
|
}
|