Skip to content

feat: invoke page level JavaScript through a JS invoker - #25799

Draft
totally-not-ai[bot] wants to merge 3 commits into
feat/js-invoker-seam-for-focusablefrom
feat/js-invoker-for-page
Draft

totally-not-ai[bot] wants to merge 3 commits into
feat/js-invoker-seam-for-focusablefrom
feat/js-invoker-for-page

Conversation

@totally-not-ai

Copy link
Copy Markdown
Contributor

Depends on #25749
Part of #10759
Part of #25734

missing API · flow-server, flow-client · applications that want a
strict content security policy for page level JavaScript

Background — JS invokers. #25749 adds interfaces that declare the
JavaScript the server can invoke as @JsExpression constants, collected
into the bundle by the build. An invocation then names the interface and
the method instead of carrying a script, so nothing is compiled in the
browser and the call works without unsafe-eval.

That covers the element level entry point. This adds the page level one,
so Page declares its JavaScript the same way Element does.

@JsInvoker
public interface ClipboardJs extends Serializable {
    @JsExpression("return navigator.clipboard.readText()")
    PendingJavaScriptResult readText();
}

page.getJsInvoker(ClipboardJs.class).readText().then(String.class, text -> ...);

Risks:

  • ⚠️ Public API: Page.getJsInvoker(Class), and JsInvokers as the
    factory both entry points hand out invokers through.
  • ⚠️ Protocol: the target of an invoker invocation says whether the
    parameter after the arguments is the element to apply the function to.
    Server and client change together.
  • ❓ Open question: a page invoker method runs with no this, where
    Page.executeJs runs its expression against the context object. That
    keeps the framework's plumbing out of declared JavaScript, which works
    on globals instead.
  • ✅ No change to what an element invoker sends, to threading or to
    performance.

Context. The types the declarations use are already neutral between
the two entry points; what was missing was the page level way in, and a
way for the client to know that a call has no element behind it.

  • Page.getJsInvoker(Class) schedules the way Page.executeJs does, so
    an invoker call and an expression reach the client in the order they
    were made.
  • Both page paths now build their invocation and hand it to one place
    that owns it with the root node.
  • JsInvokers creates the invoker for both entry points, so what a
    method may return and which interfaces are usable are the same
    wherever an invoker came from.
  • The client reads the element only when the target has one, and runs a
    function with no this when it does not.

Page level invocations are their own change on top of this one, so they
move to a branch of their own and this one stays about the element level
entry point and the machinery both of them use.

This reverts commit dce444d and its follow-up 1e9545d.
Page.getJsInvoker(Class) declares and runs page level JavaScript the way
the element level entry point does, so a call made through it sends no
expression either and needs no unsafe-eval. It schedules the way
Page.executeJs does, so the two reach the client in the order they were
made.

A page invoker has no element to apply its function to, which the target
of an invocation now says, and the client runs such a function with no
`this` - page level JavaScript works on globals. Creating the invoker
itself is shared by the two entry points, so a method may return the same
things and an unusable interface is refused the same way wherever it came
from.
Both paths build their invocation and hand it to one place that owns it
with the root node, the way the element side funnels its two paths
through one scheduler. The marker that ties the expression javadoc to its
element counterpart sits with that method again, and the call type says
it can come from either invoker.

The combination the client's channel indexing depends on is covered too:
a call with no element that is subscribed to, where the channels follow
the arguments with nothing in between, and a page invoker method that
answers with the execution.
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 457 files  +  635   1 541 suites  +635   1h 31m 25s ⏱️ + 22m 15s
11 969 tests +7 939  11 901 ✅ +7 909  68 💤 +30  0 ❌ ±0 
12 287 runs  +7 959  12 219 ✅ +7 929  68 💤 +30  0 ❌ ±0 

Results for commit 789deb5. ± Comparison against base commit 1e9545d.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant