Compare commits

...

5 Commits

Author SHA1 Message Date
Florian Rival
8bc243171a Fix cleaning 2025-10-15 11:40:45 +02:00
Florian Rival
747571958d Logs cleanup 2025-10-15 11:23:41 +02:00
Florian Rival
47cd3425ef Clean up 2025-10-15 11:20:01 +02:00
Florian Rival
cac2b8f6e5 Fix S3 preview 2025-10-15 11:17:42 +02:00
Florian Rival
47c75bab99 Refactoring 2025-10-15 11:13:10 +02:00
14 changed files with 145 additions and 162 deletions

View File

@@ -30,7 +30,6 @@ function openBrowserSWPreviewDB() {
request.onsuccess = () => {
const db = request.result;
console.log('[ServiceWorker] Preview database opened successfully');
resolve(db);
};

View File

@@ -10,6 +10,7 @@ import './UI/icomoon-font.css'; // Styles for Icomoon font.
import browserResourceSources from './ResourcesList/BrowserResourceSources';
import browserResourceExternalEditors from './ResourcesList/BrowserResourceExternalEditors';
import BrowserSWPreviewLauncher from './ExportAndShare/BrowserExporters/BrowserSWPreviewLauncher';
import BrowserS3PreviewLauncher from './ExportAndShare/BrowserExporters/BrowserS3PreviewLauncher';
import {
browserAutomatedExporters,
browserManualExporters,
@@ -19,6 +20,7 @@ import makeExtensionsLoader from './JsExtensionsLoader/BrowserJsExtensionsLoader
import ObjectsEditorService from './ObjectEditor/ObjectsEditorService';
import ObjectsRenderingService from './ObjectsRendering/ObjectsRenderingService';
import { makeBrowserSWEventsFunctionCodeWriter } from './EventsFunctionsExtensionsLoader/CodeWriters/BrowserSWEventsFunctionCodeWriter';
import { makeBrowserS3EventsFunctionCodeWriter } from './EventsFunctionsExtensionsLoader/CodeWriters/BrowserS3EventsFunctionCodeWriter';
import Providers from './MainFrame/Providers';
import ProjectStorageProviders from './ProjectsStorage/ProjectStorageProviders';
import UrlStorageProvider from './ProjectsStorage/UrlStorageProvider';
@@ -30,6 +32,7 @@ import BrowserResourceFetcher from './ProjectsStorage/ResourceFetcher/BrowserRes
import BrowserEventsFunctionsExtensionOpener from './EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionOpener';
import BrowserEventsFunctionsExtensionWriter from './EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter';
import BrowserLoginProvider from './LoginProvider/BrowserLoginProvider';
import { isServiceWorkerSupported } from './ServiceWorkerSetup';
export const create = (authentication: Authentication) => {
Window.setUpContextMenu();
@@ -38,12 +41,17 @@ export const create = (authentication: Authentication) => {
let app = null;
const appArguments = Window.getArguments();
const canUseBrowserSW = isServiceWorkerSupported();
app = (
<Providers
authentication={authentication}
disableCheckForUpdates={!!appArguments['disable-update-check']}
makeEventsFunctionCodeWriter={makeBrowserSWEventsFunctionCodeWriter}
makeEventsFunctionCodeWriter={
canUseBrowserSW
? makeBrowserSWEventsFunctionCodeWriter
: makeBrowserS3EventsFunctionCodeWriter
}
eventsFunctionsExtensionWriter={BrowserEventsFunctionsExtensionWriter}
eventsFunctionsExtensionOpener={BrowserEventsFunctionsExtensionOpener}
>
@@ -67,9 +75,13 @@ export const create = (authentication: Authentication) => {
}) => (
<MainFrame
i18n={i18n}
renderPreviewLauncher={(props, ref) => (
<BrowserSWPreviewLauncher {...props} ref={ref} />
)}
renderPreviewLauncher={(props, ref) =>
canUseBrowserSW ? (
<BrowserSWPreviewLauncher {...props} ref={ref} />
) : (
<BrowserS3PreviewLauncher {...props} ref={ref} />
)
}
renderShareDialog={props => (
<ShareDialog
project={props.project}
@@ -104,7 +116,9 @@ export const create = (authentication: Authentication) => {
filterExamples: !Window.isDev(),
})}
initialFileMetadataToOpen={initialFileMetadataToOpen}
initialExampleSlugToOpen={appArguments['create-from-example'] || null}
initialExampleSlugToOpen={
appArguments['create-from-example'] || null
}
/>
)}
</ProjectStorageProviders>

View File

@@ -23,8 +23,7 @@ const flushBatchedWrites = debounce(async () => {
console.info(
`[BrowserSWEventsFunctionCodeWriter] Storing a batch of ${
writes.length
} extension generated files in IndexedDB...`,
writes.map(w => w.path)
} extension generated files in IndexedDB...`
);
batchedWrites = [];
@@ -85,9 +84,9 @@ 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 + '/');
// TODO: This could break multiple tabs! Consider doing a folder per tab/session?
const relativePath = extensionsCodeUrl.replace(baseUrl, '');
await deleteFilesWithPrefix(relativePath + '/');
} catch (error) {
console.error(
`[BrowserSWEventsFunctionCodeWriter] Failed to clean generated files for extensions code in "${extensionsCodeUrl}/".`,

View File

@@ -0,0 +1,34 @@
// @flow
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
let nextPreviewWindowId = 0;
/**
* Open a window showing a black "loading..." screen. It's important this is done
* NOT in an asynchronous way but JUST after a click. Otherwise, browsers like Safari
* will block the window opening.
*/
export const immediatelyOpenNewPreviewWindow = (
project: gdProject
): WindowProxy => {
const width = project.getGameResolutionWidth();
const height = project.getGameResolutionHeight();
const left = window.screenX + window.innerWidth / 2 - width / 2;
const top = window.screenY + window.innerHeight / 2 - height / 2;
const targetId = 'GDevelopPreview' + nextPreviewWindowId++;
const previewWindow = window.open(
'about:blank',
targetId,
`width=${width},height=${height},left=${left},top=${top}`
);
if (!previewWindow) {
throw new Error(
"Can't open the preview window because of browser restrictions."
);
}
displayBlackLoadingScreenOrThrow(previewWindow);
return previewWindow;
};

View File

@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import BrowserPreviewErrorDialog from './BrowserPreviewErrorDialog';
import BrowserPreviewErrorDialog from '../BrowserPreview/BrowserPreviewErrorDialog';
import BrowserS3FileSystem from '../BrowserS3FileSystem';
import { findGDJS } from '../../../GameEngineFinder/BrowserS3GDJSFinder';
import assignIn from 'lodash/assignIn';
@@ -14,58 +14,23 @@ import {
browserPreviewDebuggerServer,
getExistingPreviewWindowForDebuggerId,
registerNewPreviewWindow,
} from './BrowserPreviewDebuggerServer';
} from '../BrowserPreview/BrowserPreviewDebuggerServer';
import Window from '../../../Utils/Window';
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
import { isNativeMobileApp } from '../../../Utils/Platform';
import { getIDEVersionWithHash } from '../../../Version';
import { getBrowserSWPreviewBaseUrl } from '../BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
import { immediatelyOpenNewPreviewWindow } from '../BrowserPreview/BrowserPreviewWindow';
const gd: libGDevelop = global.gd;
type State = {|
error: ?Error,
|};
let nextPreviewWindowId = 0;
/**
* Open a window showing a black "loading..." screen. It's important this is done
* NOT in an asynchronous way but JUST after a click. Otherwise, browsers like Safari
* will block the window opening.
*/
export const immediatelyOpenNewPreviewWindow = (
project: gdProject
): WindowProxy => {
const width = project.getGameResolutionWidth();
const height = project.getGameResolutionHeight();
const left = window.screenX + window.innerWidth / 2 - width / 2;
const top = window.screenY + window.innerHeight / 2 - height / 2;
const targetId = 'GDevelopPreview' + nextPreviewWindowId++;
const previewWindow = window.open(
'about:blank',
targetId,
`width=${width},height=${height},left=${left},top=${top}`
);
if (!previewWindow) {
throw new Error(
"Can't open the preview window because of browser restrictions."
);
}
console.log('Preview window opened', previewWindow);
displayBlackLoadingScreenOrThrow(previewWindow);
return previewWindow;
};
export default class BrowserS3PreviewLauncher extends React.Component<
PreviewLauncherProps,
State
> {
canDoNetworkPreview = () => false;
canDoHotReload = () => false;
state = {
error: null,
@@ -138,7 +103,7 @@ export default class BrowserS3PreviewLauncher extends React.Component<
try {
await this.getPreviewDebuggerServer().startServer({
origin: new URL(getBrowserSWPreviewBaseUrl()).origin,
origin: new URL(getBaseUrl()).origin,
});
} catch (err) {
// Ignore any error when running the debugger server - the preview

View File

@@ -102,16 +102,13 @@ export default class BrowserSWFileSystem {
} files in IndexedDB for preview...`
);
let totalBytes = 0;
const uploadPromises = this._pendingFiles.map(async file => {
const fullPath = `/${file.path}`; // TODO
const fullPath = `/${file.path}`;
const encoder = new TextEncoder();
const bytes = encoder.encode(file.content).buffer;
console.log(
`[BrowserSWFileSystem] Storing file: ${fullPath} (${
bytes.byteLength
} bytes, ${file.contentType})`
);
totalBytes += bytes.byteLength;
await putFile(fullPath, bytes, file.contentType);
});
@@ -121,7 +118,7 @@ export default class BrowserSWFileSystem {
console.log(
`[BrowserSWFileSystem] Successfully stored all ${
this._pendingFiles.length
} preview files in IndexedDB.`
} preview files in IndexedDB (${Math.ceil(totalBytes / 1000)} kB).`
);
} catch (error) {
console.error(
@@ -142,7 +139,6 @@ export default class BrowserSWFileSystem {
};
clearDir = (path: string) => {
// TODO: add to a pending operation list so we ensure it's executed.
console.info(`[BrowserSWFileSystem] Clearing directory: ${path}...`);
this._pendingDeleteOperations.push(deleteFilesWithPrefix(path));
};
@@ -205,10 +201,6 @@ export default class BrowserSWFileSystem {
const relativePath = fullPath.replace(this.baseUrl, '');
const contentType = getContentType(fullPath);
console.log(
`[BrowserSWFileSystem] Queuing file for IndexedDB: ${relativePath} (${contentType})`
);
// Queue the file to be written to IndexedDB
this._pendingFiles.push({
path: relativePath,

View File

@@ -1,11 +1,5 @@
// @flow
/**
* A utility module for managing local game preview files in IndexedDB.
* This provides a clean, promise-based interface for storing and retrieving
* game files that will be served by the service worker.
*/
// If updated, also update the service worker template.
const DB_NAME = 'gdevelop-browser-sw-preview';
const STORE_NAME = 'files';
@@ -130,12 +124,20 @@ export const putFile = async (
reject(error);
};
transaction.onabort = () => {
const error = transaction.error || new Error('Transaction aborted');
console.error(
'[BrowserSWIndexedDB] Transaction aborted while putting file:',
path,
error
);
reject(error);
};
transaction.oncomplete = () => {
resolve();
};
// TODO: add onabort?
const objectStore = transaction.objectStore(STORE_NAME);
const record: FileRecord = { bytes, contentType };

View File

@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import BrowserPreviewErrorDialog from '../BrowserS3PreviewLauncher/BrowserPreviewErrorDialog';
import BrowserPreviewErrorDialog from '../BrowserPreview/BrowserPreviewErrorDialog';
import BrowserSWFileSystem from '../BrowserSWFileSystem';
import { findGDJS } from '../../../GameEngineFinder/BrowserS3GDJSFinder';
import assignIn from 'lodash/assignIn';
@@ -11,57 +11,24 @@ import {
import {
browserPreviewDebuggerServer,
registerNewPreviewWindow,
} from '../BrowserS3PreviewLauncher/BrowserPreviewDebuggerServer';
} from '../BrowserPreview/BrowserPreviewDebuggerServer';
import Window from '../../../Utils/Window';
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
import { isNativeMobileApp } from '../../../Utils/Platform';
import { getIDEVersionWithHash } from '../../../Version';
import { getBrowserSWPreviewBaseUrl } from './BrowserSWPreviewIndexedDB';
import { immediatelyOpenNewPreviewWindow } from '../BrowserPreview/BrowserPreviewWindow';
const gd: libGDevelop = global.gd;
type State = {|
error: ?Error,
|};
let nextPreviewWindowId = 0;
/**
* Open a window showing a black "loading..." screen. It's important this is done
* NOT in an asynchronous way but JUST after a click. Otherwise, browsers like Safari
* will block the window opening.
*/
export const immediatelyOpenNewPreviewWindow = (
project: gdProject
): WindowProxy => {
const width = project.getGameResolutionWidth();
const height = project.getGameResolutionHeight();
const left = window.screenX + window.innerWidth / 2 - width / 2;
const top = window.screenY + window.innerHeight / 2 - height / 2;
const targetId = 'GDevelopPreview' + nextPreviewWindowId++;
const previewWindow = window.open(
'about:blank',
targetId,
`width=${width},height=${height},left=${left},top=${top}`
);
if (!previewWindow) {
throw new Error(
"Can't open the preview window because of browser restrictions."
);
}
displayBlackLoadingScreenOrThrow(previewWindow);
return previewWindow;
};
export default class BrowserSWPreviewLauncher extends React.Component<
PreviewLauncherProps,
State
> {
canDoNetworkPreview = () => false;
canDoHotReload = () => false;
state = {
error: null,

View File

@@ -52,7 +52,6 @@ export default class LocalPreviewLauncher extends React.Component<
State
> {
canDoNetworkPreview = () => true;
canDoHotReload = () => true;
state = {
networkPreviewDialogOpen: false,

View File

@@ -112,7 +112,6 @@ export type HotReloaderLog = {|
export type PreviewLauncherInterface = {
launchPreview: (previewOptions: PreviewOptions) => Promise<any>,
canDoNetworkPreview: () => boolean,
canDoHotReload: () => boolean,
+closePreview?: (windowId: number) => void,
+getPreviewDebuggerServer: () => ?PreviewDebuggerServer,
};

View File

@@ -1,60 +1,73 @@
// @flow
import optionalRequire from './Utils/OptionalRequire';
import { isNativeMobileApp } from './Utils/Platform';
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;
const electron = optionalRequire('electron');
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?');
}
export function isServiceWorkerSupported() {
return !!serviceWorker;
}
export function registerServiceWorker() {
if (isNativeMobileApp() || !!electron) {
return;
}
if (!serviceWorker) {
console.warn(
'Service Worker not supported on this deployment (probably: not HTTPS and not localhost).'
);
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`;
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();
});
}
});
}

View File

@@ -79,7 +79,7 @@ import EditorMosaicPlayground from './EditorMosaicPlayground';
import EditorNavigator from '../UI/EditorMosaic/EditorNavigator';
import PropertiesEditor from '../PropertiesEditor';
import { OpenConfirmDialog } from '../ProjectsStorage/OpenConfirmDialog';
import BrowserPreviewErrorDialog from '../ExportAndShare/BrowserExporters/BrowserS3PreviewLauncher/BrowserPreviewErrorDialog';
import BrowserPreviewErrorDialog from '../ExportAndShare/BrowserExporters/BrowserPreview/BrowserPreviewErrorDialog';
import RaisedButton from '../UI/RaisedButton';
import Text from '../UI/Text';
import IconButton from '../UI/IconButton';