Skip to content

Commit 6cbd408

Browse files
committed
refactor(anywidget): Clean up HMR internals
1 parent 3bb23e3 commit 6cbd408

File tree

1 file changed

+115
-94
lines changed

1 file changed

+115
-94
lines changed

packages/anywidget/src/widget.js

+115-94
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as uuid from "@lukeed/uuid";
22
import * as solid from "solid-js";
33

44
/** @import * as base from "@jupyter-widgets/base" */
5+
/** @import { Initialize, Render, AnyModel } from "@anywidget/types" */
56

67
/**
78
* @template T
@@ -10,13 +11,13 @@ import * as solid from "solid-js";
1011

1112
/**
1213
* @typedef AnyWidget
13-
* @prop initialize {import("@anywidget/types").Initialize}
14-
* @prop render {import("@anywidget/types").Render}
14+
* @prop initialize {Initialize}
15+
* @prop render {Render}
1516
*/
1617

1718
/**
1819
* @typedef AnyWidgetModule
19-
* @prop render {import("@anywidget/types").Render=}
20+
* @prop render {Render=}
2021
* @prop default {AnyWidget | (() => AnyWidget | Promise<AnyWidget>)=}
2122
*/
2223

@@ -102,19 +103,16 @@ async function load_css(css, anywidget_id) {
102103

103104
/**
104105
* @param {string} esm
105-
* @returns {Promise<{ mod: AnyWidgetModule, url: string }>}
106+
* @returns {Promise<AnyWidgetModule>}
106107
*/
107108
async function load_esm(esm) {
108109
if (is_href(esm)) {
109-
return {
110-
mod: await import(/* webpackIgnore: true */ /* @vite-ignore */ esm),
111-
url: esm,
112-
};
110+
return await import(/* webpackIgnore: true */ /* @vite-ignore */ esm);
113111
}
114112
let url = URL.createObjectURL(new Blob([esm], { type: "text/javascript" }));
115113
let mod = await import(/* webpackIgnore: true */ /* @vite-ignore */ url);
116114
URL.revokeObjectURL(url);
117-
return { mod, url };
115+
return mod;
118116
}
119117

120118
/** @param {string} anywidget_id */
@@ -148,14 +146,13 @@ To learn more, please see: https://github.com/manzt/anywidget/pull/395.
148146
/**
149147
* @param {string} esm
150148
* @param {string} anywidget_id
151-
* @returns {Promise<AnyWidget & { url: string }>}
149+
* @returns {Promise<AnyWidget>}
152150
*/
153151
async function load_widget(esm, anywidget_id) {
154-
let { mod, url } = await load_esm(esm);
152+
let mod = await load_esm(esm);
155153
if (mod.render) {
156154
warn_render_deprecation(anywidget_id);
157155
return {
158-
url,
159156
async initialize() {},
160157
render: mod.render,
161158
};
@@ -166,7 +163,7 @@ async function load_widget(esm, anywidget_id) {
166163
);
167164
let widget =
168165
typeof mod.default === "function" ? await mod.default() : mod.default;
169-
return { url, ...widget };
166+
return widget;
170167
}
171168

172169
/**
@@ -219,28 +216,35 @@ async function safe_cleanup(fn, kind) {
219216

220217
/**
221218
* @template T
222-
* @typedef {{ data: T, state: "ok" } | { error: any, state: "error" }} Result
219+
* @typedef Ready
220+
* @property {"ready"} status
221+
* @property {T} data
223222
*/
224223

225-
/** @type {<T>(data: T) => Result<T>} */
226-
function ok(data) {
227-
return { data, state: "ok" };
228-
}
224+
/**
225+
* @typedef Pending
226+
* @property {"pending"} status
227+
*/
229228

230-
/** @type {(e: any) => Result<any>} */
231-
function error(e) {
232-
return { error: e, state: "error" };
233-
}
229+
/**
230+
* @typedef Errored
231+
* @property {"error"} status
232+
* @property {unknown} error
233+
*/
234+
235+
/**
236+
* @template T
237+
* @typedef {Pending | Ready<T> | Errored} Result
238+
*/
234239

235240
/**
236241
* Cleans up the stack trace at anywidget boundary.
237242
* You can fully inspect the entire stack trace in the console interactively,
238243
* but the initial error message is cleaned up to be more user-friendly.
239244
*
240245
* @param {unknown} source
241-
* @returns {never}
242246
*/
243-
function throw_anywidget_error(source) {
247+
function log_anywidget_error(source) {
244248
if (!(source instanceof Error)) {
245249
// Don't know what to do with this.
246250
throw source;
@@ -251,7 +255,6 @@ function throw_anywidget_error(source) {
251255
anywidget_index === -1 ? lines : lines.slice(0, anywidget_index + 1);
252256
source.stack = clean_stack.join("\n");
253257
console.error(source);
254-
throw source;
255258
}
256259

257260
/**
@@ -321,66 +324,86 @@ function promise_with_resolvers() {
321324
}
322325

323326
class Runtime {
324-
/** @type {import('solid-js').Resource<Result<AnyWidget & { url: string }>>} */
327+
/** @type {solid.Accessor<Result<AnyWidget>>} */
325328
// @ts-expect-error - Set synchronously in constructor.
326329
#widget_result;
327-
/** @type {Promise<void>} */
328-
ready;
329330
/** @type {AbortSignal} */
330331
#signal;
332+
/** @type {Promise<void>} */
333+
ready;
331334

332335
/**
333336
* @param {base.DOMWidgetModel} model
334337
* @param {{ signal: AbortSignal }} options
335338
*/
336339
constructor(model, options) {
337340
/** @type {PromiseWithResolvers<void>} */
338-
const resolvers = promise_with_resolvers();
341+
let resolvers = promise_with_resolvers();
339342
this.ready = resolvers.promise;
340343
this.#signal = options.signal;
341344
this.#signal.throwIfAborted();
342345
this.#signal.addEventListener("abort", () => dispose());
346+
347+
AbortSignal.timeout(2000).addEventListener("abort", () => {
348+
console.error("timed out");
349+
resolvers.reject(new Error("[anywidget] Failed to load"));
350+
});
343351
let dispose = solid.createRoot((dispose) => {
352+
let id = () => model.get("_anywidget_id");
344353
let [css, set_css] = solid.createSignal(model.get("_css"));
345354
model.on("change:_css", () => {
346-
let id = model.get("_anywidget_id");
347-
console.debug(`[anywidget] css hot updated: ${id}`);
355+
console.debug(`[anywidget] css hot updated: ${id()}`);
348356
set_css(model.get("_css"));
349357
});
350358
solid.createEffect(() => {
351-
let id = model.get("_anywidget_id");
352-
load_css(css(), id);
359+
css() && load_css(css(), id());
353360
});
354361

355-
/** @type {import("solid-js").Signal<string>} */
362+
/** @type {solid.Signal<string>} */
356363
let [esm, setEsm] = solid.createSignal(model.get("_esm"));
357364
model.on("change:_esm", async () => {
358-
let id = model.get("_anywidget_id");
359-
console.debug(`[anywidget] esm hot updated: ${id}`);
365+
console.debug(`[anywidget] esm hot updated: ${id()}`);
360366
setEsm(model.get("_esm"));
361367
});
362-
/** @type {void | (() => Awaitable<void>)} */
363-
let cleanup;
364-
this.#widget_result = solid.createResource(esm, async (update) => {
365-
await safe_cleanup(cleanup, "initialize");
366-
try {
367-
model.off(null, null, INITIALIZE_MARKER);
368-
let widget = await load_widget(update, model.get("_anywidget_id"));
369-
resolvers.resolve();
370-
cleanup = await widget.initialize?.({
371-
model: model_proxy(model, INITIALIZE_MARKER),
372-
experimental: {
373-
// @ts-expect-error - bind isn't working
374-
invoke: invoke.bind(null, model),
375-
},
376-
});
377-
return ok(widget);
378-
} catch (e) {
379-
return error(e);
380-
}
381-
})[0];
368+
369+
let [widget_result, set_widget_result] = solid.createSignal(
370+
/** @type {Result<AnyWidget>} */ ({
371+
status: "pending",
372+
}),
373+
);
374+
375+
this.#widget_result = widget_result;
376+
377+
solid.createEffect(() => {
378+
let controller = new AbortController();
379+
solid.onCleanup(() => controller.abort());
380+
model.off(null, null, INITIALIZE_MARKER);
381+
load_widget(esm(), model.get("_anywidget_id"))
382+
.then(async (widget) => {
383+
if (controller.signal.aborted) {
384+
return;
385+
}
386+
let cleanup = await widget.initialize?.({
387+
model: model_proxy(model, INITIALIZE_MARKER),
388+
experimental: {
389+
// @ts-expect-error - bind isn't working
390+
invoke: invoke.bind(null, model),
391+
},
392+
});
393+
if (controller.signal.aborted) {
394+
safe_cleanup(cleanup, "esm update");
395+
return;
396+
}
397+
controller.signal.addEventListener("abort", () =>
398+
safe_cleanup(cleanup, "esm update"),
399+
);
400+
set_widget_result({ status: "ready", data: widget });
401+
resolvers.resolve();
402+
})
403+
.catch((error) => set_widget_result({ status: "error", error }));
404+
});
405+
382406
return () => {
383-
cleanup?.();
384407
model.off("change:_css");
385408
model.off("change:_esm");
386409
dispose();
@@ -399,42 +422,41 @@ class Runtime {
399422
signal.throwIfAborted();
400423
signal.addEventListener("abort", () => dispose());
401424
let dispose = solid.createRoot((dispose) => {
402-
/** @type {void | (() => Awaitable<void>)} */
403-
let cleanup;
404-
let resource = solid.createResource(
405-
this.#widget_result,
406-
async (widget_result) => {
407-
cleanup?.();
408-
// Clear all previous event listeners from this hook.
409-
model.off(null, null, view);
410-
view.$el.empty();
411-
if (widget_result.state === "error") {
412-
throw_anywidget_error(widget_result.error);
413-
}
414-
let widget = widget_result.data;
415-
try {
416-
cleanup = await widget.render?.({
425+
solid.createEffect(() => {
426+
// Clear all previous event listeners from this hook.
427+
model.off(null, null, view);
428+
view.$el.empty();
429+
let result = this.#widget_result();
430+
if (result.status === "pending") {
431+
return;
432+
}
433+
if (result.status === "error") {
434+
log_anywidget_error(result.error);
435+
return;
436+
}
437+
let controller = new AbortController();
438+
solid.onCleanup(() => controller.abort());
439+
Promise.resolve()
440+
.then(async () => {
441+
let cleanup = await result.data.render?.({
417442
model: model_proxy(model, view),
418443
el: view.el,
419444
experimental: {
420445
// @ts-expect-error - bind isn't working
421446
invoke: invoke.bind(null, model),
422447
},
423448
});
424-
} catch (e) {
425-
throw_anywidget_error(e);
426-
}
427-
},
428-
)[0];
429-
solid.createEffect(() => {
430-
if (resource.error) {
431-
// TODO: Show error in the view?
432-
}
449+
if (controller.signal.aborted) {
450+
safe_cleanup(cleanup, "dispose view - already aborted");
451+
return;
452+
}
453+
controller.signal.addEventListener("abort", () =>
454+
safe_cleanup(cleanup, "dispose view - aborted"),
455+
);
456+
})
457+
.catch((error) => log_anywidget_error(error));
433458
});
434-
return () => {
435-
dispose();
436-
cleanup?.();
437-
};
459+
return () => dispose();
438460
});
439461
}
440462
}
@@ -463,21 +485,20 @@ export default function ({ DOMWidgetModel, DOMWidgetView }) {
463485
initialize(...args) {
464486
super.initialize(...args);
465487
let controller = new AbortController();
466-
let runtime = new Runtime(this, { signal: controller.signal });
467488
this.once("destroy", () => {
468-
try {
469-
controller.abort("[anywidget] Runtime destroyed.");
470-
} finally {
471-
RUNTIMES.delete(this);
472-
}
489+
controller.abort("[anywidget] Runtime destroyed.");
490+
RUNTIMES.delete(this);
473491
});
474-
RUNTIMES.set(this, runtime);
492+
RUNTIMES.set(this, new Runtime(this, { signal: controller.signal }));
475493
}
476494

477495
/** @param {Parameters<InstanceType<DOMWidgetModel>["_handle_comm_msg"]>} msg */
478496
async _handle_comm_msg(...msg) {
479-
const runtime = RUNTIMES.get(this);
480-
await runtime?.ready;
497+
let runtime = RUNTIMES.get(this);
498+
await runtime?.ready.catch(() => {
499+
console.log("timed out");
500+
console.log({ msg });
501+
});
481502
return super._handle_comm_msg(...msg);
482503
}
483504

0 commit comments

Comments
 (0)