You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When deployed to GitHub Pages and Netlify, the application does not function properly in Safari.
Netlify has found that it can be used by setting the appropriate headers using the _headers file. The following PR reflects this in the documentation #9
We use coi-serviceworker to enable SQLite Wasm + OPFS to work correctly on GitHub Pages. However, this mechanism does not work properly in Safari, which prevents us from using it.
Why do we use coi-serviceworker?
On GitHub Pages, you cannot use OPFS because you cannot set the Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers.
The coi-serviceworker is used to overcome this limitation.
However, this service worker mechanism does not function correctly in Safari, resulting in Safari's incompatibility with the GitHub Pages environment.
How should we address Safari compatibility?
We recommend using a platform other than GitHub Pages (if it works correctly there).
Inform users not to use Safari when accessing the application on GitHub Pages.
Additionally, if the environment is not functioning properly, the following check returns false. We could detect this and display an appropriate message to users.
console.log('SharedArrayBuffer'inwindow);
Alternative Attempt (Unsuccessful): Using IndexedDB
For Safari, we attempted to use IndexedDB, but it did not work as expected. When a process related to IndexedDB is called, it remains in a pending state without returning any value.
Code
Here's the code as it was actually tryed.
/// <reference path="./index.d.ts" />import{sqlite3Worker1Promiser}from"@sqlite.org/sqlite-wasm";import{ExecuteResult,QueryResult,NeverChangeDBasINeverChangeDB,Migration,}from"./types";import{initialMigration}from"./migrations";import{loadFromIndexedDB,syncToIndexedDB}from"./indexeddb";exportclassNeverChangeDBimplementsINeverChangeDB{privatedbPromise: Promise<(command: string,params: any)=>Promise<any>>|null=null;privatedbId: string|null=null;privatemigrations: Migration[]=[];privateuseIndexedDB: boolean=false;constructor(privatedbName: string,privateoptions: {debug?: boolean;isMigrationActive?: boolean}={},){this.options.debug=options.debug??false;this.options.isMigrationActive=options.isMigrationActive??true;if(this.options.isMigrationActive){this.addMigrations([initialMigration]);}}privatelog(...args: any[]): void{if(this.options.debug){console.log(...args);}}asyncinit(): Promise<void>{if(this.dbPromise)return;try{this.dbPromise=this.initializeDatabase();awaitthis.dbPromise;if(this.options.isMigrationActive){awaitthis.createMigrationTable();awaitthis.runMigrations();}}catch(err){console.error("Failed to initialize database:",err);throwerr;}}privateasyncinitializeDatabase(): Promise<(command: string,params: any)=>Promise<any>>{this.log("Loading and initializing SQLite3 module...");constpromiser=awaitthis.getPromiser();this.log("Done initializing. Opening database...");constopenResponse=awaitthis.openDatabase(promiser);this.dbId=openResponse.result.dbId;if(this.useIndexedDB){awaitsyncToIndexedDB(this.dbId,this.dbName,()=>this.dumpDatabase(),this.options.debug,);}this.log("Database initialized successfully");returnpromiser;}privateasyncgetPromiser(): Promise<(command: string,params: any)=>Promise<any>>{returnnewPromise<(command: string,params: any)=>Promise<any>>((resolve)=>{sqlite3Worker1Promiser({onready: (promiser: (command: string,params: any)=>Promise<any>)=>resolve(promiser),});},);}privateasyncopenDatabase(promiser: (command: string,params: any)=>Promise<any>,): Promise<any>{try{constresponse=awaitpromiser("open",{filename: `file:${this.dbName}.sqlite3?vfs=opfs`,});this.log("OPFS database opened:",response.result.filename);returnresponse;}catch(opfsError){console.warn("OPFS is not available, falling back to in-memory database:",opfsError,);this.useIndexedDB=true;// Try to load the database from IndexedDBconstsavedData=awaitloadFromIndexedDB(this.dbName,this.options.debug,);if(savedData){constresponse=awaitpromiser("open",{filename: `:memory:`});awaitpromiser("exec",{sql: "PRAGMA foreign_keys = OFF; BEGIN TRANSACTION;",});awaitpromiser("exec",{sql: savedData,dbId: this.dbId});awaitpromiser("exec",{sql: "COMMIT;"});this.log("Database loaded from IndexedDB");returnresponse;}else{constresponse=awaitpromiser("open",{filename: ":memory:"});this.log("In-memory database opened (no data in IndexedDB)");returnresponse;}}}asyncexecute(sql: string,params: any[]=[]): Promise<ExecuteResult>{try{constpromiser=awaitthis.getPromiserOrThrow();constresult=awaitpromiser("exec",{
sql,bind: params,dbId: this.dbId,});if(this.useIndexedDB){awaitsyncToIndexedDB(this.dbId,this.dbName,()=>this.dumpDatabase(),this.options.debug,);}returnresult;}catch(error){console.error("Error executing SQL:",error);throwerror;}}asyncquery<T=any>(sql: string,params: any[]=[],): Promise<QueryResult<T>>{constpromiser=awaitthis.getPromiserOrThrow();constresult=awaitpromiser("exec",{
sql,bind: params,rowMode: "object",dbId: this.dbId,});if(this.useIndexedDB){awaitsyncToIndexedDB(this.dbId,this.dbName,()=>this.dumpDatabase(),this.options.debug,);}returnresult.result.resultRows||[];}asyncclose(): Promise<void>{if(this.dbId){constpromiser=awaitthis.getPromiserOrThrow();awaitpromiser("close",{dbId: this.dbId});this.dbId=null;this.dbPromise=null;}}addMigrations(migrations: Migration[]): void{this.migrations.push(...migrations);this.migrations.sort((a,b)=>a.version-b.version);}privateasynccreateMigrationTable(): Promise<void>{awaitthis.execute(` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);}privateasyncgetCurrentVersion(): Promise<number>{consttables=awaitthis.query<{name: string}>("SELECT name FROM sqlite_master WHERE type='table'",);if(!tables.some((t)=>t.name==="migrations"))return0;constresult=awaitthis.query<{version: number}>("SELECT MAX(version) as version FROM migrations",);returnresult[0]?.version||0;}privateasyncrunMigrations(): Promise<void>{constcurrentVersion=awaitthis.getCurrentVersion();constpendingMigrations=this.migrations.filter((m)=>m.version>currentVersion,);for(constmigrationofpendingMigrations){this.log(`Running migration to version ${migration.version}`);awaitmigration.up(this);awaitthis.execute("INSERT INTO migrations (version) VALUES (?)",[migration.version,]);this.log(`Migration to version ${migration.version} completed`);}}privateasyncgetPromiserOrThrow(): Promise<(command: string,params: any)=>Promise<any>>{if(!this.dbPromise){thrownewError("Database not initialized. Call init() first.");}returnthis.dbPromise;}privateescapeBlob(blob: Uint8Array): string{return`X'${Array.from(newUint8Array(blob),(byte)=>byte.toString(16).padStart(2,"0")).join("")}'`;}asyncdumpDatabase(options: {compatibilityMode?: boolean;table?: string}={},): Promise<string>{const{ compatibilityMode =false, table }=options;letdumpOutput="";if(compatibilityMode){dumpOutput+=`PRAGMA foreign_keys = OFF;\nBEGIN TRANSACTION;\n\n`;}// Get all database objects or just the specified tableconstobjectsQuery=table
? `SELECT type, name, sql FROM sqlite_master WHERE type='table' AND name = ?`
: `SELECT type, name, sql FROM sqlite_master WHERE sql NOT NULL AND name != 'sqlite_sequence'`;constobjects=awaitthis.query<{type: string;name: string;sql: string|null;}>(objectsQuery,table ? [table] : []);// Dump table contentsfor(constobjofobjects){if(obj.type==="table"){dumpOutput+=`${obj.sql};\n`;// Dump table contentsconstrows=awaitthis.query(`SELECT * FROM ${obj.name}`);for(constrowofrows){constcolumns=Object.keys(row).join(", ");constvalues=Object.values(row).map((value)=>{if(valueinstanceofUint8Array){returnthis.escapeBlob(value);}elseif(value===null){return"NULL";}elseif(typeofvalue==="string"){return`'${value.replace(/'/g,"''")}'`;}returnvalue;}).join(", ");dumpOutput+=`INSERT INTO ${obj.name} (${columns}) VALUES (${values});\n`;}dumpOutput+="\n";}}// Handle sqlite_sequence separately if no specific table is specifiedif(!table){try{constseqRows=awaitthis.query<{name: string;seq: number}>(`SELECT * FROM sqlite_sequence`,);if(seqRows.length>0){dumpOutput+=`DELETE FROM sqlite_sequence;\n`;for(constrowofseqRows){dumpOutput+=`INSERT INTO sqlite_sequence VALUES('${row.name}', ${row.seq});\n`;}dumpOutput+="\n";}}catch(error){this.log("sqlite_sequence table does not exist, skipping...");}}// Add other database objects (views, indexes, triggers) if no specific table is specifiedif(!table){for(constobjofobjects){if(obj.type!=="table"){dumpOutput+=`${obj.sql};\n\n`;}}}if(compatibilityMode){dumpOutput+=`COMMIT;\n`;}returndumpOutput;}asyncimportDump(dumpContent: string,options: {compatibilityMode?: boolean}={},): Promise<void>{const{ compatibilityMode =false}=options;conststatements=dumpContent.split(";").map((stmt)=>stmt.trim()).filter(Boolean);if(!compatibilityMode){awaitthis.execute("PRAGMA foreign_keys=OFF");awaitthis.execute("BEGIN TRANSACTION");}try{// Drop all existing tables, views, and indexesconstexistingObjects=awaitthis.query<{type: string;name: string}>(` SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name != 'sqlite_sequence' `);for(const{ type, name }ofexistingObjects){awaitthis.execute(`DROP ${type} IF EXISTS ${name}`);}// Execute all statements from the dumpfor(conststatementofstatements){if(statement!=="COMMIT"){// Skip the final COMMIT statementawaitthis.execute(statement);}}if(!compatibilityMode){awaitthis.execute("COMMIT");awaitthis.execute("PRAGMA foreign_keys = ON");}}catch(error){if(!compatibilityMode){awaitthis.execute("ROLLBACK");awaitthis.execute("PRAGMA foreign_keys = ON");}throwerror;}}}
new file: indexeddb.ts
constINDEXED_DB_NAME="neverchange";exportconstsaveToIndexedDB=async(dbName: string,data: Uint8Array,showLog: boolean|undefined,): Promise<void>=>{returnnewPromise((resolve,reject)=>{constrequest=indexedDB.open(INDEXED_DB_NAME);request.onupgradeneeded=(event)=>{constdb=(event.targetasIDBOpenDBRequest).result;if(!db.objectStoreNames.contains(dbName)){db.createObjectStore(dbName);showLog&&console.log(`Object store '${dbName}' created`);}};request.onsuccess=(event)=>{constdb=(event.targetasIDBOpenDBRequest).result;consttransaction=db.transaction(dbName,"readwrite");conststore=transaction.objectStore(dbName);store.put(data,dbName);transaction.oncomplete=()=>resolve();transaction.onerror=(err)=>reject(err);};request.onerror=(err)=>reject(err);});};exportconstloadFromIndexedDB=async(dbName: string,showLog: boolean|undefined,): Promise<Uint8Array|null>=>{returnnewPromise((resolve,reject)=>{constrequest=indexedDB.open(INDEXED_DB_NAME);request.onupgradeneeded=(event)=>{constdb=(event.targetasIDBOpenDBRequest).result;if(!db.objectStoreNames.contains(dbName)){db.createObjectStore(dbName);showLog&&console.log(`Object store '${dbName}' created`);}};request.onsuccess=(event)=>{constdb=(event.targetasIDBOpenDBRequest).result;consttransaction=db.transaction(dbName,"readonly");conststore=transaction.objectStore(dbName);constgetRequest=store.get(dbName);getRequest.onsuccess=()=>{resolve(getRequest.result||null);};getRequest.onerror=(err)=>reject(err);};request.onerror=(err)=>reject(err);});};exportconstsyncToIndexedDB=async(dbId: string|null,dbName: string,dumpDatabase: Function,showLog: boolean|undefined,): Promise<void>=>{if(dbId){constdump=awaitdumpDatabase();constuint8Array=newTextEncoder().encode(dump);awaitsaveToIndexedDB(dbName,uint8Array,showLog);if(showLog){console.log("Database synchronized to IndexedDB");}}};
The text was updated successfully, but these errors were encountered:
Summary
When deployed to GitHub Pages and
Netlify, the application does not function properly in Safari.Netlify has found that it can be used by setting the appropriate headers using the _headers file. The following PR reflects this in the documentation
#9
We use
coi-serviceworker
to enable SQLite Wasm + OPFS to work correctly on GitHub Pages. However, this mechanism does not work properly in Safari, which prevents us from using it.Why do we use
coi-serviceworker
?On GitHub Pages, you cannot use OPFS because you cannot set the
Cross-Origin-Opener-Policy: same-origin
andCross-Origin-Embedder-Policy: require-corp
headers.The
coi-serviceworker
is used to overcome this limitation.However, this service worker mechanism does not function correctly in Safari, resulting in Safari's incompatibility with the GitHub Pages environment.
How should we address Safari compatibility?
We recommend using a platform other than GitHub Pages (if it works correctly there).
Inform users not to use Safari when accessing the application on GitHub Pages.
Additionally, if the environment is not functioning properly, the following check returns
false
. We could detect this and display an appropriate message to users.Alternative Attempt (Unsuccessful): Using IndexedDB
For Safari, we attempted to use IndexedDB, but it did not work as expected. When a process related to IndexedDB is called, it remains in a
pending
state without returning any value.Code
Here's the code as it was actually tryed.
new file:
indexeddb.ts
The text was updated successfully, but these errors were encountered: