Files
leporello-js/src/record_io.js

249 lines
7.1 KiB
JavaScript
Raw Normal View History

2023-02-06 01:53:34 +08:00
import {set_record_call} from './runtime.js'
2023-02-07 20:09:43 +08:00
2023-05-13 11:13:29 +03:00
// Modify window object on module load
// TODO check - if modules reloaded on window reopen
// TODO - cxt.window
apply_io_patches()
// Current context for current execution of code
let cxt
export const set_current_context = _cxt => {
cxt = _cxt
}
const io_patch = (path, use_context = false) => {
2023-02-07 20:09:43 +08:00
let obj = cxt.window
for(let i = 0; i < path.length - 1; i++) {
obj = obj[path[i]]
}
const method = path.at(-1)
2023-02-06 01:53:34 +08:00
if(obj == null || obj[method] == null) {
// Method is absent in current env, skip patching
return
}
2023-02-07 20:09:43 +08:00
const name = path.join('.')
2023-02-06 01:53:34 +08:00
const original = obj[method]
obj[method] = function(...args) {
2023-02-14 18:03:10 +08:00
// TODO if called from prev execution, then throw to finish it
// ASAP
2023-02-06 01:53:34 +08:00
if(cxt.io_cache_is_replay_aborted) {
// Try to finish fast
2023-02-07 19:36:47 +08:00
throw new Error('io replay aborted')
2023-02-07 23:07:37 +08:00
}
2023-02-14 18:03:10 +08:00
const has_new_target = new.target != null
if(cxt.is_recording_deferred_calls) {
return has_new_target
? new original(...args)
: original.apply(this, args)
}
2023-02-07 23:07:37 +08:00
if(cxt.io_cache_is_recording) {
2023-02-06 01:53:34 +08:00
let ok, value, error
try {
2023-02-14 18:03:10 +08:00
// save call, so on expand_call and find_call IO functions would not be
// called.
// TODO: we have a problem when IO function is called from third-party
// lib and async context is lost
2023-02-06 01:53:34 +08:00
set_record_call(cxt)
const index = cxt.io_cache.length
if(name == 'setTimeout') {
args = args.slice()
// Patch callback
const cb = args[0]
2023-02-14 18:03:10 +08:00
args[0] = Object.defineProperty(function() {
// TODO if called from prev execution, then throw to
// finish it ASAP
if(cxt.io_cache_is_replay_aborted) {
// Non necessary
return
}
2023-02-06 01:53:34 +08:00
cxt.io_cache.push({type: 'resolution', index})
cb()
2023-02-14 18:03:10 +08:00
}, 'name', {value: cb.name})
2023-02-06 01:53:34 +08:00
}
value = has_new_target
? new original(...args)
: original.apply(this, args)
2023-02-07 21:36:36 +08:00
if(value instanceof cxt.window.Promise) {
2023-02-08 05:32:32 +08:00
// TODO use cxt.promise_then, not finally which calls
// patched 'then'?
value = value.finally(() => {
2023-02-14 18:03:10 +08:00
// TODO if called from prev execution, then throw to
// finish it ASAP
if(cxt.io_cache_is_replay_aborted) {
// Non necessary
return
}
2023-02-06 01:53:34 +08:00
cxt.io_cache.push({type: 'resolution', index})
})
}
ok = true
return value
} catch(e) {
error = e
ok = false
throw e
} finally {
cxt.io_cache.push({
type: 'call',
name,
ok,
value,
error,
args,
// To discern calls with and without 'new' keyword, primary for
// Date that can be called with and without new
has_new_target,
use_context,
context: use_context ? this : undefined,
})
}
} else {
const call = cxt.io_cache[cxt.io_cache_index]
2023-02-07 21:36:36 +08:00
2023-02-14 18:03:10 +08:00
// TODO if call == null or call.type == 'resolution', then do not discard
// cache, instead switch to record mode and append new calls to the
// cache?
2023-02-06 01:53:34 +08:00
if(
call == null
|| call.type != 'call'
2023-02-14 18:03:10 +08:00
|| call.has_new_target != has_new_target
2023-02-06 01:53:34 +08:00
|| call.use_context && (call.context != this)
|| call.name != name
|| (
2023-02-14 18:03:10 +08:00
(name == 'setTimeout' && (args[1] != call.args[1])) /* compares timeout*/
||
(
name != 'setTimeout'
&&
JSON.stringify(call.args) != JSON.stringify(args)
)
2023-02-06 01:53:34 +08:00
)
){
cxt.io_cache_is_replay_aborted = true
// Try to finish fast
throw new Error('io replay aborted')
} else {
2023-02-14 18:03:10 +08:00
2023-02-06 01:53:34 +08:00
const next_resolution = cxt.io_cache.find((e, i) =>
e.type == 'resolution' && i > cxt.io_cache_index
)
if(next_resolution != null && !cxt.io_cache_resolver_is_set) {
2023-02-07 20:09:43 +08:00
const original_setTimeout = cxt.window.setTimeout.__original
2023-02-06 01:53:34 +08:00
cxt.io_cache_resolver_is_set = true
original_setTimeout(() => {
2023-02-14 18:03:10 +08:00
// TODO if called from prev execution, then throw to finish it ASAP
2023-02-07 23:07:37 +08:00
if(cxt.io_cache_is_replay_aborted) {
return
}
2023-02-06 01:53:34 +08:00
cxt.io_cache_resolver_is_set = false
2023-02-07 23:07:37 +08:00
// Sanity check
2023-02-06 01:53:34 +08:00
if(cxt.io_cache_index >= cxt.io_cache.length) {
throw new Error('illegal state')
2023-02-07 23:07:37 +08:00
}
const next_event = cxt.io_cache[cxt.io_cache_index]
if(next_event.type == 'call') {
cxt.io_cache_is_replay_aborted = true
2023-02-06 01:53:34 +08:00
} else {
2023-02-07 23:07:37 +08:00
while(
cxt.io_cache_index < cxt.io_cache.length
&&
cxt.io_cache[cxt.io_cache_index].type == 'resolution'
) {
const resolution = cxt.io_cache[cxt.io_cache_index]
const resolver = cxt.io_cache_resolvers.get(resolution.index)
cxt.io_cache_index++
if(cxt.io_cache[resolution.index].name == 'setTimeout') {
resolver()
} else {
resolver(cxt.io_cache[resolution.index].value)
2023-02-06 01:53:34 +08:00
}
}
}
}, 0)
}
cxt.io_cache_index++
if(call.ok) {
2023-02-07 21:36:36 +08:00
if(call.value instanceof cxt.window.Promise) {
2023-02-08 05:32:32 +08:00
// Always make promise originate from run_window
return new cxt.window.Promise(resolve => {
2023-02-06 01:53:34 +08:00
cxt.io_cache_resolvers.set(cxt.io_cache_index - 1, resolve)
})
} else if(name == 'setTimeout') {
const timeout_cb = args[0]
cxt.io_cache_resolvers.set(cxt.io_cache_index - 1, timeout_cb)
return call.value
} else {
return call.value
}
} else {
throw call.error
}
}
}
}
Object.defineProperty(obj[method], 'name', {value: original.name})
obj[method].__original = original
}
2023-02-14 18:03:10 +08:00
// TODO bare IO functions should not be exposed at all, to allow calling it
// only from patched versions. Especially setInterval which can cause leaks
2023-05-13 11:13:29 +03:00
export const apply_io_patches = () => {
io_patch(['Math', 'random'])
2023-02-06 01:53:34 +08:00
2023-05-13 11:13:29 +03:00
io_patch(['setTimeout'])
2023-02-14 18:03:10 +08:00
// TODO if call setTimeout and then clearTimeout, cache it and remove call of
// clearTimeout, and make only setTimeout, then it would never be called when
// replaying from cache
2023-05-13 11:13:29 +03:00
io_patch(['clearTimeout'])
2023-02-06 01:53:34 +08:00
2023-02-14 18:03:10 +08:00
// TODO patch setInterval to only cleanup all intervals on finish
2023-02-07 20:09:43 +08:00
const Date = cxt.window.Date
2023-05-13 11:13:29 +03:00
io_patch(['Date'])
2023-02-07 20:09:43 +08:00
cxt.window.Date.parse = Date.parse
cxt.window.Date.now = Date.now
cxt.window.Date.UTC = Date.UTC
2023-05-13 11:13:29 +03:00
io_patch(['Date', 'now'])
2023-02-06 01:53:34 +08:00
2023-05-13 11:13:29 +03:00
io_patch(['fetch'])
2023-02-06 01:53:34 +08:00
// Check if Response is defined, for node.js
2023-02-07 20:09:43 +08:00
if(cxt.window.Response != null) {
2023-05-13 11:13:29 +03:00
const Response_methods = [
'arrayBuffer',
'blob',
'formData',
'json',
'text',
]
2023-02-06 01:53:34 +08:00
for(let key of Response_methods) {
2023-05-13 11:13:29 +03:00
io_patch(['Response', 'prototype', key], true)
2023-02-06 01:53:34 +08:00
}
}
}