@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.
Files changed (3) hide show
  1. package/index.js +64 -120
  2. package/package.json +1 -1
  3. 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
- export const name = "sqlite";
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 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
- }
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
- export function createDB(name = 'default') {
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
- 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));
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
- 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
- }
43
+ worker.postMessage(payload, [port2]);
44
+ })
53
45
  }
54
46
 
47
+ // Public API
55
48
  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
- }
49
+ getWorker(name).postMessage({ action: 'downloadDB' });
68
50
  }
69
51
 
70
- 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
- });
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 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
- });
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 getWorker(name = 'default') {
113
- let worker = workers[name];
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 getWorkers() {
118
- return workers;
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
- 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" });
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[name];
88
+ let worker = workers.get(name);
138
89
  if (worker) {
139
- worker.postMessage({ command: 'terminate' });
140
- }
90
+ worker.terminate();
91
+ workers.delete(name);
92
+ }
141
93
  }
142
94
 
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.');
95
+ export function getWorkers() {
96
+ return workers;
153
97
  }
package/package.json CHANGED
@@ -28,5 +28,5 @@
28
28
  "url": "git+https://github.com/vanillaspa/sqlite-database.git"
29
29
  },
30
30
  "type": "module",
31
- "version": "1.0.0"
31
+ "version": "1.1.0"
32
32
  }
package/sqliteWorker.js CHANGED
@@ -1,31 +1,65 @@
1
1
  import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
2
2
 
3
- var db = null;
4
- var sqlite3 = null;
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
- const { newDB, message } = await createDatabase(name)
12
- db = newDB;
13
- postMessage({ type: 'created', message });
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 = await db.exec({ sql, returnValue: "resultRows" });
20
- // console.log(sql, result);
21
- postMessage({ result, type: "application/json" });
59
+ const result = db.exec({ sql, returnValue: "resultRows" });
60
+ reply(result);
22
61
  } 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
- }
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
- // console.debug(sql, values);
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
- // console.debug("stmt", stmt)
72
+ const columns = stmt.getColumnNames();
42
73
  const result = [];
43
74
  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);
75
+ const row = stmt.get([]);
76
+ result.push(Object.fromEntries(columns.map((columnName, index) => [columnName, row[index]])));
50
77
  }
51
- // console.debug("RESULT", result)
52
- postMessage({ result, type: "application/json" });
78
+ reply(result);
53
79
  } 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
- }
80
+ handleSQLiteError(sql, e);
61
81
  } finally {
62
- stmt.finalize();
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 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
89
+ const message = await uploadDatabase(name, arrayBuffer)
90
+ reply(message);
76
91
  } 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);
92
+ replyError(e.message);
81
93
  }
82
94
  break;
83
- case 'closeDB':
84
- closeDB();
85
- postMessage({ type: "closed" });
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.log(data)
106
+ console.warn('Unknown action:', data)
89
107
  }
90
108
  }
91
109
 
92
110
  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` };
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
- 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
- }
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
  }