Skip to content
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

feat(jupyter): send Jupyter messaging metadata with Deno.jupyter.broadcast #20714

Merged
merged 4 commits into from
Sep 29, 2023

Conversation

manzt
Copy link
Contributor

@manzt manzt commented Sep 27, 2023

Will rebase following merge of #20710 in to main.

Exposes metadata to the Deno.jupyter.broadcast API.

await Deno.jupyter.broadcast(msgType, content, metadata);

The metadata is required for "comm_open" for with jupyter.widget target. With this PR I can successfully communicate with the anywidget frontend, sending frontend ESM via comms with updates :)

Example

Make sure you have anywidget installed:

pip install jupyterlab anywidget
import { display } from "https://deno.land/x/display/mod.ts";

let comm_id = crypto.randomUUID();

await Deno.jupyter.broadcast("comm_open", {
    "comm_id": comm_id,
    "target_name": "jupyter.widget",
    "data": { 
        "state": {
            "_model_module": "anywidget",
            "_model_name": "AnyModel",
            "_model_module_version": "0.6.5",
            "_view_module": "anywidget",
            "_view_name": "AnyView",
            "_view_module_version": "0.6.5",
            "_view_count": null,
        }
    }
}, {
    "version": "2.1.0"
});

// Create an anywidget module (export a "render" function) for the front end
// The `model` is synchronized with `update` messages sent from the comm_msg
let esm = `export ${async function render({ model, el }) {
  let { default: confetti } = await import ("https://esm.sh/canvas-confetti@1.6.0");
  model.on("change:value", () => confetti({ angle: model.get("value") }));
  confetti();
}.toString()}`;

// Send initial widget state
await Deno.jupyter.broadcast("comm_msg", {
    "comm_id": comm_id,
    "data": { "method": "update", "state": { "_esm": esm, "_anywidget_id": comm_id } }
});

display({
    "application/vnd.jupyter.widget-view+json": {
        "version_major": 2,
        "version_minor": 1,
        "model_id": comm_id,
    }
})
await Deno.jupyter.broadcast("comm_msg", {
    "comm_id": comm_id,
    "data": { "method": "update", "state": { "value": 50 } },
});

await new Promise(resolve => setTimeout(resolve, 1000));

await Deno.jupyter.broadcast("comm_msg", {
    "comm_id": comm_id,
    "data": { "method": "update", "state": { "value": 100 } },
});

await new Promise(resolve => setTimeout(resolve, 1000));

This is sooo cool!!

Screen.Recording.2023-09-27.at.2.44.30.PM.mov

@CLAassistant
Copy link

CLAassistant commented Sep 27, 2023

CLA assistant check
All committers have signed the CLA.

@manzt
Copy link
Contributor Author

manzt commented Sep 27, 2023

cc: @rgbkrk

@manzt
Copy link
Contributor Author

manzt commented Sep 27, 2023

I'm sure others are more familiar with the latest for how to serialize functions / modules to run clientside. Currently I have this hack:

// Create an anywidget module (export a "render" function) for the front end
// The `model` is synchronized with `update` messages sent from the comm_msg
let esm = `export ${async function render({ model, el }) {
  let { default: confetti } = await import ("https://esm.sh/canvas-confetti@1.6.0");
  model.on("change:value", () => confetti({ angle: model.get("value") }));
  confetti();
}.toString()}`;

But anywidget, just expects you to ship ESM that exports a function called render. The render function gives you access to a model which you can listen for and emit updates to state for objects that live in the kernel.

More details.

@manzt manzt changed the title feat(unstable): Add metadata to Deno.jupyter.broadcast feat(unstable): send Jupyter messaging metadata with Deno.jupyter.broadcast Sep 27, 2023
@manzt manzt changed the title feat(unstable): send Jupyter messaging metadata with Deno.jupyter.broadcast feat(jupyter): send Jupyter messaging metadata with Deno.jupyter.broadcast Sep 27, 2023
@manzt
Copy link
Contributor Author

manzt commented Sep 27, 2023

Works with updates to the frontend. Would be awesome to be able to listen for messages from the frontend as well (i.e. enabling 2-way data binding). Not sure that is implemented yet.

Screen.Recording.2023-09-27.at.4.58.53.PM.mov

} = core.ensureFastOps();

globalThis.Deno.jupyter = {
async broadcast(msgType, content, metadata = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for optional metadata

@rgbkrk
Copy link
Contributor

rgbkrk commented Sep 27, 2023

This is awesome, I'm glad you were able to make this fly so quickly. Thank you!

@rgbkrk
Copy link
Contributor

rgbkrk commented Sep 27, 2023

Would be awesome to be able to listen for messages from the frontend as well (i.e. enabling 2-way data binding). Not sure that is implemented yet.

That's #20592. It would be nice to have the Javascript API for it that we want to expose that is as close to one of the many web standard options (to be Deno-y).

You're making me realize we probably need to support the Jupyter message buffers as the final optional argument of this big broadcast message.

Roughly we're looking at an overall interface of:

type DenoJupyter = typeof Deno & {
  jupyter: {
    broadcast(
      msg_type: string,
      content: { [key: string]: object },
      metadata?: { [key: string]: object },
      buffers?: ArrayBuffer[]
    ): Promise<void>;
  };
};

@manzt
Copy link
Contributor Author

manzt commented Sep 28, 2023

It would be nice to have the Javascript API for it that we want to expose that is as close to one of the many web standard options (to be Deno-y).

Yes! I have some ideas.

You're making me realize we probably need to support the Jupyter message buffers as the final optional argument of this big broadcast message.

Indeed.

The other option would be to have a single object for other possible message data. Not sure if the other fields could be something we'd expose in the future.

type DenoJupyter = typeof Deno & {
  jupyter: {
    broadcast(
      msg_type: string,
      content: { [key: string]: object },
      extra?: {
        metadata?: Record<string, object>,
        buffers?: ArrayBuffer[]
      }
    ): Promise<void>;
  };
};

@bartlomieju
Copy link
Member

I just merged #20710, can you please rebase the PR @manzt?

@bartlomieju
Copy link
Member

The other option would be to have a single object for other possible message data. Not sure if the other fields could be something we'd expose in the future.

This seems like a reasonable solution to avoid balooning number of arguments this API accepts.

@rgbkrk
Copy link
Contributor

rgbkrk commented Sep 28, 2023

Not sure if the other fields could be something we'd expose in the future.

parent_header is controlled by the Deno kernel, header we also shouldn't muck with.

Famous last words and all, the jupyter spec will not extend the body of the message beyond what is there with parent_header, header, content, metadata, and buffers.

I'm totally cool with extra though. 😄

@manzt manzt force-pushed the manzt/jupyter-metadata branch from 88fea55 to 6004cb2 Compare September 28, 2023 18:01
@manzt manzt force-pushed the manzt/jupyter-metadata branch from 6004cb2 to 675603a Compare September 28, 2023 18:07
@manzt
Copy link
Contributor Author

manzt commented Sep 28, 2023

I just merged #20710, can you please rebase the PR @manzt?

Done, @bartlomieju! (TIL git cherry-pick 😅)

I'm totally cool with extra though. 😄

Okay, how about we introduce extra in this PR, but just with optional metadata? Sound OK?

It's nice to have the explicit names (as well as the option to extend extra if necessary in the future for whatever reason).

Deno.jupyter.broadcast(
  "comm_open",
  {
    "comm_id": "blah",
    "data": {
      "method": "update",
      "state": { "value": null },
      "buffer_paths": [["value"]]
    }
  },
  {
    metadata: { "version": "2.1.0" },
    buffers: [new Uint8Array([1, 2, 3, 4, 5]).buffer]
  }
);

I doubt the API would explode with the positional arguments, but this way the ordering of arguments isn't important. Since metadata or buffers could be sent independently of each other, it's also nice to not need to pass explicit undefined to the API:

Deno.jupyter.broadcast(msgType, content, undefined, buffers);

vs

Deno.jupyter.broadcast(msgType, content, { buffers });

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, could you please update cli/tsc/dts/lib.deno.unstable.d.ts to match the new JS API?

@manzt
Copy link
Contributor Author

manzt commented Sep 28, 2023

done!

@manzt
Copy link
Contributor Author

manzt commented Sep 29, 2023

Implemented a mod.ts in anywidget @rgbkrk . (Think I might have messed up the denoland webhooks, since it's a monorepo and I'm not sure how to specify release for the packages/anywidget/mod.ts path).

import { widget } from "https://raw.githubusercontent.com/manzt/anywidget/main/packages/anywidget/mod.ts";

let model = await widget({
    // shared model between frontend and backend
    state: { value: 0 },
    // front-end render function
    render: ({ model, el }) => {
        let h1 = Object.assign(document.createElement("h1"), {
            innerText: "Value is " + model.get("value"),
        });
        model.on("change:value", () => {
            h1.innerText = "Value is " + model.get("value");
        });
        el.appendChild(h1);
    }
});

model
for (let i = 1; i <= 10; i++) {
    model.set("value", i);
    await new Promise(resolve => setTimeout(resolve, 500));
}

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you @manzt, great improvement and I can't wait to see what you'll be able to do with it!

@bartlomieju bartlomieju merged commit 7bcf121 into denoland:main Sep 29, 2023
@manzt manzt deleted the manzt/jupyter-metadata branch September 29, 2023 23:02
@rgbkrk
Copy link
Contributor

rgbkrk commented Oct 1, 2023

Wrote a way to do imports with the anywidget in manzt/anywidget#311

bartlomieju pushed a commit that referenced this pull request Oct 12, 2023
…adcast` (#20714)

Exposes
[`metadata`](https://jupyter-client.readthedocs.io/en/latest/messaging.html#metadata)
to the `Deno.jupyter.broadcast` API.

```js
await Deno.jupyter.broadcast(msgType, content, metadata);
```

The metadata is required for
[`"comm_open"`](https://github.com/jupyter-widgets/ipywidgets/blob/main/packages/schema/messages.md#instantiating-a-widget-object-1)
for with `jupyter.widget` target.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants