mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
fix service_worker lose dir_handle on restart
This commit is contained in:
@@ -18,10 +18,10 @@ let dir_handle
|
|||||||
self.addEventListener('message', async function(e) {
|
self.addEventListener('message', async function(e) {
|
||||||
const msg = e.data
|
const msg = e.data
|
||||||
let reply
|
let reply
|
||||||
if(msg.type == 'SET') {
|
if(msg.type == 'SET_DIR_HANDLE') {
|
||||||
dir_handle = msg.data
|
dir_handle = msg.data
|
||||||
reply = null
|
reply = null
|
||||||
} else if(msg.type == 'GET') {
|
} else if(msg.type == 'GET_DIR_HANDLE') {
|
||||||
reply = dir_handle
|
reply = dir_handle
|
||||||
} else {
|
} else {
|
||||||
throw new Error('unknown message type: ' + msg.type)
|
throw new Error('unknown message type: ' + msg.type)
|
||||||
@@ -29,38 +29,75 @@ self.addEventListener('message', async function(e) {
|
|||||||
e.ports[0].postMessage(reply)
|
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
|
// Fake directory, http requests to this directory intercepted by service_worker
|
||||||
const FILES_ROOT = new URL('.', globalThis.location).pathname + '__leporello_files/'
|
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 => {
|
self.addEventListener("fetch", event => {
|
||||||
const url = new URL(event.request.url)
|
const url = new URL(event.request.url)
|
||||||
if(url.pathname.startsWith(FILES_ROOT)) {
|
if(url.pathname.startsWith(FILES_ROOT)) {
|
||||||
const path = url.pathname.replace(FILES_ROOT, '')
|
event.respondWith(serve_response_from_dir(event))
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const send_message = (message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalThis.clear_directory_handle = () => {
|
globalThis.clear_directory_handle = () => {
|
||||||
send_message({type: 'SET', data: null})
|
send_message({type: 'SET_DIR_HANDLE', data: null})
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +24,31 @@ let dir_handle
|
|||||||
|
|
||||||
const request_directory_handle = async () => {
|
const request_directory_handle = async () => {
|
||||||
dir_handle = await globalThis.showDirectoryPicker()
|
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
|
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 = () => {
|
export const load_persisted_directory_handle = () => {
|
||||||
return navigator.serviceWorker.register('service_worker.js')
|
return navigator.serviceWorker.register('service_worker.js')
|
||||||
.then(() => navigator.serviceWorker.ready)
|
.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 => {
|
.then(async h => {
|
||||||
if(h == null || (await h.queryPermission()) != 'granted') {
|
if(h == null || (await h.queryPermission()) != 'granted') {
|
||||||
return null
|
return null
|
||||||
|
|||||||
41
src/index.js
41
src/index.js
@@ -1,6 +1,6 @@
|
|||||||
import {UI} from './editor/ui.js'
|
import {UI} from './editor/ui.js'
|
||||||
import {EFFECTS, render_initial_state, apply_side_effects} from './effects.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 =>
|
const EXAMPLE = `const fib = n =>
|
||||||
n == 0 || n == 1
|
n == 0 || n == 1
|
||||||
@@ -34,9 +34,18 @@ const get_html_url = state => {
|
|||||||
: base + state.html_file + '?leporello'
|
: 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
|
// By default run code in hidden iframe, until user explicitly opens visible
|
||||||
// window
|
// window
|
||||||
const open_run_iframe = (state, onload) => {
|
const open_run_iframe = (state) => {
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe')
|
||||||
iframe.src = get_html_url(state)
|
iframe.src = get_html_url(state)
|
||||||
iframe.setAttribute('hidden', '')
|
iframe.setAttribute('hidden', '')
|
||||||
@@ -44,7 +53,7 @@ const open_run_iframe = (state, onload) => {
|
|||||||
// for run_window, do not set unhandled rejection, because having rejected
|
// for run_window, do not set unhandled rejection, because having rejected
|
||||||
// promises in user code is normal condition
|
// promises in user code is normal condition
|
||||||
set_error_handler(iframe.contentWindow, false)
|
set_error_handler(iframe.contentWindow, false)
|
||||||
iframe.contentWindow.addEventListener('load', onload)
|
iframe.contentWindow.addEventListener('load', on_window_load)
|
||||||
globalThis.run_window = iframe.contentWindow
|
globalThis.run_window = iframe.contentWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,17 +71,6 @@ export const open_run_window = state => {
|
|||||||
return nav != null && nav.loadEventEnd > 0
|
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 = () => {
|
const add_load_handler = () => {
|
||||||
/*
|
/*
|
||||||
Wait until 'load event', then set unload handler. The page after
|
Wait until 'load event', then set unload handler. The page after
|
||||||
@@ -89,11 +87,15 @@ export const open_run_window = state => {
|
|||||||
if(is_loaded()) {
|
if(is_loaded()) {
|
||||||
// Already loaded
|
// Already loaded
|
||||||
add_unload_handler()
|
add_unload_handler()
|
||||||
onload()
|
on_window_load()
|
||||||
} else {
|
} else {
|
||||||
next_window.addEventListener('load', () => {
|
next_window.addEventListener('load', () => {
|
||||||
add_unload_handler()
|
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)
|
render_initial_state(ui, state)
|
||||||
|
|
||||||
open_run_iframe(state, () => {
|
open_run_iframe(state)
|
||||||
exec(
|
|
||||||
'open_run_window',
|
|
||||||
new Set(Object.getOwnPropertyNames(globalThis.run_window))
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user