mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
[WIP] Refactoring
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/* 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');
|
||||
@@ -16,7 +17,7 @@ const DB_VERSION = 1;
|
||||
/**
|
||||
* Opens the IndexedDB database for browser SW preview files.
|
||||
*/
|
||||
function openPreviewDB() {
|
||||
function openBrowserSWPreviewDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
@@ -49,41 +50,57 @@ function openPreviewDB() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file from IndexedDB.
|
||||
* Retrieves a preview file from IndexedDB.
|
||||
*/
|
||||
async function getPreviewFile(path) {
|
||||
async function getBrowserSWPreviewFile(path) {
|
||||
try {
|
||||
const db = await openPreviewDB();
|
||||
const db = await openBrowserSWPreviewDB();
|
||||
|
||||
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 {
|
||||
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 error = transaction.error || new Error('Transaction failed');
|
||||
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||
|
||||
// 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);
|
||||
reject(error);
|
||||
safeReject(error);
|
||||
};
|
||||
|
||||
const objectStore = transaction.objectStore(STORE_NAME);
|
||||
const request = objectStore.get(path);
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const req = store.get(path);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result;
|
||||
if (!result) {
|
||||
console.warn('[ServiceWorker] File not found in IndexedDB:', path);
|
||||
}
|
||||
resolve(result || null);
|
||||
req.onsuccess = () => {
|
||||
const result = req.result;
|
||||
safeResolve(result || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
const error = request.error || new Error('Get operation failed');
|
||||
req.onerror = () => {
|
||||
const error = req.error || new Error('Get operation failed');
|
||||
console.error('[ServiceWorker] Error retrieving file from IndexedDB:', path, error);
|
||||
reject(error);
|
||||
safeReject(error);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[ServiceWorker] Exception during get operation:', path, error);
|
||||
reject(error);
|
||||
safeReject(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -105,7 +122,7 @@ self.addEventListener('fetch', (event) => {
|
||||
event.respondWith((async () => {
|
||||
try {
|
||||
// Try to get the file from IndexedDB
|
||||
const fileRecord = await getPreviewFile(relativePath);
|
||||
const fileRecord = await getBrowserSWPreviewFile(relativePath);
|
||||
|
||||
if (!fileRecord) {
|
||||
console.warn('[ServiceWorker] File not found in IndexedDB:', relativePath);
|
||||
@@ -163,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
|
||||
importScripts(
|
||||
'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
|
||||
);
|
||||
|
||||
/* global workbox */
|
||||
if (workbox) {
|
||||
console.log('[ServiceWorker] Workbox loaded successfully');
|
||||
|
@@ -3,10 +3,13 @@ import {
|
||||
type EventsFunctionCodeWriter,
|
||||
type EventsFunctionCodeWriterCallbacks,
|
||||
} from '..';
|
||||
import { deleteFilesWithPrefix, putFile } from '../../Utils/BrowserSWIndexedDB';
|
||||
import {
|
||||
deleteFilesWithPrefix,
|
||||
putFile,
|
||||
getBrowserSWPreviewBaseUrl,
|
||||
} from '../../ExportAndShare/BrowserExporters/BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||
import slugs from 'slugs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { getBrowserSWPreviewBaseUrl } from '../../Utils/BrowserSWIndexedDB';
|
||||
|
||||
let batchedWrites: Array<{
|
||||
path: string,
|
||||
@@ -40,7 +43,6 @@ const flushBatchedWrites = debounce(async () => {
|
||||
results.forEach((result, index) => {
|
||||
const write = writes[index];
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(`[BrowserSWEventsFunctionCodeWriter] Stored: ${write.path}`);
|
||||
write.onSuccess();
|
||||
} else {
|
||||
console.error(
|
||||
@@ -83,6 +85,8 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
||||
// 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(
|
||||
@@ -106,9 +110,7 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
||||
const path = getPathFor(functionCodeNamespace);
|
||||
onWriteFile({ includeFile: path, content: code });
|
||||
const relativePath = path.replace(baseUrl, '');
|
||||
console.log(
|
||||
`[BrowserSWEventsFunctionCodeWriter] Writing function code to ${relativePath}...`
|
||||
);
|
||||
|
||||
return writeFileInNextBatch(relativePath, code);
|
||||
},
|
||||
|
||||
@@ -119,9 +121,7 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
||||
const path = getPathFor(behaviorCodeNamespace);
|
||||
onWriteFile({ includeFile: path, content: code });
|
||||
const relativePath = path.replace(baseUrl, '');
|
||||
console.log(
|
||||
`[BrowserSWEventsFunctionCodeWriter] Writing behavior code to ${path}...`
|
||||
);
|
||||
|
||||
return writeFileInNextBatch(relativePath, code);
|
||||
},
|
||||
|
||||
@@ -132,9 +132,7 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
||||
const path = getPathFor(objectCodeNamespace);
|
||||
onWriteFile({ includeFile: path, content: code });
|
||||
const relativePath = path.replace(baseUrl, '');
|
||||
console.log(
|
||||
`[BrowserSWEventsFunctionCodeWriter] Writing object code to ${path}...`
|
||||
);
|
||||
|
||||
return writeFileInNextBatch(relativePath, code);
|
||||
},
|
||||
};
|
||||
|
@@ -20,7 +20,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { getBrowserSWPreviewBaseUrl } from '../../../Utils/BrowserSWIndexedDB';
|
||||
import { getBrowserSWPreviewBaseUrl } from '../BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
|
@@ -1,6 +1,9 @@
|
||||
// @flow
|
||||
import path from 'path-browserify';
|
||||
import { deleteFilesWithPrefix, putFile } from '../../Utils/BrowserSWIndexedDB';
|
||||
import {
|
||||
deleteFilesWithPrefix,
|
||||
putFile,
|
||||
} from './BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export type TextFileDescriptor = {|
|
||||
|
@@ -32,7 +32,7 @@ export const getBrowserSWPreviewBaseUrl = (): string => {
|
||||
* Opens or returns the existing IndexedDB database connection.
|
||||
* Handles database upgrades and version management.
|
||||
*/
|
||||
const openBrowserSWIndexedDB = (): Promise<IDBDatabase> => {
|
||||
const openBrowserSWPreviewIndexedDB = (): Promise<IDBDatabase> => {
|
||||
if (dbInstance && dbInstance.version === DB_VERSION) {
|
||||
return Promise.resolve(dbInstance);
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export const putFile = async (
|
||||
contentType: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const db = await openBrowserSWIndexedDB();
|
||||
const db = await openBrowserSWPreviewIndexedDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
@@ -174,7 +174,7 @@ export const deleteFilesWithPrefix = async (
|
||||
pathPrefix: string
|
||||
): Promise<number> => {
|
||||
try {
|
||||
const db = await openBrowserSWIndexedDB();
|
||||
const db = await openBrowserSWPreviewIndexedDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
@@ -17,7 +17,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { getBrowserSWPreviewBaseUrl } from '../../../Utils/BrowserSWIndexedDB';
|
||||
import { getBrowserSWPreviewBaseUrl } from './BrowserSWPreviewIndexedDB';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
@@ -78,7 +78,9 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
||||
const isForInGameEdition = false; // TODO: adapt for the 3D editor branch.
|
||||
|
||||
const baseUrl = getBrowserSWPreviewBaseUrl();
|
||||
const outputDir = `${baseUrl}/${isForInGameEdition ? 'in-game-editor-preview' : 'preview'}`;
|
||||
const outputDir = `${baseUrl}/${
|
||||
isForInGameEdition ? 'in-game-editor-preview' : 'preview'
|
||||
}`;
|
||||
|
||||
console.log(
|
||||
'[BrowserSWPreviewLauncher] Preview will be served from:',
|
||||
@@ -113,7 +115,7 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
||||
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
||||
const shouldHotReload = previewOptions.hotReload && !!debuggerIds.length;
|
||||
|
||||
// Immediatel open windows (otherwise Safari will block the window opening if done after
|
||||
// Immediately open windows (otherwise Safari will block the window opening if done after
|
||||
// an asynchronous operation).
|
||||
const previewWindows = shouldHotReload
|
||||
? []
|
||||
@@ -159,13 +161,7 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||
}
|
||||
|
||||
// TODO
|
||||
// if (isNativeMobileApp()) {
|
||||
// previewExportOptions.useMinimalDebuggerClient();
|
||||
// } else {
|
||||
previewExportOptions.useWindowMessageDebuggerClient();
|
||||
// }
|
||||
|
||||
previewExportOptions.useWindowMessageDebuggerClient();
|
||||
|
||||
const includeFileHashs = this.props.getIncludeFileHashs();
|
||||
for (const includeFile in includeFileHashs) {
|
||||
@@ -178,11 +174,6 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
||||
shouldHotReload && previewOptions.projectDataOnlyExport
|
||||
);
|
||||
|
||||
// TODO: remove as useless now because no cache by the service worker.
|
||||
// Scripts generated from extensions keep the same URL even after being modified.
|
||||
// Use a cache bursting parameter to force the browser to reload them.
|
||||
// previewExportOptions.setNonRuntimeScriptsCacheBurst(Date.now());
|
||||
|
||||
previewExportOptions.setFullLoadingScreen(
|
||||
previewOptions.fullLoadingScreen
|
||||
);
|
||||
@@ -238,34 +229,32 @@ export default class BrowserSWPreviewLauncher extends React.Component<
|
||||
previewExportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
// Store files in IndexedDB instead of uploading to S3
|
||||
console.log(
|
||||
'[BrowserSWPreviewLauncher] Storing preview files in IndexedDB...'
|
||||
);
|
||||
await browserSWFileSystem.applyPendingOperations();
|
||||
|
||||
// Change the HTML file displayed by the preview window so that it starts loading
|
||||
// the game.
|
||||
console.log('[BrowserSWPreviewLauncher] Opening preview in window(s)...');
|
||||
previewWindows.forEach((previewWindow: WindowProxy) => {
|
||||
previewWindow.location = outputDir + '/index.html';
|
||||
try {
|
||||
previewWindow.focus();
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
// 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 (shouldHotReload) {
|
||||
console.log('Sending hot reload message to debuggers', debuggerIds);
|
||||
console.log('[BrowserSWPreviewLauncher] Triggering hot reload...');
|
||||
debuggerIds.forEach(debuggerId => {
|
||||
console.log('Sending hot reload message to debugger', debuggerId);
|
||||
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
|
||||
command: 'hotReload',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
'[BrowserSWPreviewLauncher] Opening new preview window(s)...'
|
||||
);
|
||||
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);
|
||||
browserPreviewDebuggerServer.registerCallbacks({
|
||||
onErrorReceived: () => {},
|
||||
|
@@ -204,7 +204,7 @@ export default class LocalPreviewLauncher extends React.Component<
|
||||
// useful if the user opens the Debugger editor later, or want to
|
||||
// hot reload.
|
||||
return this.getPreviewDebuggerServer()
|
||||
.startServer()
|
||||
.startServer({})
|
||||
.catch(err => {
|
||||
// Ignore any error when running the debugger server - the preview
|
||||
// can still work without it.
|
||||
|
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,
|
||||
installAnalyticsEvents,
|
||||
} from './Utils/Analytics/EventSender';
|
||||
import { registerServiceWorker } from './serviceWorker';
|
||||
import { registerServiceWorker } from './ServiceWorkerSetup';
|
||||
import './UI/icomoon-font.css'; // Styles for Icomoon font.
|
||||
import optionalRequire from './Utils/OptionalRequire';
|
||||
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