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