Consider aspect
import $, {state} from 'spect'
$(el, el => {
setTimeout(() => state({x: 1}))
})
In such case, state
is called in separate tick, therefore loses currentTarget
and is hard to identify the aspect to rerender.
Similar situation happens with async functions, where fn continues in a separate tick, dropping the callstack. That is fixable in latest node https://thecodebarbarian.com/async-stack-traces-in-node-js-12, but impossible anywhere else.
Approaches:
Parsing callstack
Possible approach is storing aspect bodies by callsite, and figuring out "out of tick" effect calls based on if their initial callsite is contained within one of tracked aspects.
That is purely visual approach to code. It is slow-ish, non-standard-ish (stacktrace is not regulated feature) and with medium weight logic. Besides, bound or native or alike aspects, which are not serializable aspect.toString()
, instantly lose source (toString gives [native code]
). We can hoist up, looking for nearest triggered aspect entry, presuming that is the source of async call. But that breaks trivially when we delegate aspect handling to some external function, that can reside in a separate module.
So that can work with some limitations, similar to react hooks.
- The effect should be called visually from the same scope as the aspect. Effects residing literally anywhere outside of effect scope aren't going to work (until browsers learn --async-stacktrace).
- But - that wouldn't even work with visible arguments. If
setTimeout
callback is something external, that's naturally out of reach.
- Effects cannot be incorporated into wrappers. (Wrapping effects must be registered).
- As far as wrapper is called from aspect scope, it is detectable.
- Aspect functions cannot be external/native. They must be unbound, described in-place, with valid
toSource
.
Out of scope effects can be figured out and error thrown.
Static transform
That is likely must-do, turning generic effects into particular ones, mb wrapping them with bind
.
Webworker sandboxing
Another form is creating a webworker per aspect, that would also solve the #38. Tool like https://github.com/GoogleChromeLabs/clooney/ can be to the place.
That creates a web-worker aspect, but the created sandbox loses access to surrounding environment. Besides, creating a sandbox per-aspect scope is quite heavy.
That's better left for a separate effect, eg. work
, on the stage of web-components:
el => {
let result = work(() => {
})
}
See #38 for progress on that.
Possibly like VM, that could reproduce global context, although would require same imports as the main one... Seems like breaking fx rules isn't good idea.
Runtime sandboxing
There seems to be no other way but create a runtime sandbox, wrapping initial aspect source with bound effects
$(target, el => {
html``
})
// converted to
$(target, ((html, fx, ...) => { return (el => {
}) })(html.bind(target), fx.bind(target)) )
That's going to be the fastest and safest within others, considering that the code is just wrapped, no user input expected (life can be ironic though).
Problem with this approach is that effects referred by the aspect can have any aliased name within the current module.
So this can be used with limitations.
- Imported effect names. Similar to limitation on react hook names.
- The error can be displayed to avoid aliasing effect names.
- Fn must have
toSource
, there's no way to wrap that keeping clean source.
VM sandboxing
Running aspect code in a separate context. In browser that's done via iframe.
It's not necessary to replace globals. We have to mock deps per-aspect.
Sounds horrendous of course, although mb possible.
Throwing/catching error to obtain async stack
The error thrown in setTimeout
is thrown in another tick. It may have no access to the original scope. Even without global effects. Therefore the effects should be created by scope in this or another way, but if setTimeout
intends to run it outside of the scope, there's no way to do it even in react. Same is for external functions - if an effect is run outside of scope, it naturally has no lexical access to the source scope. That isn't even possible with jquery-like refs. To have access to scope the aspect arguments must be lexically visible.