web: fix web app on mobile browsers

This commit is contained in:
Abdullah Atta
2024-06-28 09:57:24 +05:00
committed by Abdullah Atta
parent 9e064d88c6
commit 6e6b793568
6 changed files with 129 additions and 43 deletions

View File

@@ -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,

View File

@@ -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,12 +42,18 @@ export const createDialect = (
): Dialect => { ): Dialect => {
return { return {
createDriver: () => createDriver: () =>
new WaSqliteWorkerDriver({ globalThis.SharedWorker
async: !isFeatureSupported("opfs"), ? new WaSqliteWorkerMultipleTabDriver({
dbName: name, async: !isFeatureSupported("opfs"),
encrypted, dbName: name,
init encrypted,
}), 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),
createQueryCompiler: () => new SqliteQueryCompiler() createQueryCompiler: () => new SqliteQueryCompiler()

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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>
>;
} }
} }

View File

@@ -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
); );