-
-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Losing scope in async global effects #43
Comments
(getting dejavus with implementing jquery-spect, exactly the thing.) Seems that we're limited in hooks to repeat react. There's no known for me way for out-of-scope function to detect the scope (in general case) it was called from, so the effects are quite limited in use-cases, quite like react. Switching state manually isn't useful for async calls either. Seems that we're off with jquery-spect only, providing aspect builder plugin, and pure no-jquery version. |
jQuery code is confusing and noisy, although promising for jquery land: https://gist.github.com/dy/a556d817042c3d492455e93831fd3572. Within all options, scope detection is the fastest and the safest. |
Ugh. That doesn't seem to be possible via stacktrace. let log = [],
el = document.createElement('div'),
el2 = document.createElement('div')
// invocation site - available from stack
$(el, aspect)
// the definition site (start of aspect) - unavailable from stack
function aspect(el) {
log.push(state().x)
$(el2, el2 => {});
(
() => {
$(el2, el2 => {});
setTimeout(() => {
$(el2, el2 => {});
// the timeout site (topmost detectable)
(() => state({ x: 1 }))()
})
}
)()
} Neither creating new function helps detecting aspect - we can get only site of function evaluator, which says nothing about exact aspect. The new functions know nothing about imported deps, they need providing effects as arguments anyways. |
Ok. Even if we try to create scoped function via scoped-function, we lose function surroundings, like imported modules. To use scoped functions, we have to indicate scope. So seems that's proven - there's no way to provide context-less global effects directly, not changing their API ( |
AfterthoughtWe could use state from function app(el) {
timeout(() => {
state()
})
} In this case There's limitation for fx though: function app(el) {
fx(async fn() {
await smth()
state()
})
} The async tick is guaranteed, and for Why doubts in jQuery approach?
We could, actually, attach effects explicitly via deps, so that we wouldn't have to register implicit observables via reading some external state. function app(el) {
fx(async (state, attr, id) => {
await tick()
// state is attached to current element context
state({ el: value})
}, state, attr, id)
} That looks weird though - passing context along with attribs. On the other side
|
Although - state seems to be the only bottleneck. let state = useState(defaults)
//...
setTimeout(()=>{
state.x = 1
}) |
Getting back to this lovely approach. The only issue remains then - how to apply context. use.call(target, el => {
state.id = 1
fx(() => {
attr(a => a.loading = true)(el)
attr.loading = true
state.user = await ky.get`./api/user/${ state('id') }`
attr.loading = false
}, [id])
html`<p use=${i18n}>${
attr.loading ? `Hello, ${ state.user.name }!` : `Thanks for patience...`
}</p>`
use(() => {
// like that probably?
// but how do we switch context back? eg. read initial state?
// mb contexts are not for that? eg. use(anotherAspect)
// or manual indeed? ctxStart(), ctxEnd()?
}, externalEl)
css`...`
}) Although even let elCtx = el.getContext('spect')
// or
let $el = context(el)
// or
let $el = spect(el) So we just build a platform of contexts. |
Couple more reasons against jquery-refs. Yes, we add chaining, but.
html(target, h`${ data.map(tpl) }`)
const tpl = data => h`...`
html(target, h`${ data.map(item => h`<${Comp} ...${item}/>`) }`)
const comp = el => html(el)`<${}/>`
|
Ok, that's a separate ticket. |
Consider aspect
In such case,
state
is called in separate tick, therefore losescurrentTarget
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.
setTimeout
callback is something external, that's naturally out of reach.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: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
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.
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 ifsetTimeout
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.The text was updated successfully, but these errors were encountered: