From 620d380598467cae798c2795c4a7c52d1c3c8243 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Fri, 29 Sep 2023 13:36:37 -0400 Subject: [PATCH] feat(deno): add experimenal deno mod (#309) --- packages/anywidget/mod.ts | 123 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 packages/anywidget/mod.ts diff --git a/packages/anywidget/mod.ts b/packages/anywidget/mod.ts new file mode 100644 index 00000000..d74e48c8 --- /dev/null +++ b/packages/anywidget/mod.ts @@ -0,0 +1,123 @@ +import mitt, { type Emitter } from "npm:mitt@3"; + +class Comm { + id: string; + #protocol_version_major: number; + #protocol_version_minor: number; + #anywidget_version: string; + + constructor() { + this.id = crypto.randomUUID(); + this.#protocol_version_major = 2; + this.#protocol_version_minor = 1; + this.#anywidget_version = "0.6.5"; + } + + async init() { + await Deno.jupyter.broadcast( + "comm_open", + { + "comm_id": this.id, + "target_name": "jupyter.widget", + "data": { + "state": { + "_model_module": "anywidget", + "_model_name": "AnyModel", + "_model_module_version": this.#anywidget_version, + "_view_module": "anywidget", + "_view_name": "AnyView", + "_view_module_version": this.#anywidget_version, + "_view_count": null, + }, + }, + }, + { + "metadata": { + "version": + `${this.#protocol_version_major}.${this.#protocol_version_minor}.0`, + }, + }, + ); + } + + async send_state(state: object) { + await Deno.jupyter.broadcast("comm_msg", { + "comm_id": this.id, + "data": { "method": "update", "state": state }, + }); + } + + mimebundle() { + return { + "application/vnd.jupyter.widget-view+json": { + "version_major": this.#protocol_version_major, + "version_minor": this.#protocol_version_minor, + "model_id": this.id, + }, + }; + } +} + +type ChangeEvents = { + [K in (string & keyof State) as `change:${K}`]: State[K]; +}; + +class Model { + _state: State; + _emitter: Emitter>; + + constructor(state: State) { + this._state = state; + this._emitter = mitt>(); + } + get(key: K): State[K] { + return this._state[key]; + } + set(key: K, value: State[K]): void { + // @ts-expect-error can't convince TS that K is a key of State + this._emitter.emit(`change:${key}`, value); + this._state[key] = value; + } + on>( + name: Event, + callback: (data: ChangeEvents[Event]) => void, + ): void { + this._emitter.on(name, callback); + } +} + +type FrontEndModel = Model & { + save_changes(): void; +}; + +export async function widget({ state, render }: { + state: State; + render: ( + context: { + model: FrontEndModel; + el: HTMLElement; + }, + ) => unknown; +}) { + let model = new Model(state); + let comm = new Comm(); + await comm.init(); + // TODO: more robust serialization of render function (with context?) + await comm.send_state({ + ...state, + _esm: "export const render = " + render.toString(), + }); + for (let key in state) { + model.on(`change:${key}`, (data) => { + comm.send_state({ [key]: data }); + }); + } + return new Proxy(model, { + get(target, prop, receiver) { + if (prop === Symbol.for("Jupyter.display")) { + return comm.mimebundle.bind(comm); + } + return Reflect.get(target, prop, receiver); + }, + }); +}