mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
2 Commits
96ca91e35e
...
a05b0cd0ef
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a05b0cd0ef | ||
![]() |
c87b83806e |
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable no-restricted-globals */
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// IndexedDB Virtual File System for Local Previews
|
// Browser Service Worker powered Preview ("Browser SW Preview"), using IndexedDB.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
console.log('[ServiceWorker] Service worker file executed');
|
console.log('[ServiceWorker] Service worker file executed');
|
||||||
@@ -16,7 +17,7 @@ const DB_VERSION = 1;
|
|||||||
/**
|
/**
|
||||||
* Opens the IndexedDB database for browser SW preview files.
|
* Opens the IndexedDB database for browser SW preview files.
|
||||||
*/
|
*/
|
||||||
function openPreviewDB() {
|
function openBrowserSWPreviewDB() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
@@ -49,43 +50,57 @@ function openPreviewDB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a file from IndexedDB.
|
* Retrieves a preview file from IndexedDB.
|
||||||
*/
|
*/
|
||||||
async function getPreviewFile(path) {
|
async function getBrowserSWPreviewFile(path) {
|
||||||
try {
|
try {
|
||||||
const db = await openPreviewDB();
|
const db = await openBrowserSWPreviewDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
const safeResolve = (v) => { if (!settled) { settled = true; resolve(v); } };
|
||||||
|
const safeReject = (e) => { if (!settled) { settled = true; reject(e); } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transaction = db.transaction(STORE_NAME, 'readonly');
|
// Sanity-check the store exists (avoids InvalidStateError).
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
const err = new Error(`Object store "${STORE_NAME}" not found`);
|
||||||
|
console.error('[ServiceWorker] Missing object store while getting file:', path, err);
|
||||||
|
return safeReject(err);
|
||||||
|
}
|
||||||
|
|
||||||
transaction.onerror = () => {
|
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||||
const error = transaction.error || new Error('Transaction failed');
|
|
||||||
|
// If the transaction aborts (quota, deadlock, explicit abort, etc.), reject.
|
||||||
|
tx.onabort = () => {
|
||||||
|
const error = tx.error || new Error('Transaction aborted');
|
||||||
|
console.error('[ServiceWorker] Transaction aborted while getting file:', path, error);
|
||||||
|
safeReject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// `onerror` at the transaction level can fire even if request handlers didn’t.
|
||||||
|
tx.onerror = () => {
|
||||||
|
const error = tx.error || new Error('Transaction failed');
|
||||||
console.error('[ServiceWorker] Transaction error while getting file:', path, error);
|
console.error('[ServiceWorker] Transaction error while getting file:', path, error);
|
||||||
reject(error);
|
safeReject(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const objectStore = transaction.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = objectStore.get(path);
|
const req = store.get(path);
|
||||||
|
|
||||||
request.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
const result = request.result;
|
const result = req.result;
|
||||||
if (result) {
|
safeResolve(result || null);
|
||||||
console.log('[ServiceWorker] File retrieved from IndexedDB:', path, '(' + result.bytes.byteLength + ' bytes)');
|
|
||||||
} else {
|
|
||||||
console.warn('[ServiceWorker] File not found in IndexedDB:', path);
|
|
||||||
}
|
|
||||||
resolve(result || null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
req.onerror = () => {
|
||||||
const error = request.error || new Error('Get operation failed');
|
const error = req.error || new Error('Get operation failed');
|
||||||
console.error('[ServiceWorker] Error retrieving file from IndexedDB:', path, error);
|
console.error('[ServiceWorker] Error retrieving file from IndexedDB:', path, error);
|
||||||
reject(error);
|
safeReject(error);
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ServiceWorker] Exception during get operation:', path, error);
|
console.error('[ServiceWorker] Exception during get operation:', path, error);
|
||||||
reject(error);
|
safeReject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,12 +118,11 @@ self.addEventListener('fetch', (event) => {
|
|||||||
// Check if this is a request for a browser SW preview file
|
// Check if this is a request for a browser SW preview file
|
||||||
if (url.pathname.startsWith('/browser_sw_preview/')) {
|
if (url.pathname.startsWith('/browser_sw_preview/')) {
|
||||||
const relativePath = url.pathname.replace('/browser_sw_preview', '');
|
const relativePath = url.pathname.replace('/browser_sw_preview', '');
|
||||||
console.log('[ServiceWorker] Intercepting browser SW preview request:', url.pathname);
|
|
||||||
|
|
||||||
event.respondWith((async () => {
|
event.respondWith((async () => {
|
||||||
try {
|
try {
|
||||||
// Try to get the file from IndexedDB
|
// Try to get the file from IndexedDB
|
||||||
const fileRecord = await getPreviewFile(relativePath);
|
const fileRecord = await getBrowserSWPreviewFile(relativePath);
|
||||||
|
|
||||||
if (!fileRecord) {
|
if (!fileRecord) {
|
||||||
console.warn('[ServiceWorker] File not found in IndexedDB:', relativePath);
|
console.warn('[ServiceWorker] File not found in IndexedDB:', relativePath);
|
||||||
@@ -121,7 +135,6 @@ self.addEventListener('fetch', (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the file with appropriate headers
|
// Return the file with appropriate headers
|
||||||
console.log('[ServiceWorker] Serving file from IndexedDB:', relativePath, 'Content-Type:', fileRecord.contentType);
|
|
||||||
return new Response(fileRecord.bytes, {
|
return new Response(fileRecord.bytes, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -167,14 +180,16 @@ self.addEventListener('activate', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Standard Workbox Configuration
|
// Standard Workbox Configuration (for "semi-offline"/caching of GDevelop static files and resources)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// TODO: remove this check
|
self.__WB_DISABLE_DEV_LOGS = true;
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
importScripts(
|
importScripts(
|
||||||
'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
|
'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
/* global workbox */
|
/* global workbox */
|
||||||
if (workbox) {
|
if (workbox) {
|
||||||
console.log('[ServiceWorker] Workbox loaded successfully');
|
console.log('[ServiceWorker] Workbox loaded successfully');
|
||||||
|
@@ -3,11 +3,13 @@ import {
|
|||||||
type EventsFunctionCodeWriter,
|
type EventsFunctionCodeWriter,
|
||||||
type EventsFunctionCodeWriterCallbacks,
|
type EventsFunctionCodeWriterCallbacks,
|
||||||
} from '..';
|
} from '..';
|
||||||
import { putFile } from '../../Utils/BrowserSWIndexedDB';
|
import {
|
||||||
import { makeTimestampedId } from '../../Utils/TimestampedId';
|
deleteFilesWithPrefix,
|
||||||
|
putFile,
|
||||||
|
getBrowserSWPreviewBaseUrl,
|
||||||
|
} from '../../ExportAndShare/BrowserExporters/BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||||
import slugs from 'slugs';
|
import slugs from 'slugs';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { getBrowserSWPreviewBaseUrl } from '../../Utils/BrowserSWIndexedDB';
|
|
||||||
|
|
||||||
let batchedWrites: Array<{
|
let batchedWrites: Array<{
|
||||||
path: string,
|
path: string,
|
||||||
@@ -41,7 +43,6 @@ const flushBatchedWrites = debounce(async () => {
|
|||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
const write = writes[index];
|
const write = writes[index];
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
console.log(`[BrowserSWEventsFunctionCodeWriter] Stored: ${write.path}`);
|
|
||||||
write.onSuccess();
|
write.onSuccess();
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -78,11 +79,25 @@ const writeFileInNextBatch = (path: string, content: string) => {
|
|||||||
export const makeBrowserSWEventsFunctionCodeWriter = ({
|
export const makeBrowserSWEventsFunctionCodeWriter = ({
|
||||||
onWriteFile,
|
onWriteFile,
|
||||||
}: EventsFunctionCodeWriterCallbacks): EventsFunctionCodeWriter => {
|
}: EventsFunctionCodeWriterCallbacks): EventsFunctionCodeWriter => {
|
||||||
const prefix = makeTimestampedId();
|
|
||||||
const baseUrl = getBrowserSWPreviewBaseUrl();
|
const baseUrl = getBrowserSWPreviewBaseUrl();
|
||||||
|
const extensionsCodeUrl = baseUrl + '/extensions-code';
|
||||||
|
|
||||||
|
// At startup, clean up the old generated files for extensions code.
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// TODO: maybe don't do it at startup because this could break multiple tabs!
|
||||||
|
// TODO: Also consider doing a preview per tab?
|
||||||
|
await deleteFilesWithPrefix(extensionsCodeUrl + '/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[BrowserSWEventsFunctionCodeWriter] Failed to clean generated files for extensions code in "${extensionsCodeUrl}/".`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const getPathFor = (codeNamespace: string) => {
|
const getPathFor = (codeNamespace: string) => {
|
||||||
return `${baseUrl}/${prefix}/${slugs(codeNamespace)}.js`;
|
return `${extensionsCodeUrl}/${slugs(codeNamespace)}.js`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -95,9 +110,7 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
|||||||
const path = getPathFor(functionCodeNamespace);
|
const path = getPathFor(functionCodeNamespace);
|
||||||
onWriteFile({ includeFile: path, content: code });
|
onWriteFile({ includeFile: path, content: code });
|
||||||
const relativePath = path.replace(baseUrl, '');
|
const relativePath = path.replace(baseUrl, '');
|
||||||
console.log(
|
|
||||||
`[BrowserSWEventsFunctionCodeWriter] Writing function code to ${relativePath}...`
|
|
||||||
);
|
|
||||||
return writeFileInNextBatch(relativePath, code);
|
return writeFileInNextBatch(relativePath, code);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -108,9 +121,7 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
|||||||
const path = getPathFor(behaviorCodeNamespace);
|
const path = getPathFor(behaviorCodeNamespace);
|
||||||
onWriteFile({ includeFile: path, content: code });
|
onWriteFile({ includeFile: path, content: code });
|
||||||
const relativePath = path.replace(baseUrl, '');
|
const relativePath = path.replace(baseUrl, '');
|
||||||
console.log(
|
|
||||||
`[BrowserSWEventsFunctionCodeWriter] Writing behavior code to ${path}...`
|
|
||||||
);
|
|
||||||
return writeFileInNextBatch(relativePath, code);
|
return writeFileInNextBatch(relativePath, code);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -121,9 +132,7 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
|||||||
const path = getPathFor(objectCodeNamespace);
|
const path = getPathFor(objectCodeNamespace);
|
||||||
onWriteFile({ includeFile: path, content: code });
|
onWriteFile({ includeFile: path, content: code });
|
||||||
const relativePath = path.replace(baseUrl, '');
|
const relativePath = path.replace(baseUrl, '');
|
||||||
console.log(
|
|
||||||
`[BrowserSWEventsFunctionCodeWriter] Writing object code to ${path}...`
|
|
||||||
);
|
|
||||||
return writeFileInNextBatch(relativePath, code);
|
return writeFileInNextBatch(relativePath, code);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -59,19 +59,21 @@ const setupWindowClosedPolling = () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PREVIEWS_ORIGIN = 'https://game-previews.gdevelop.io';
|
let previewOrigin = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A debugger server implemented using the ability to send/receive messages
|
* A debugger server implemented using the ability to send/receive messages
|
||||||
* from popup windows in the browser.
|
* from popup windows in the browser.
|
||||||
*/
|
*/
|
||||||
export const browserPreviewDebuggerServer: PreviewDebuggerServer = {
|
export const browserPreviewDebuggerServer: PreviewDebuggerServer = {
|
||||||
startServer: async () => {
|
startServer: async ({ origin }) => {
|
||||||
if (debuggerServerState === 'started') return;
|
if (debuggerServerState === 'started') return;
|
||||||
debuggerServerState = 'started';
|
debuggerServerState = 'started';
|
||||||
|
|
||||||
|
previewOrigin = origin;
|
||||||
|
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
if (event.origin !== PREVIEWS_ORIGIN) return;
|
if (event.origin !== previewOrigin) return;
|
||||||
|
|
||||||
const id = getDebuggerIdForPreviewWindow(event.source);
|
const id = getDebuggerIdForPreviewWindow(event.source);
|
||||||
if (id === null) return; // Could not find the id of this preview window.
|
if (id === null) return; // Could not find the id of this preview window.
|
||||||
@@ -98,7 +100,7 @@ export const browserPreviewDebuggerServer: PreviewDebuggerServer = {
|
|||||||
if (!previewWindow) return;
|
if (!previewWindow) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
previewWindow.postMessage(message, PREVIEWS_ORIGIN);
|
previewWindow.postMessage(message, previewOrigin);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unable to send a message to the preview window:', error);
|
console.error('Unable to send a message to the preview window:', error);
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
|
|||||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||||
import { getIDEVersionWithHash } from '../../../Version';
|
import { getIDEVersionWithHash } from '../../../Version';
|
||||||
|
import { getBrowserSWPreviewBaseUrl } from '../BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||||
const gd: libGDevelop = global.gd;
|
const gd: libGDevelop = global.gd;
|
||||||
|
|
||||||
type State = {|
|
type State = {|
|
||||||
@@ -52,6 +53,7 @@ export const immediatelyOpenNewPreviewWindow = (
|
|||||||
"Can't open the preview window because of browser restrictions."
|
"Can't open the preview window because of browser restrictions."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
console.log('Preview window opened', previewWindow);
|
||||||
|
|
||||||
displayBlackLoadingScreenOrThrow(previewWindow);
|
displayBlackLoadingScreenOrThrow(previewWindow);
|
||||||
|
|
||||||
@@ -135,7 +137,9 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
|||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.getPreviewDebuggerServer().startServer();
|
await this.getPreviewDebuggerServer().startServer({
|
||||||
|
origin: new URL(getBrowserSWPreviewBaseUrl()).origin,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore any error when running the debugger server - the preview
|
// Ignore any error when running the debugger server - the preview
|
||||||
// can still work without it.
|
// can still work without it.
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import path from 'path-browserify';
|
import path from 'path-browserify';
|
||||||
import { deleteFilesWithPrefix, putFile } from '../../Utils/BrowserSWIndexedDB';
|
import {
|
||||||
|
deleteFilesWithPrefix,
|
||||||
|
putFile,
|
||||||
|
} from './BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||||
const gd: libGDevelop = global.gd;
|
const gd: libGDevelop = global.gd;
|
||||||
|
|
||||||
export type TextFileDescriptor = {|
|
export type TextFileDescriptor = {|
|
||||||
@@ -10,7 +13,6 @@ export type TextFileDescriptor = {|
|
|||||||
|
|
||||||
type ConstructorArgs = {|
|
type ConstructorArgs = {|
|
||||||
filesContent: Array<TextFileDescriptor>,
|
filesContent: Array<TextFileDescriptor>,
|
||||||
prefix: string,
|
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
@@ -51,7 +53,6 @@ const getContentType = (filePath: string): string => {
|
|||||||
* and serves them via a service worker for GDevelop previews.
|
* and serves them via a service worker for GDevelop previews.
|
||||||
*/
|
*/
|
||||||
export default class BrowserSWFileSystem {
|
export default class BrowserSWFileSystem {
|
||||||
prefix: string;
|
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
||||||
// Store the content of some files.
|
// Store the content of some files.
|
||||||
@@ -64,12 +65,13 @@ export default class BrowserSWFileSystem {
|
|||||||
contentType: string,
|
contentType: string,
|
||||||
|}> = [];
|
|}> = [];
|
||||||
|
|
||||||
|
_pendingDeleteOperations: Array<Promise<any>> = [];
|
||||||
|
|
||||||
// Store a set of all external URLs copied so that we can simulate
|
// Store a set of all external URLs copied so that we can simulate
|
||||||
// readDir result.
|
// readDir result.
|
||||||
_allCopiedExternalUrls = new Set<string>();
|
_allCopiedExternalUrls = new Set<string>();
|
||||||
|
|
||||||
constructor({ filesContent, prefix, baseUrl }: ConstructorArgs) {
|
constructor({ filesContent, baseUrl }: ConstructorArgs) {
|
||||||
this.prefix = prefix;
|
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
|
|
||||||
this._indexedFilesContent = {};
|
this._indexedFilesContent = {};
|
||||||
@@ -84,6 +86,15 @@ export default class BrowserSWFileSystem {
|
|||||||
* Uploads all pending files to IndexedDB.
|
* Uploads all pending files to IndexedDB.
|
||||||
*/
|
*/
|
||||||
applyPendingOperations = async () => {
|
applyPendingOperations = async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all(this._pendingDeleteOperations);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'[BrowserSWFileSystem] Error while deleting files in IndexedDB. Ignoring.',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
`[BrowserSWFileSystem] Storing ${
|
`[BrowserSWFileSystem] Storing ${
|
||||||
@@ -133,16 +144,7 @@ export default class BrowserSWFileSystem {
|
|||||||
clearDir = (path: string) => {
|
clearDir = (path: string) => {
|
||||||
// TODO: add to a pending operation list so we ensure it's executed.
|
// TODO: add to a pending operation list so we ensure it's executed.
|
||||||
console.info(`[BrowserSWFileSystem] Clearing directory: ${path}...`);
|
console.info(`[BrowserSWFileSystem] Clearing directory: ${path}...`);
|
||||||
deleteFilesWithPrefix(path)
|
this._pendingDeleteOperations.push(deleteFilesWithPrefix(path));
|
||||||
.then(() => {
|
|
||||||
console.info(`[BrowserSWFileSystem] Cleared directory: ${path}`);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(
|
|
||||||
`[BrowserSWFileSystem] Error clearing directory: ${path}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getTempDir = () => {
|
getTempDir = () => {
|
||||||
|
@@ -32,7 +32,7 @@ export const getBrowserSWPreviewBaseUrl = (): string => {
|
|||||||
* Opens or returns the existing IndexedDB database connection.
|
* Opens or returns the existing IndexedDB database connection.
|
||||||
* Handles database upgrades and version management.
|
* Handles database upgrades and version management.
|
||||||
*/
|
*/
|
||||||
export const openDatabase = (): Promise<IDBDatabase> => {
|
const openBrowserSWPreviewIndexedDB = (): Promise<IDBDatabase> => {
|
||||||
if (dbInstance && dbInstance.version === DB_VERSION) {
|
if (dbInstance && dbInstance.version === DB_VERSION) {
|
||||||
return Promise.resolve(dbInstance);
|
return Promise.resolve(dbInstance);
|
||||||
}
|
}
|
||||||
@@ -106,10 +106,7 @@ export const openDatabase = (): Promise<IDBDatabase> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a file in IndexedDB.
|
* Stores a "file" in IndexedDB.
|
||||||
* @param path - The virtual path of the file (e.g., '/browser_sw_preview/session-123/index.html')
|
|
||||||
* @param bytes - The file content as ArrayBuffer
|
|
||||||
* @param contentType - The MIME type of the file
|
|
||||||
*/
|
*/
|
||||||
export const putFile = async (
|
export const putFile = async (
|
||||||
path: string,
|
path: string,
|
||||||
@@ -117,7 +114,7 @@ export const putFile = async (
|
|||||||
contentType: string
|
contentType: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const db = await openDatabase();
|
const db = await openBrowserSWPreviewIndexedDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
@@ -134,7 +131,6 @@ export const putFile = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
transaction.oncomplete = () => {
|
transaction.oncomplete = () => {
|
||||||
console.log('[BrowserSWIndexedDB] File stored successfully:', path);
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,15 +166,15 @@ export const putFile = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all files with a given path prefix.
|
* Deletes all "files" stored in IndexedDB with a given path prefix.
|
||||||
* Useful for cleaning up old preview sessions.
|
*
|
||||||
* @param pathPrefix - The path prefix to match (e.g., '/browser_sw_preview/session-123/')
|
* @param pathPrefix - The path prefix to match.
|
||||||
*/
|
*/
|
||||||
export const deleteFilesWithPrefix = async (
|
export const deleteFilesWithPrefix = async (
|
||||||
pathPrefix: string
|
pathPrefix: string
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
const db = await openDatabase();
|
const db = await openBrowserSWPreviewIndexedDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
@@ -196,12 +192,6 @@ export const deleteFilesWithPrefix = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
transaction.oncomplete = () => {
|
transaction.oncomplete = () => {
|
||||||
console.log(
|
|
||||||
'[BrowserSWIndexedDB] Deleted',
|
|
||||||
deletedCount,
|
|
||||||
'files with prefix:',
|
|
||||||
pathPrefix
|
|
||||||
);
|
|
||||||
resolve(deletedCount);
|
resolve(deletedCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,15 +236,3 @@ export const deleteFilesWithPrefix = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the database connection.
|
|
||||||
* Should be called when the application is shutting down.
|
|
||||||
*/
|
|
||||||
export const closeDatabase = (): void => {
|
|
||||||
if (dbInstance) {
|
|
||||||
console.log('[BrowserSWIndexedDB] Closing database connection');
|
|
||||||
dbInstance.close();
|
|
||||||
dbInstance = null;
|
|
||||||
}
|
|
||||||
};
|
|
@@ -8,10 +8,8 @@ import {
|
|||||||
type PreviewOptions,
|
type PreviewOptions,
|
||||||
type PreviewLauncherProps,
|
type PreviewLauncherProps,
|
||||||
} from '../../PreviewLauncher.flow';
|
} from '../../PreviewLauncher.flow';
|
||||||
import { makeTimestampedId } from '../../../Utils/TimestampedId';
|
|
||||||
import {
|
import {
|
||||||
browserPreviewDebuggerServer,
|
browserPreviewDebuggerServer,
|
||||||
getExistingPreviewWindowForDebuggerId,
|
|
||||||
registerNewPreviewWindow,
|
registerNewPreviewWindow,
|
||||||
} from '../BrowserS3PreviewLauncher/BrowserPreviewDebuggerServer';
|
} from '../BrowserS3PreviewLauncher/BrowserPreviewDebuggerServer';
|
||||||
import Window from '../../../Utils/Window';
|
import Window from '../../../Utils/Window';
|
||||||
@@ -19,7 +17,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
|
|||||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||||
import { getIDEVersionWithHash } from '../../../Version';
|
import { getIDEVersionWithHash } from '../../../Version';
|
||||||
import { getBrowserSWPreviewBaseUrl } from '../../../Utils/BrowserSWIndexedDB';
|
import { getBrowserSWPreviewBaseUrl } from './BrowserSWPreviewIndexedDB';
|
||||||
const gd: libGDevelop = global.gd;
|
const gd: libGDevelop = global.gd;
|
||||||
|
|
||||||
type State = {|
|
type State = {|
|
||||||
@@ -77,9 +75,12 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
|||||||
return findGDJS('preview').then(({ gdjsRoot, filesContent }) => {
|
return findGDJS('preview').then(({ gdjsRoot, filesContent }) => {
|
||||||
console.info('[BrowserSWPreviewLauncher] GDJS found in', gdjsRoot);
|
console.info('[BrowserSWPreviewLauncher] GDJS found in', gdjsRoot);
|
||||||
|
|
||||||
const prefix = makeTimestampedId();
|
const isForInGameEdition = false; // TODO: adapt for the 3D editor branch.
|
||||||
|
|
||||||
const baseUrl = getBrowserSWPreviewBaseUrl();
|
const baseUrl = getBrowserSWPreviewBaseUrl();
|
||||||
const outputDir = `${baseUrl}/${prefix}`;
|
const outputDir = `${baseUrl}/${
|
||||||
|
isForInGameEdition ? 'in-game-editor-preview' : 'preview'
|
||||||
|
}`;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'[BrowserSWPreviewLauncher] Preview will be served from:',
|
'[BrowserSWPreviewLauncher] Preview will be served from:',
|
||||||
@@ -89,7 +90,6 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
|||||||
const browserSWFileSystem = new BrowserSWFileSystem({
|
const browserSWFileSystem = new BrowserSWFileSystem({
|
||||||
filesContent,
|
filesContent,
|
||||||
baseUrl: `${baseUrl}/`,
|
baseUrl: `${baseUrl}/`,
|
||||||
prefix,
|
|
||||||
});
|
});
|
||||||
const fileSystem = assignIn(
|
const fileSystem = assignIn(
|
||||||
new gd.AbstractFileSystemJS(),
|
new gd.AbstractFileSystemJS(),
|
||||||
@@ -113,21 +113,12 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
|||||||
});
|
});
|
||||||
|
|
||||||
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
||||||
const lastDebuggerId = debuggerIds.length
|
const shouldHotReload = previewOptions.hotReload && !!debuggerIds.length;
|
||||||
? debuggerIds[debuggerIds.length - 1]
|
|
||||||
: null;
|
|
||||||
const shouldHotReload = previewOptions.hotReload && lastDebuggerId !== null;
|
|
||||||
|
|
||||||
// We abuse the "hot reload" to choose if we open a new window or replace
|
// Immediately open windows (otherwise Safari will block the window opening if done after
|
||||||
// the content of an existing one. But hot reload is NOT implemented (yet -
|
// an asynchronous operation).
|
||||||
// it would need to generate the preview in the same place and trigger a reload
|
const previewWindows = shouldHotReload
|
||||||
// of the scripts).
|
? []
|
||||||
const existingPreviewWindow = shouldHotReload
|
|
||||||
? getExistingPreviewWindowForDebuggerId(lastDebuggerId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const previewWindows = existingPreviewWindow
|
|
||||||
? [existingPreviewWindow]
|
|
||||||
: Array.from({ length: numberOfWindows }, () => {
|
: Array.from({ length: numberOfWindows }, () => {
|
||||||
try {
|
try {
|
||||||
return immediatelyOpenNewPreviewWindow(project);
|
return immediatelyOpenNewPreviewWindow(project);
|
||||||
@@ -141,7 +132,9 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
|||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.getPreviewDebuggerServer().startServer();
|
await this.getPreviewDebuggerServer().startServer({
|
||||||
|
origin: new URL(getBrowserSWPreviewBaseUrl()).origin,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore any error when running the debugger server - the preview
|
// Ignore any error when running the debugger server - the preview
|
||||||
// can still work without it.
|
// can still work without it.
|
||||||
@@ -168,15 +161,18 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
|||||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNativeMobileApp()) {
|
previewExportOptions.useWindowMessageDebuggerClient();
|
||||||
previewExportOptions.useMinimalDebuggerClient();
|
|
||||||
} else {
|
const includeFileHashs = this.props.getIncludeFileHashs();
|
||||||
previewExportOptions.useWindowMessageDebuggerClient();
|
for (const includeFile in includeFileHashs) {
|
||||||
|
const hash = includeFileHashs[includeFile];
|
||||||
|
previewExportOptions.setIncludeFileHash(includeFile, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scripts generated from extensions keep the same URL even after being modified.
|
previewExportOptions.setProjectDataOnlyExport(
|
||||||
// Use a cache bursting parameter to force the browser to reload them.
|
// Only export project data if asked and if a hot-reloading is being done.
|
||||||
previewExportOptions.setNonRuntimeScriptsCacheBurst(Date.now());
|
shouldHotReload && previewOptions.projectDataOnlyExport
|
||||||
|
);
|
||||||
|
|
||||||
previewExportOptions.setFullLoadingScreen(
|
previewExportOptions.setFullLoadingScreen(
|
||||||
previewOptions.fullLoadingScreen
|
previewOptions.fullLoadingScreen
|
||||||
@@ -233,26 +229,32 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
|||||||
previewExportOptions.delete();
|
previewExportOptions.delete();
|
||||||
exporter.delete();
|
exporter.delete();
|
||||||
|
|
||||||
// Store files in IndexedDB instead of uploading to S3
|
|
||||||
console.log(
|
console.log(
|
||||||
'[BrowserSWPreviewLauncher] Storing preview files in IndexedDB...'
|
'[BrowserSWPreviewLauncher] Storing preview files in IndexedDB...'
|
||||||
);
|
);
|
||||||
await browserSWFileSystem.applyPendingOperations();
|
await browserSWFileSystem.applyPendingOperations();
|
||||||
|
|
||||||
// Change the HTML file displayed by the preview window so that it starts loading
|
if (shouldHotReload) {
|
||||||
// the game.
|
console.log('[BrowserSWPreviewLauncher] Triggering hot reload...');
|
||||||
console.log('[BrowserSWPreviewLauncher] Opening preview in window(s)...');
|
debuggerIds.forEach(debuggerId => {
|
||||||
previewWindows.forEach((previewWindow: WindowProxy) => {
|
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
|
||||||
previewWindow.location = outputDir + '/index.html';
|
command: 'hotReload',
|
||||||
try {
|
});
|
||||||
previewWindow.focus();
|
});
|
||||||
} catch (e) {}
|
} else {
|
||||||
});
|
console.log(
|
||||||
|
'[BrowserSWPreviewLauncher] Opening new preview window(s)...'
|
||||||
// If the preview windows are new, register them so that they can be accessed
|
);
|
||||||
// by the debugger and for the captures to be detected when they close.
|
|
||||||
if (!existingPreviewWindow) {
|
|
||||||
previewWindows.forEach((previewWindow: WindowProxy) => {
|
previewWindows.forEach((previewWindow: WindowProxy) => {
|
||||||
|
// Change the HTML file displayed by the preview window so that it starts loading
|
||||||
|
// the game.
|
||||||
|
previewWindow.location = outputDir + '/index.html';
|
||||||
|
try {
|
||||||
|
previewWindow.focus();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Register the window so that it can be accessed
|
||||||
|
// by the debugger and for the captures to be detected when it closes.
|
||||||
const debuggerId = registerNewPreviewWindow(previewWindow);
|
const debuggerId = registerNewPreviewWindow(previewWindow);
|
||||||
browserPreviewDebuggerServer.registerCallbacks({
|
browserPreviewDebuggerServer.registerCallbacks({
|
||||||
onErrorReceived: () => {},
|
onErrorReceived: () => {},
|
||||||
|
@@ -204,7 +204,7 @@ export default class LocalPreviewLauncher extends React.Component<
|
|||||||
// useful if the user opens the Debugger editor later, or want to
|
// useful if the user opens the Debugger editor later, or want to
|
||||||
// hot reload.
|
// hot reload.
|
||||||
return this.getPreviewDebuggerServer()
|
return this.getPreviewDebuggerServer()
|
||||||
.startServer()
|
.startServer({})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
// Ignore any error when running the debugger server - the preview
|
// Ignore any error when running the debugger server - the preview
|
||||||
// can still work without it.
|
// can still work without it.
|
||||||
|
@@ -91,7 +91,7 @@ export type ServerAddress = {
|
|||||||
|
|
||||||
/** Interface to run a debugger server for previews. */
|
/** Interface to run a debugger server for previews. */
|
||||||
export type PreviewDebuggerServer = {|
|
export type PreviewDebuggerServer = {|
|
||||||
startServer: () => Promise<void>,
|
startServer: ({ origin?: string }) => Promise<void>,
|
||||||
getServerState: () => 'started' | 'stopped',
|
getServerState: () => 'started' | 'stopped',
|
||||||
getExistingDebuggerIds: () => Array<DebuggerId>,
|
getExistingDebuggerIds: () => Array<DebuggerId>,
|
||||||
sendMessage: (id: DebuggerId, message: Object) => void,
|
sendMessage: (id: DebuggerId, message: Object) => void,
|
||||||
|
60
newIDE/app/src/ServiceWorkerSetup.js
Normal file
60
newIDE/app/src/ServiceWorkerSetup.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
const PUBLIC_URL: string = process.env.PUBLIC_URL || '';
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
export function registerServiceWorker() {
|
||||||
|
const enabled = true;
|
||||||
|
const serviceWorker = navigator.serviceWorker;
|
||||||
|
|
||||||
|
if (enabled && serviceWorker) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Use a cache-buster for development so that the service worker is
|
||||||
|
// always reloaded when the app is reloaded.
|
||||||
|
const swUrl = isDev
|
||||||
|
? `${PUBLIC_URL}/service-worker.js?dev=${Date.now()}`
|
||||||
|
: `${PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
const alreadyHasAServiceWorker = !!serviceWorker.controller;
|
||||||
|
if (alreadyHasAServiceWorker) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all tabs for this page are closed.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Service worker has been installed for the first time.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
serviceWorker.ready.then(registration => {
|
||||||
|
// Forces a check right now for a newer service worker script in development.
|
||||||
|
// If there is one, it will be installed (see the service worker script to verify how in development
|
||||||
|
// a new service worker script does a `self.skipWaiting()` and `self.clients.claim()`).
|
||||||
|
registration.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Service Worker disabled - TODO: fallback to S3?');
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import {
|
|||||||
sendProgramOpening,
|
sendProgramOpening,
|
||||||
installAnalyticsEvents,
|
installAnalyticsEvents,
|
||||||
} from './Utils/Analytics/EventSender';
|
} from './Utils/Analytics/EventSender';
|
||||||
import { registerServiceWorker } from './serviceWorker';
|
import { registerServiceWorker } from './ServiceWorkerSetup';
|
||||||
import './UI/icomoon-font.css'; // Styles for Icomoon font.
|
import './UI/icomoon-font.css'; // Styles for Icomoon font.
|
||||||
import optionalRequire from './Utils/OptionalRequire';
|
import optionalRequire from './Utils/OptionalRequire';
|
||||||
import { loadScript } from './Utils/LoadScript';
|
import { loadScript } from './Utils/LoadScript';
|
||||||
|
@@ -1,112 +0,0 @@
|
|||||||
// @flow
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(
|
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const PUBLIC_URL: string = process.env.PUBLIC_URL || '';
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
export function registerServiceWorker(config) {
|
|
||||||
const enabled = true;
|
|
||||||
|
|
||||||
if (enabled && 'serviceWorker' in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(PUBLIC_URL, window.location.href);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
|
||||||
console.log(
|
|
||||||
`The origin in PUBLIC_URL (${
|
|
||||||
publicUrl.origin
|
|
||||||
}) is different than the current origin (${
|
|
||||||
window.location.origin
|
|
||||||
}) - Service Worker disabled`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
// Use a cache-buster for development so that the service worker is
|
|
||||||
// always reloaded when the app is reloaded.
|
|
||||||
const swUrl = isDev
|
|
||||||
? `${PUBLIC_URL}/service-worker.js?dev=${Date.now()}`
|
|
||||||
: `${PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
|
|
||||||
if (isLocalhost) {
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
|
||||||
// service worker/PWA documentation.
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
if (isDev) registration.update(); // TODO
|
|
||||||
console.log(
|
|
||||||
'This web app is being served cache-first by a service ' +
|
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('Service Worker disabled - TODO: fallback to S3?');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl, config) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then(registration => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if (installingWorker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === 'installed') {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the updated precached content has been fetched,
|
|
||||||
// but the previous service worker will still serve the older
|
|
||||||
// content until all client tabs are closed.
|
|
||||||
console.log(
|
|
||||||
'New content is available and will be used when all ' +
|
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onUpdate) {
|
|
||||||
config.onUpdate(registration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// "Content is cached for offline use." message.
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onSuccess) {
|
|
||||||
config.onSuccess(registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user