reload app_window on every code execution

This commit is contained in:
Dmitry Vasilev
2024-02-23 18:17:50 +08:00
parent cb115bf030
commit 8239e19c89
17 changed files with 437 additions and 350 deletions

View File

@@ -1,5 +1,5 @@
import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq}
from './utils.js'
import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq,
set_is_eq} from './utils.js'
import {
is_eq, is_child, ancestry, ancestry_inc, map_tree,
find_leaf, find_fn_by_location, find_node, find_error_origin_node,
@@ -69,14 +69,19 @@ const apply_eval_result = (state, eval_result) => {
}
}
const run_code = (s, dirty_files) => {
const run_code = (s, globals) => {
const is_globals_eq = s.globals == null
? globals == null
: set_is_eq(s.globals, globals)
const parse_result = load_modules(s.entrypoint, module => {
if(dirty_files != null && dirty_files.includes(module)) {
if(s.dirty_files != null && s.dirty_files.has(module)) {
return s.files[module]
}
if(s.parse_result != null) {
// If globals change, then errors for using undeclared identifiers may be
// no longer valid. Do not use cache
if(is_globals_eq) {
const result = s.parse_result.cache[module]
if(result != null) {
return result
@@ -87,10 +92,18 @@ const run_code = (s, dirty_files) => {
return s.files[module]
}
}, s.globals)
}, globals)
const dirty_files = new Set(
[...(s.dirty_files ?? new Set())].filter(file =>
parse_result.modules[file] == null
)
)
const state = {
...s,
dirty_files,
globals,
parse_result,
calltree: null,
modules: null,
@@ -121,15 +134,7 @@ const run_code = (s, dirty_files) => {
.map(i => i.node.full_import_path)
)
if(
external_imports.length != 0
&&
(
state.external_imports_cache == null
||
external_imports.some(i => state.external_imports_cache[i] == null)
)
) {
if(external_imports.length != 0) {
// Trigger loading of external modules
return {...state,
loading_external_imports_state: {
@@ -138,18 +143,8 @@ const run_code = (s, dirty_files) => {
}
} else {
// Modules were loaded and cached, proceed
return external_imports_loaded(
state,
state,
state.external_imports_cache == null
? null
: filter_object(
state.external_imports_cache,
(module_name, module) => external_imports.includes(module_name)
),
)
return external_imports_loaded(state, state)
}
}
const external_imports_loaded = (
@@ -168,7 +163,6 @@ const external_imports_loaded = (
const state = {
...s,
external_imports_cache: external_imports,
loading_external_imports_state: null
}
@@ -204,6 +198,7 @@ const external_imports_loaded = (
state.on_deferred_call,
state.calltree_changed_token,
state.io_trace,
state.storage,
)
if(result.then != null) {
@@ -252,10 +247,12 @@ const eval_modules_finished = (state, prev_state, result) => {
const input = (state, code, index) => {
const files = {...state.files, [state.current_module]: code}
const next = run_code(
set_cursor_position({...state, files}, index),
[state.current_module]
)
const with_files = {
...state,
files,
dirty_files: new Set([...(state.dirty_files ?? []), state.current_module])
}
const next = set_cursor_position(with_files, index)
const effect_save = {
type: 'write',
args: [
@@ -519,12 +516,10 @@ const change_current_module = (state, current_module) => {
}
const change_entrypoint = (state, entrypoint, current_module = entrypoint) => {
return run_code(
{...state,
entrypoint,
current_module,
}
)
return {...state,
entrypoint,
current_module,
}
}
const change_html_file = (state, html_file) => {
@@ -852,7 +847,7 @@ const on_deferred_call = (state, call, calltree_changed_token, logs) => {
}
const clear_io_trace = state => {
return run_code({...state, io_trace: null})
return {...state, io_trace: null}
}
const load_files = (state, dir) => {
@@ -892,7 +887,7 @@ const apply_entrypoint_settings = (state, entrypoint_settings) => {
const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
// Clear parse cache and rerun code
const with_dir = load_files(state, dir)
return run_code({
return {
...(
entrypoint_settings == null
? with_dir
@@ -904,30 +899,15 @@ const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
// remove cache. We have to clear cache because imports of modules that are
// not available because project_dir is not available have errors and the
// errors are cached
parse_result: null,
external_imports_cache: null,
})
parse_result: {...state.parse_result, cache: {}},
}
}
const create_file = (state, dir, current_module) => {
return {...load_dir(state, dir, true), current_module}
}
const open_app_window = (state, globals) => {
// After we reopen run window, we should reload external modules in the
// context of new window. Clear external_imports_cache
return run_code({
...state,
globals,
external_imports_cache: null,
// Bust parse result cache because list of globals may change
parse_result: null,
// Clear io trace because promises in io_trace become invalid after their
// window close
io_trace: null,
})
}
const open_app_window = state => ({...state, storage: new Map()})
const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
const with_files = state.project_dir == null
@@ -938,6 +918,7 @@ const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
return {
...with_settings,
storage: new Map(),
cursor_position_by_file: {[with_settings.current_module]: cursor_pos},
}
}
@@ -945,6 +926,7 @@ const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
export const COMMANDS = {
get_initial_state,
input,
run_code,
open_app_window,
load_dir,
create_file,

View File

@@ -1,4 +1,4 @@
import {exec, get_state} from '../index.js'
import {exec, get_state, exec_and_reload_app_window} from '../index.js'
import {ValueExplorer} from './value_explorer.js'
import {stringify_for_header} from '../value_explorer_utils.js'
import {el} from './domutils.js'
@@ -98,7 +98,7 @@ export class Editor {
normalize_events(this.ace_editor, {
on_change: () => {
try {
exec('input', this.ace_editor.getValue(), this.get_cursor_position())
exec_and_reload_app_window('input', this.ace_editor.getValue(), this.get_cursor_position())
} catch(e) {
// Do not throw Error to ACE because it breaks typing
console.error(e)

View File

@@ -6,7 +6,7 @@ import {
exec,
get_state,
open_directory,
reload_app_window,
exec_and_reload_app_window,
close_directory,
} from '../index.js'
@@ -20,16 +20,14 @@ export class Files {
this.render(get_state())
}
change_entrypoint(e) {
const file = e.target.value
exec('change_entrypoint', file)
change_entrypoint(entrypoint, current_module) {
exec_and_reload_app_window('change_entrypoint', entrypoint, current_module)
this.ui.editor.focus()
}
change_html_file(e) {
const html_file = e.target.value
exec('change_html_file', html_file)
reload_app_window(get_state())
exec_and_reload_app_window('change_html_file', html_file)
}
@@ -119,7 +117,7 @@ export class Files {
name: 'js_entrypoint',
value: file.path,
checked: state.entrypoint == file.path,
change: e => this.change_entrypoint(e),
change: e => this.change_entrypoint(e.target.value),
click: e => e.stopPropagation(),
})
)
@@ -210,9 +208,9 @@ export class Files {
// Reload all files for simplicity
open_dir(false).then(dir => {
if(is_dir) {
exec('load_dir', dir, true)
exec_and_reload_app_window('load_dir', dir, true)
} else {
exec('create_file', dir, path)
exec_and_reload_app_window('create_file', dir, path)
}
})
}
@@ -229,7 +227,7 @@ export class Files {
if(file.path == null) {
// root of examples dir, do nothing
} else if(file.path == '') {
exec('change_entrypoint', '')
this.change_entrypoint('')
} else {
const find_node = n =>
n.path == file.path
@@ -244,8 +242,7 @@ export class Files {
// in examples mode, on click file we also change entrypoint for
// simplicity
const example = examples.find(e => e.path == example_dir.path)
exec(
'change_entrypoint',
this.change_entrypoint(
example.entrypoint,
file.kind == 'directory' ? undefined : file.path
)

View File

@@ -1,4 +1,4 @@
import {exec, get_state, open_app_window} from '../index.js'
import {exec, get_state, open_app_window, exec_and_reload_app_window} from '../index.js'
import {Editor} from './editor.js'
import {Files} from './files.js'
import {CallTree} from './calltree.js'
@@ -73,7 +73,7 @@ export class UI {
el('a', {
'class': 'statusbar_action first',
href: 'javascript: void(0)',
click: () => exec('clear_io_trace')
click: () => this.clear_io_trace(),
},
'Clear IO trace (F6)'
),
@@ -196,6 +196,10 @@ export class UI {
}
}
clear_io_trace() {
exec_and_reload_app_window('clear_io_trace')
}
open_app_window() {
this.toggle_open_app_window_tooltip(false)
localStorage.onboarding_open_app_window = true

View File

@@ -421,6 +421,7 @@ export const eval_modules = (
on_deferred_call,
calltree_changed_token,
io_trace,
storage,
) => {
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc,
// __await_start, __await_finish, __Multiversion, __create_array, __create_object
@@ -498,6 +499,8 @@ export const eval_modules = (
is_toplevel_call: true,
window: globalThis.app_window,
storage,
}
const result = run(module_fns, cxt, io_trace)

View File

@@ -80,6 +80,8 @@ const add_trivial_definition = node => {
* will be assigned by the time the closures would be called
*/
const DEFAULT_GLOBALS = new Set(['leporello']) // Leporello.js API
export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => {
// sanity check
@@ -94,7 +96,7 @@ export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, m
} else {
const definition = scope[ast.value]
if(definition == null){
if(globals.has(ast.value)) {
if(globals.has(ast.value) || DEFAULT_GLOBALS.has(ast.value)) {
return {node: {...ast, definition: 'global'}, undeclared: null, closed: new Set()}
} else {
return {node: ast, undeclared: [ast], closed: new Set()}

View File

@@ -52,7 +52,7 @@ const get_html_url = state => {
const on_window_load = w => {
init_window_service_worker(w)
exec(
'open_app_window',
'run_code',
new Set(Object.getOwnPropertyNames(w))
)
}
@@ -78,6 +78,7 @@ const open_run_iframe = (state) => {
export const open_app_window = state => {
// TODO set_error_handler? Or we dont need to set_error_handler for child
// window because error is always caught by parent window handler?
exec('open_app_window')
globalThis.app_window.close()
globalThis.app_window = open(get_html_url(state))
init_app_window(globalThis.app_window)
@@ -139,10 +140,8 @@ const init_app_window = w => {
add_load_handler()
}
export const reload_app_window = state => {
// TODO after window location reload, open_app_window command will be fired.
// Maybe we should have separate commands for open_app_window and
// reload_app_window?
const reload_app_window = state => {
// after window location reload, `run_code` command will be fired.
globalThis.app_window.location = get_html_url(state)
}
@@ -154,19 +153,23 @@ const get_entrypoint_settings = () => {
}
}
export const exec_and_reload_app_window = (...exec_args) => {
exec(...exec_args)
reload_app_window(get_state())
}
export const open_directory = () => {
if(globalThis.showDirectoryPicker == null) {
throw new Error('Your browser is not supporting File System Access API')
}
open_dir(true).then(dir => {
exec('load_dir', dir, true, get_entrypoint_settings())
exec_and_reload_app_window('load_dir', dir, true, get_entrypoint_settings())
})
}
export const close_directory = async () => {
close_dir()
exec('load_dir', await examples_dir_promise, false, get_entrypoint_settings())
exec_and_reload_app_window('load_dir', await examples_dir_promise, false, get_entrypoint_settings())
}

View File

@@ -76,18 +76,25 @@ const make_patched_method = (window, original, name, use_context) => {
: original.apply(this, args)
if(value?.[Symbol.toStringTag] == 'Promise') {
// TODO use __original_then, not finally which calls
// patched 'then'?
value = value.finally(() => {
if(cxt_copy != cxt) {
return
}
if(cxt.io_trace_is_replay_aborted) {
// Non necessary
return
}
cxt.io_trace.push({type: 'resolution', index})
})
value = value
.then(val => {
value.status = {ok: true, value: val}
return val
})
.catch(error => {
value.status = {ok: true, error}
throw error
})
.finally(() => {
if(cxt_copy != cxt) {
return
}
if(cxt.io_trace_is_replay_aborted) {
// Non necessary
return
}
cxt.io_trace.push({type: 'resolution', index})
})
}
ok = true
@@ -150,10 +157,11 @@ const make_patched_method = (window, original, name, use_context) => {
)
if(next_resolution != null && !cxt.io_trace_resolver_is_set) {
const original_setTimeout = cxt.window.setTimeout.__original
cxt.io_trace_resolver_is_set = true
original_setTimeout(() => {
// use setTimeout function from host window (because this module was
// loaded as `external` by host window)
setTimeout(() => {
if(cxt_copy != cxt) {
return
}
@@ -180,14 +188,22 @@ const make_patched_method = (window, original, name, use_context) => {
cxt.io_trace[cxt.io_trace_index].type == 'resolution'
) {
const resolution = cxt.io_trace[cxt.io_trace_index]
const resolver = cxt.io_trace_resolvers.get(resolution.index)
const {resolve, reject} = cxt.io_trace_resolvers.get(resolution.index)
cxt.io_trace_index++
if(cxt.io_trace[resolution.index].name == 'setTimeout') {
resolver()
resolve()
} else {
resolver(cxt.io_trace[resolution.index].value)
const promise = cxt.io_trace[resolution.index].value
if(promise.status == null) {
throw new Error('illegal state')
}
if(promise.status.ok) {
resolve(promise.status.value)
} else {
reject(promise.status.error)
}
}
}
}
@@ -203,12 +219,12 @@ const make_patched_method = (window, original, name, use_context) => {
// trace) and instanceof would not work
if(call.value?.[Symbol.toStringTag] == 'Promise') {
// Always make promise originate from app_window
return new cxt.window.Promise(resolve => {
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, resolve)
return new cxt.window.Promise((resolve, reject) => {
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, {resolve, reject})
})
} else if(name == 'setTimeout') {
const timeout_cb = args[0]
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, timeout_cb)
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, {resolve: timeout_cb})
return call.value
} else {
return call.value

View File

@@ -19,9 +19,8 @@ const gen_to_promise = gen_fn => {
if(result.done){
return result.value
} else {
// If promise
if(result.value?.then != null) {
return result.value.__original_then(
if(result.value?.[Symbol.toStringTag] == 'Promise') {
return result.value.then(
value => next(gen.next(value)),
error => next(gen.throw(error)),
)
@@ -36,7 +35,7 @@ const gen_to_promise = gen_fn => {
const make_promise_with_rejector = cxt => {
let rejector
const p = new cxt.window.Promise(r => rejector = r)
const p = new Promise(r => rejector = r)
return [p, rejector]
}
@@ -107,8 +106,8 @@ const do_run = function*(module_fns, cxt, io_trace){
create_array,
create_object,
)
if(result instanceof cxt.window.Promise) {
yield cxt.window.Promise.race([replay_aborted_promise, result])
if(result?.[Symbol.toStringTag] == 'Promise') {
yield Promise.race([replay_aborted_promise, result])
} else {
yield result
}
@@ -144,7 +143,10 @@ export const run = gen_to_promise(function*(module_fns, cxt, io_trace) {
if(!cxt.window.__is_initialized) {
defineMultiversion(cxt.window)
apply_io_patches(cxt.window)
inject_leporello_api(cxt)
cxt.window.__is_initialized = true
} else {
throw new Error('illegal state')
}
const result = yield* do_run(module_fns, cxt, io_trace)
@@ -154,14 +156,21 @@ export const run = gen_to_promise(function*(module_fns, cxt, io_trace) {
result.rt_cxt.is_recording_deferred_calls = false
// run again without io trace
// TODO reload app_window before second run
return yield* do_run(module_fns, cxt, null)
} else {
return result
}
})
const inject_leporello_api = cxt => {
cxt.window.leporello = { storage: cxt.storage }
}
const apply_promise_patch = cxt => {
if(cxt.window.Promise.prototype.__original_then != null) {
throw new Error('illegal state')
}
const original_then = cxt.window.Promise.prototype.then
cxt.window.Promise.prototype.__original_then = cxt.window.Promise.prototype.then
@@ -197,6 +206,7 @@ const apply_promise_patch = cxt => {
const remove_promise_patch = cxt => {
cxt.window.Promise.prototype.then = cxt.window.Promise.prototype.__original_then
delete cxt.window.Promise.prototype.__original_then
}
export const set_record_call = cxt => {
@@ -339,7 +349,7 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
try {
value = fn(...args)
ok = true
if(value instanceof cxt.window.Promise) {
if(value?.[Symbol.toStringTag] == 'Promise') {
set_record_call(cxt)
}
return value
@@ -485,7 +495,7 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
value = undefined
}
ok = true
if(value instanceof cxt.window.Promise) {
if(value?.[Symbol.toStringTag] == 'Promise') {
set_record_call(cxt)
}

View File

@@ -10,6 +10,9 @@ export const set_push = (x,y) => new Set([...x, y])
export const set_union = (x,y) => new Set([...x, ...y])
export const set_is_eq = (a, b) =>
a.size === b.size && [...a].every(value => b.has(value))
export const set_diff = (x,y) => {
return new Set([...x].filter(el => !y.has(el)))
}

View File

@@ -11,8 +11,7 @@ const isError = object =>
||
object instanceof globalThis.app_window.Error
const isPromise = object =>
object instanceof globalThis.app_window.Promise
const isPromise = object => object?.[Symbol.toStringTag] == 'Promise'
// Override behaviour for Date, becase Date has toJSON defined
const isDate = object =>