mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 23:19:40 +01:00
web: fix web app on mobile browsers
This commit is contained in:
committed by
Abdullah Atta
parent
9e064d88c6
commit
6e6b793568
@@ -71,7 +71,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
|||||||
pageSize: 8192,
|
pageSize: 8192,
|
||||||
cacheSize: -32000,
|
cacheSize: -32000,
|
||||||
password: Buffer.from(databaseKey).toString("hex"),
|
password: Buffer.from(databaseKey).toString("hex"),
|
||||||
skipInitialization: !IS_DESKTOP_APP
|
skipInitialization: !IS_DESKTOP_APP && !!globalThis.SharedWorker
|
||||||
},
|
},
|
||||||
storage: storage,
|
storage: storage,
|
||||||
eventsource: EventSource,
|
eventsource: EventSource,
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import {
|
|||||||
SqliteIntrospector,
|
SqliteIntrospector,
|
||||||
Dialect
|
Dialect
|
||||||
} from "kysely";
|
} from "kysely";
|
||||||
import { WaSqliteWorkerDriver } from "./wa-sqlite-kysely-driver";
|
import {
|
||||||
|
WaSqliteWorkerMultipleTabDriver,
|
||||||
|
WaSqliteWorkerSingleTabDriver
|
||||||
|
} from "./wa-sqlite-kysely-driver";
|
||||||
import { isFeatureSupported } from "../../utils/feature-check";
|
import { isFeatureSupported } from "../../utils/feature-check";
|
||||||
|
|
||||||
declare module "kysely" {
|
declare module "kysely" {
|
||||||
@@ -39,11 +42,17 @@ export const createDialect = (
|
|||||||
): Dialect => {
|
): Dialect => {
|
||||||
return {
|
return {
|
||||||
createDriver: () =>
|
createDriver: () =>
|
||||||
new WaSqliteWorkerDriver({
|
globalThis.SharedWorker
|
||||||
|
? new WaSqliteWorkerMultipleTabDriver({
|
||||||
async: !isFeatureSupported("opfs"),
|
async: !isFeatureSupported("opfs"),
|
||||||
dbName: name,
|
dbName: name,
|
||||||
encrypted,
|
encrypted,
|
||||||
init
|
init
|
||||||
|
})
|
||||||
|
: new WaSqliteWorkerSingleTabDriver({
|
||||||
|
async: !isFeatureSupported("opfs"),
|
||||||
|
dbName: name,
|
||||||
|
encrypted
|
||||||
}),
|
}),
|
||||||
createAdapter: () => new SqliteAdapter(),
|
createAdapter: () => new SqliteAdapter(),
|
||||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||||
|
|||||||
@@ -182,7 +182,9 @@ export class SharedService<T extends object> extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sendPortToClient(message: any, port: MessagePort) {
|
#sendPortToClient(message: any, port: MessagePort) {
|
||||||
sharedWorker?.port.postMessage(message, [port]);
|
if (!sharedWorker)
|
||||||
|
throw new Error("Shared worker is not supported in this environment.");
|
||||||
|
sharedWorker.port.postMessage(message, [port]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getClientId() {
|
async #getClientId() {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import type { SQLiteAPI, SQLiteCompatibleType } from "./sqlite-types";
|
import type { SQLiteAPI, SQLiteCompatibleType } from "./sqlite-types";
|
||||||
import { Factory, SQLITE_ROW, SQLiteError } from "./sqlite-api";
|
import { Factory, SQLITE_ROW, SQLiteError } from "./sqlite-api";
|
||||||
import { transfer } from "comlink";
|
import { expose, transfer } from "comlink";
|
||||||
import type { RunMode } from "./type";
|
import type { RunMode } from "./type";
|
||||||
import { QueryResult } from "kysely";
|
import { QueryResult } from "kysely";
|
||||||
import { DatabaseSource } from "./sqlite-export";
|
import { DatabaseSource } from "./sqlite-export";
|
||||||
@@ -32,6 +32,12 @@ type PreparedStatement = {
|
|||||||
columns: string[];
|
columns: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SQLiteOptions = {
|
||||||
|
async: boolean;
|
||||||
|
url?: string;
|
||||||
|
encrypted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
class _SQLiteWorker {
|
class _SQLiteWorker {
|
||||||
sqlite!: SQLiteAPI;
|
sqlite!: SQLiteAPI;
|
||||||
db: number | undefined = undefined;
|
db: number | undefined = undefined;
|
||||||
@@ -39,21 +45,22 @@ class _SQLiteWorker {
|
|||||||
initialized = false;
|
initialized = false;
|
||||||
preparedStatements: Map<string, PreparedStatement> = new Map();
|
preparedStatements: Map<string, PreparedStatement> = new Map();
|
||||||
retryCounter: Record<string, number> = {};
|
retryCounter: Record<string, number> = {};
|
||||||
constructor(
|
encrypted = false;
|
||||||
private readonly dbName: string,
|
name = "";
|
||||||
private readonly encrypted: boolean
|
async = false;
|
||||||
) {
|
|
||||||
console.log("new sqlite worker", dbName, encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
async open(async: boolean, url?: string) {
|
async open(name: string, options: SQLiteOptions) {
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
console.error("Database is already initialized", this.db);
|
console.error("Database is already initialized", this.db);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = url ? { locateFile: () => url } : {};
|
this.encrypted = options.encrypted;
|
||||||
const sqliteModule = async
|
this.name = name;
|
||||||
|
this.async = options.async;
|
||||||
|
|
||||||
|
const option = options.url ? { locateFile: () => options.url } : {};
|
||||||
|
const sqliteModule = options.async
|
||||||
? await import("./wa-sqlite-async").then(
|
? await import("./wa-sqlite-async").then(
|
||||||
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
||||||
)
|
)
|
||||||
@@ -61,11 +68,11 @@ class _SQLiteWorker {
|
|||||||
SQLiteSyncESMFactory(option)
|
SQLiteSyncESMFactory(option)
|
||||||
);
|
);
|
||||||
this.sqlite = Factory(sqliteModule);
|
this.sqlite = Factory(sqliteModule);
|
||||||
this.vfs = await this.getVFS(this.dbName, async);
|
this.vfs = await this.getVFS(name, options.async);
|
||||||
|
|
||||||
this.sqlite.vfs_register(this.vfs, false);
|
this.sqlite.vfs_register(this.vfs, false);
|
||||||
this.db = await this.sqlite.open_v2(
|
this.db = await this.sqlite.open_v2(
|
||||||
this.dbName,
|
name,
|
||||||
undefined,
|
undefined,
|
||||||
`multipleciphers-${this.vfs.name}`
|
`multipleciphers-${this.vfs.name}`
|
||||||
);
|
);
|
||||||
@@ -163,7 +170,7 @@ class _SQLiteWorker {
|
|||||||
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
|
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
|
||||||
await this.waitForDatabase();
|
await this.waitForDatabase();
|
||||||
}
|
}
|
||||||
if (!this.db) throw new Error("No database is not opened.");
|
if (!this.db) throw new Error("Database is not opened.");
|
||||||
|
|
||||||
const rows = (await this.exec(sql, mode, parameters)) as R[];
|
const rows = (await this.exec(sql, mode, parameters)) as R[];
|
||||||
if (mode === "query") return { rows };
|
if (mode === "query") return { rows };
|
||||||
@@ -194,16 +201,16 @@ class _SQLiteWorker {
|
|||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async export(dbName: string, async: boolean) {
|
async export() {
|
||||||
const vfs = await this.getVFS(dbName, async);
|
const vfs = await this.getVFS(this.name, this.async);
|
||||||
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
|
const stream = new ReadableStream(new DatabaseSource(vfs, this.name));
|
||||||
return transfer(stream, [stream]);
|
return transfer(stream, [stream]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(dbName: string, async: boolean) {
|
async delete() {
|
||||||
await this.close();
|
await this.close();
|
||||||
if (this.vfs) await this.vfs.delete();
|
if (this.vfs) await this.vfs.delete();
|
||||||
else await (await this.getVFS(dbName, async)).delete();
|
else await (await this.getVFS(this.name, this.async)).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVFS(dbName: string, async: boolean) {
|
async getVFS(dbName: string, async: boolean) {
|
||||||
@@ -222,7 +229,7 @@ class _SQLiteWorker {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
self.dispatchEvent(
|
self.dispatchEvent(
|
||||||
new MessageEvent("message", {
|
new MessageEvent("message", {
|
||||||
data: { type: "databaseInitialized", dbName: this.dbName }
|
data: { type: "databaseInitialized", dbName: this.name }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
console.log("Database initialized", this.db);
|
console.log("Database initialized", this.db);
|
||||||
@@ -237,7 +244,7 @@ class _SQLiteWorker {
|
|||||||
self.addEventListener("message", (ev) => {
|
self.addEventListener("message", (ev) => {
|
||||||
if (
|
if (
|
||||||
ev.data.type === "databaseInitialized" &&
|
ev.data.type === "databaseInitialized" &&
|
||||||
ev.data.dbName === this.dbName
|
ev.data.dbName === this.name
|
||||||
)
|
)
|
||||||
resolve(true);
|
resolve(true);
|
||||||
})
|
})
|
||||||
@@ -251,11 +258,17 @@ export type SQLiteWorker = typeof _SQLiteWorker.prototype;
|
|||||||
|
|
||||||
addEventListener("message", async (event) => {
|
addEventListener("message", async (event) => {
|
||||||
if (!event.data.type) {
|
if (!event.data.type) {
|
||||||
const worker = new _SQLiteWorker(event.data.dbName, event.data.encrypted);
|
const worker = new _SQLiteWorker();
|
||||||
await worker.open(event.data.async, event.data.uri);
|
await worker.open(event.data.dbName, {
|
||||||
|
async: event.data.async,
|
||||||
|
encrypted: event.data.encrypted,
|
||||||
|
url: event.data.uri
|
||||||
|
});
|
||||||
const providerPort = createSharedServicePort(worker);
|
const providerPort = createSharedServicePort(worker);
|
||||||
postMessage(null, [providerPort]);
|
postMessage(null, [providerPort]);
|
||||||
|
|
||||||
self.addEventListener("beforeunload", () => worker.close());
|
self.addEventListener("beforeunload", () => worker.close());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const worker = new _SQLiteWorker();
|
||||||
|
expose(worker);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import SQLiteSyncURI from "./wa-sqlite.wasm?url";
|
|||||||
import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
|
import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
import { SharedService } from "./shared-service";
|
import { SharedService } from "./shared-service";
|
||||||
|
import { Remote, wrap } from "comlink";
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
dbName: string;
|
dbName: string;
|
||||||
@@ -38,13 +39,14 @@ const servicePool = new Map<
|
|||||||
{ service: SharedService<SQLiteWorker>; activated: boolean; closed: boolean }
|
{ service: SharedService<SQLiteWorker>; activated: boolean; closed: boolean }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
export class WaSqliteWorkerDriver implements Driver {
|
export class WaSqliteWorkerMultipleTabDriver implements Driver {
|
||||||
private connection?: DatabaseConnection;
|
private connection?: DatabaseConnection;
|
||||||
private connectionMutex = new Mutex();
|
private connectionMutex = new Mutex();
|
||||||
private initializationMutex = new Mutex();
|
private initializationMutex = new Mutex();
|
||||||
private readonly serviceName;
|
private readonly serviceName;
|
||||||
|
|
||||||
constructor(private readonly config: Config) {
|
constructor(private readonly config: Config) {
|
||||||
|
console.log("multi tab driver", config.dbName);
|
||||||
this.serviceName = `${config.dbName}-service`;
|
this.serviceName = `${config.dbName}-service`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +61,11 @@ export class WaSqliteWorkerDriver implements Driver {
|
|||||||
if (activated) {
|
if (activated) {
|
||||||
if (closed) {
|
if (closed) {
|
||||||
console.log("Already activated. Reinitializing...");
|
console.log("Already activated. Reinitializing...");
|
||||||
await service.proxy.open(
|
await service.proxy.open(this.config.dbName, {
|
||||||
this.config.async,
|
async: this.config.async,
|
||||||
this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
encrypted: this.config.encrypted,
|
||||||
);
|
url: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||||
|
});
|
||||||
this.needsInitialization = true;
|
this.needsInitialization = true;
|
||||||
servicePool.set(this.serviceName, {
|
servicePool.set(this.serviceName, {
|
||||||
service,
|
service,
|
||||||
@@ -193,19 +196,76 @@ export class WaSqliteWorkerDriver implements Driver {
|
|||||||
async delete() {
|
async delete() {
|
||||||
const service = servicePool.get(this.serviceName);
|
const service = servicePool.get(this.serviceName);
|
||||||
if (!service || !service.service) return;
|
if (!service || !service.service) return;
|
||||||
await service.service?.proxy?.delete(this.config.dbName, this.config.async);
|
await service.service?.proxy?.delete();
|
||||||
service.closed = true;
|
service.closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async export() {
|
async export() {
|
||||||
return servicePool
|
return servicePool.get(this.serviceName)?.service?.proxy?.export();
|
||||||
.get(this.serviceName)
|
}
|
||||||
?.service?.proxy?.export(this.config.dbName, this.config.async);
|
}
|
||||||
|
|
||||||
|
export class WaSqliteWorkerSingleTabDriver implements Driver {
|
||||||
|
private connection?: DatabaseConnection;
|
||||||
|
private connectionMutex = new Mutex();
|
||||||
|
private readonly worker = wrap<SQLiteWorker>(
|
||||||
|
new Worker({ name: this.config.dbName })
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(private readonly config: Config) {
|
||||||
|
console.log("single tab driver", config.dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await this.worker.open(this.config.dbName, {
|
||||||
|
async: this.config.async,
|
||||||
|
encrypted: this.config.encrypted,
|
||||||
|
url: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||||
|
});
|
||||||
|
this.connection = new WaSqliteWorkerConnection(this.worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireConnection(): Promise<DatabaseConnection> {
|
||||||
|
if (!this.connection) throw new Error("Driver not initialized.");
|
||||||
|
|
||||||
|
// SQLite only has one single connection. We use a mutex here to wait
|
||||||
|
// until the single connection has been released.
|
||||||
|
await this.connectionMutex.waitForUnlock();
|
||||||
|
await this.connectionMutex.acquire();
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseConnection(): Promise<void> {
|
||||||
|
this.connectionMutex.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
await this.worker.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete() {
|
||||||
|
await this.worker.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async export() {
|
||||||
|
return await this.worker.export();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WaSqliteWorkerConnection implements DatabaseConnection {
|
class WaSqliteWorkerConnection implements DatabaseConnection {
|
||||||
constructor(private readonly worker: SQLiteWorker) {}
|
constructor(private readonly worker: SQLiteWorker | Remote<SQLiteWorker>) {}
|
||||||
|
|
||||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||||
throw new Error("wasqlite driver doesn't support streaming");
|
throw new Error("wasqlite driver doesn't support streaming");
|
||||||
@@ -221,6 +281,8 @@ class WaSqliteWorkerConnection implements DatabaseConnection {
|
|||||||
: query.kind === "RawNode"
|
: query.kind === "RawNode"
|
||||||
? "raw"
|
? "raw"
|
||||||
: "exec";
|
: "exec";
|
||||||
return this.worker.run(mode, sql, parameters as any);
|
return this.worker.run(mode, sql, parameters as any) as Promise<
|
||||||
|
QueryResult<R>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async function initializeLogger() {
|
|||||||
synchronous: "normal",
|
synchronous: "normal",
|
||||||
pageSize: 8192,
|
pageSize: 8192,
|
||||||
cacheSize: -32000,
|
cacheSize: -32000,
|
||||||
skipInitialization: !IS_DESKTOP_APP
|
skipInitialization: !IS_DESKTOP_APP && !!globalThis.SharedWorker
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user