From 686055dc7c9c39c59386f937ab04060aecc699ed Mon Sep 17 00:00:00 2001 From: Dmitry Vasilev Date: Mon, 19 Jun 2023 12:51:04 +0300 Subject: [PATCH] fix service_worker lose dir_handle on restart --- service_worker.js | 93 +++++++++++++++++++++++++++++++++-------------- src/filesystem.js | 23 ++++++++++-- src/index.js | 41 ++++++++++----------- 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/service_worker.js b/service_worker.js index a4a9579..09f2ee9 100644 --- a/service_worker.js +++ b/service_worker.js @@ -18,10 +18,10 @@ let dir_handle self.addEventListener('message', async function(e) { const msg = e.data let reply - if(msg.type == 'SET') { + if(msg.type == 'SET_DIR_HANDLE') { dir_handle = msg.data reply = null - } else if(msg.type == 'GET') { + } else if(msg.type == 'GET_DIR_HANDLE') { reply = dir_handle } else { throw new Error('unknown message type: ' + msg.type) @@ -29,38 +29,75 @@ self.addEventListener('message', async function(e) { e.ports[0].postMessage(reply) }) +const send_message = (client, message) => { + return new Promise(function(resolve) { + const messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = function(event) { + resolve(event.data) + }; + client.postMessage(message, + [messageChannel.port2]); + }); +} + // Fake directory, http requests to this directory intercepted by service_worker const FILES_ROOT = new URL('.', globalThis.location).pathname + '__leporello_files/' +const serve_response_from_dir = async event => { + const url = new URL(event.request.url) + const path = url.pathname.replace(FILES_ROOT, '') + + let file + + if(path == '__leporello_blank.html') { + file = '' + } else if(dir_handle != null) { + file = await read_file(dir_handle, path) + } else { + let client = await self.clients.get(event.clientId) + + if(client == null) { + // Try to find main window and get dir_handle from it + for(const c of await self.clients.matchAll()) { + if(new URL(c.url).pathname == '/') { + client = c + } + } + } + + + // client is null for run_window initial page load, and is run_window for + // js scripts + if(client == null) { + // User probably reloaded run_window by manually hitting F5 after IDE + // window was closed + return new Response("", {status: 404}) + } else { + dir_handle = await send_message(client, {type: 'GET_DIR_HANDLE'}) + if(dir_handle == null) { + return new Response("", {status: 404}) + } else { + file = await read_file(dir_handle, path) + } + } + } + + const headers = new Headers([ + [ + 'Content-Type', + path.endsWith('.js') || path.endsWith('.mjs') + ? 'text/javascript' + : 'text/html' + ] + ]) + + return new Response(file, {headers}) +} + self.addEventListener("fetch", event => { const url = new URL(event.request.url) if(url.pathname.startsWith(FILES_ROOT)) { - const path = url.pathname.replace(FILES_ROOT, '') - - let file - - if(path == '__leporello_blank.html') { - file = Promise.resolve('') - } else if(dir_handle != null) { - file = read_file(dir_handle, path) - } else { - // Delegate request to browser - return - } - - const headers = new Headers([ - [ - 'Content-Type', - path.endsWith('.js') || path.endsWith('.mjs') - ? 'text/javascript' - : 'text/html' - ] - ]) - - const response = file.then(file => - new Response(file, {headers}) - ) - event.respondWith(response) + event.respondWith(serve_response_from_dir(event)) } }) diff --git a/src/filesystem.js b/src/filesystem.js index 5a2dc65..c772873 100644 --- a/src/filesystem.js +++ b/src/filesystem.js @@ -16,7 +16,7 @@ const send_message = (message) => { } globalThis.clear_directory_handle = () => { - send_message({type: 'SET', data: null}) + send_message({type: 'SET_DIR_HANDLE', data: null}) window.location.reload() } @@ -24,14 +24,31 @@ let dir_handle const request_directory_handle = async () => { dir_handle = await globalThis.showDirectoryPicker() - await send_message({type: 'SET', data: dir_handle}) + await send_message({type: 'SET_DIR_HANDLE', data: dir_handle}) return dir_handle } +export const init_window_service_worker = window => { + window.navigator.serviceWorker.ready.then(() => { + window.navigator.serviceWorker.addEventListener('message', e => { + if(e.data.type == 'GET_DIR_HANDLE') { + e.ports[0].postMessage(dir_handle) + } + }) + }) +} + export const load_persisted_directory_handle = () => { return navigator.serviceWorker.register('service_worker.js') .then(() => navigator.serviceWorker.ready) - .then(() => send_message({type: 'GET'})) + /* + Main window also provides dir_handle to service worker, together with + run_window. run_window provides dir_handle to service worker when it + issues fetch event. If clientId is '' then service worker will try to get + dir_handle from main window + */ + .then(() => init_window_service_worker(globalThis)) + .then(() => send_message({type: 'GET_DIR_HANDLE'})) .then(async h => { if(h == null || (await h.queryPermission()) != 'granted') { return null diff --git a/src/index.js b/src/index.js index 72cef82..498d8d1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import {UI} from './editor/ui.js' import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js' -import {load_dir} from './filesystem.js' +import {load_dir, init_window_service_worker} from './filesystem.js' const EXAMPLE = `const fib = n => n == 0 || n == 1 @@ -34,9 +34,18 @@ const get_html_url = state => { : base + state.html_file + '?leporello' } +const on_window_load = () => { + init_window_service_worker(globalThis.run_window) + exec( + 'open_run_window', + new Set(Object.getOwnPropertyNames(globalThis.run_window)) + ) +} + + // By default run code in hidden iframe, until user explicitly opens visible // window -const open_run_iframe = (state, onload) => { +const open_run_iframe = (state) => { const iframe = document.createElement('iframe') iframe.src = get_html_url(state) iframe.setAttribute('hidden', '') @@ -44,7 +53,7 @@ const open_run_iframe = (state, onload) => { // for run_window, do not set unhandled rejection, because having rejected // promises in user code is normal condition set_error_handler(iframe.contentWindow, false) - iframe.contentWindow.addEventListener('load', onload) + iframe.contentWindow.addEventListener('load', on_window_load) globalThis.run_window = iframe.contentWindow } @@ -62,17 +71,6 @@ export const open_run_window = state => { return nav != null && nav.loadEventEnd > 0 } - // Wait until `load` event before executing code, because service worker that - // is responsible for loading external modules seems not working until `load` - // event fired. TODO: better register SW explicitly and don't rely on - // already registered SW? - const onload = () => { - exec( - 'open_run_window', - new Set(Object.getOwnPropertyNames(globalThis.run_window)) - ) - } - const add_load_handler = () => { /* Wait until 'load event', then set unload handler. The page after @@ -89,11 +87,15 @@ export const open_run_window = state => { if(is_loaded()) { // Already loaded add_unload_handler() - onload() + on_window_load() } else { next_window.addEventListener('load', () => { add_unload_handler() - onload() + // Wait until `load` event before executing code, because service worker that + // is responsible for loading external modules seems not working until `load` + // event fired. TODO: better register SW explicitly and don't rely on + // already registered SW? + on_window_load() }) } } @@ -185,12 +187,7 @@ export const init = (container, _COMMANDS) => { render_initial_state(ui, state) - open_run_iframe(state, () => { - exec( - 'open_run_window', - new Set(Object.getOwnPropertyNames(globalThis.run_window)) - ) - }) + open_run_iframe(state) }) }