@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.
Files changed (3) hide show
  1. package/index.js +71 -121
  2. package/package.json +4 -2
  3. 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
- // https://www.npmjs.com/package/@sqlite.org/sqlite-wasm
4
- // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
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
- export const name = "sqlite";
13
+ const workers = new Map();
7
14
 
8
- const workers = {};
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 initalizeWorker(name) {
11
- let worker = new Worker(new URL('./sqliteWorker.js', import.meta.url), { type: 'module' });
12
- if (workers[name]) {
13
- console.error("InstantiationError: already taken");
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
- export function createDB(name = 'default') {
27
+ function enqueue(worker, payload) {
28
+ const { port1, port2 } = new MessageChannel();
21
29
  return new Promise((resolve, reject) => {
22
- initalizeWorker(name);
23
- let worker = getWorker(name);
24
- worker.onmessage = function ({ data }) {
25
- const { type, message } = data;
26
- if (type === 'created') {
27
- resolve({ message });
28
- }
29
- }
30
- worker.onerror = (error) => {
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({ action: 'createDB', name });
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
- var root = await navigator.storage.getDirectory();
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
- }
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
- let worker = workers[name];
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
- }
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 new Promise((resolve, reject) => {
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 new Promise((resolve, reject) => {
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
- let [name, extension] = fileName.split(".");
123
- if (['sqlite', 'sqlite3'].includes(extension)) {
124
- let worker = workers[name];
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
- let worker = workers[name];
84
+ const worker = workers.get(name);
138
85
  if (worker) {
139
- worker.postMessage({ command: 'terminate' });
140
- }
86
+ worker.terminate();
87
+ workers.delete(name);
88
+ }
141
89
  }
142
90
 
143
- if (window.Worker) {
144
- try {
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.0.0"
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
- var db = null;
4
- var sqlite3 = null;
4
+ let db = null;
5
+ let sqlite3 = null;
5
6
 
6
- onmessage = async function ({ data }) {
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
- const { newDB, message } = await createDatabase(name)
12
- db = newDB;
13
- postMessage({ type: 'created', message });
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 = await db.exec({ sql, returnValue: "resultRows" });
20
- // console.log(sql, result);
21
- postMessage({ result, type: "application/json" });
65
+ const result = db.exec({ sql, returnValue: "resultRows" });
66
+ reply(port, result);
22
67
  } catch (e) {
23
- if (e.message.indexOf("SQLITE_CANTOPEN") != -1) {
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
- // console.debug(sql, values);
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
- // console.debug("stmt", stmt)
78
+ const columns = stmt.getColumnNames();
42
79
  const result = [];
43
80
  while (stmt.step()) {
44
- let row = stmt.get([]);
45
- let zipped = columns.map(function (columnName, index) {
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
- // console.debug("RESULT", result)
52
- postMessage({ result, type: "application/json" });
84
+ reply(port, result);
53
85
  } catch (e) {
54
- if (e.message.indexOf("SQLITE_CANTOPEN") != -1) {
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.finalize();
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 byteArray = sqlite3.capi.sqlite3_js_db_export(db);
74
- const blob = new Blob([byteArray.buffer], { type: "application/vnd.sqlite3" });
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
- if (e.message.indexOf("SQLITE_NOMEM") != -1)
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
- case 'closeDB':
84
- closeDB();
85
- postMessage({ type: "closed" });
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.log(data)
112
+ console.warn('Unknown action:', data)
89
113
  }
90
114
  }
91
115
 
92
116
  async function createDatabase(name) {
93
- const sqlite3 = await getInstance();
94
- return 'opfs' in sqlite3
95
- ? { newDB: new sqlite3.oo1.OpfsDb(`/${name}.sqlite3`), message: `OPFS is available, created persisted database at /${name}.sqlite3` }
96
- : { newDB: new sqlite3.oo1.DB(`/${name}.sqlite3`, 'ct'), message: `OPFS is not available, created transient database /${name}.sqlite3` };
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
- try {
101
- const sqlite3 = await getInstance();
102
- if ('opfs' in sqlite3) {
103
- const size = await sqlite3.oo1.OpfsDb.importDb(`${name}.sqlite3`, arrayBuffer);
104
- if (size) {
105
- db = new sqlite3.oo1.OpfsDb(`/${name}.sqlite3`);
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
  }