Skip to content

Commit

Permalink
Add support for deferred loading of bokehjs' bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap committed Feb 20, 2024
1 parent 0021c18 commit a3beb7d
Show file tree
Hide file tree
Showing 18 changed files with 253 additions and 117 deletions.
24 changes: 17 additions & 7 deletions bokehjs/src/compiler/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const loader = `\
if (alias != null)
return alias;
const trailing = name.length > 0 && name[name.lenght-1] === "/";
const trailing = name.length > 0 && name[name.length-1] === "/";
const index = aliases[name + (trailing ? "" : "/") + "index"];
if (index != null)
return index;
Expand Down Expand Up @@ -104,7 +104,7 @@ const loader = `\
});
}
modules[id].call(mod.exports, require, mod, mod.exports, __esModule, __esExport);
modules[id].call(mod.exports, require, mod, mod.exports, __esModule, __esExport, base_url);
} else {
cache[name] = mod;
}
Expand Down Expand Up @@ -195,9 +195,18 @@ ${comment(license)}
}
const Bokeh = root.Bokeh;
Bokeh[bokeh.version] = bokeh;
})(this, function() {
})(globalThis, function() {
let define;
const parent_require = typeof require === "function" && require
const parent_require = typeof require === "function" && require;
const base_url = (() => {
if (typeof document !== "undefined" && document.currentScript != null) {
const parts = document.currentScript.src.split("/");
parts.pop();
return parts.join("/");
} else {
return null
}
})()
return ${loader}\
`
}
Expand All @@ -216,10 +225,10 @@ export function plugin_prelude(options?: {version?: string}): string {
${comment(license)}
(function(root, factory) {
factory(root["Bokeh"], ${str(options?.version)});
})(this, function(Bokeh, version) {
})(globalThis, function(Bokeh, version) {
let define;
return (function(modules, entry, aliases, externals) {
const bokeh = typeof Bokeh !== "undefined" && (version != null ? Bokeh[version] : Bokeh);
const bokeh = typeof Bokeh !== "undefined" ? (version != null ? Bokeh[version] : Bokeh) : null;
if (bokeh != null) {
return bokeh.register_plugin(modules, entry, aliases);
} else {
Expand All @@ -233,8 +242,9 @@ export function default_prelude(options?: {global?: string}): string {
return `\
(function(root, factory) {
${options?.global != null ? `root[${str(options.global)}] = factory()` : "Object.assign(root, factory())"};
})(this, function() {
})(globalThis, function() {
const parent_require = typeof require === "function" && require
const base_url = null
return ${loader}
`
}
2 changes: 1 addition & 1 deletion bokehjs/src/compiler/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ export function wrap_in_function(module_name: string) {
return (context: ts.TransformationContext) => (root: ts.SourceFile): ts.SourceFile => {
const {factory} = context
const p = (name: string) => factory.createParameterDeclaration(undefined, undefined, name)
const params = [p("require"), p("module"), p("exports"), p("__esModule"), p("__esExport")]
const params = [p("require"), p("module"), p("exports"), p("__esModule"), p("__esExport"), p("base_url")]
const block = factory.createBlock(root.statements, true)
const func = factory.createFunctionDeclaration(undefined, undefined, "_", undefined, params, undefined, block)
ts.addSyntheticLeadingComment(func, ts.SyntaxKind.MultiLineCommentTrivia, ` ${module_name} `, false)
Expand Down
26 changes: 14 additions & 12 deletions bokehjs/src/lib/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ClientConnection {
closed_permanently: boolean = false
id: string

protected _current_handler: ((message: Message<unknown>) => void) | null = null
protected _current_handler: ((message: Message<unknown>) => Promise<void>) | null = null
protected _pending_replies: Map<string, PendingReply> = new Map()
protected _pending_messages: Message<unknown>[] = []
protected readonly _receiver: Receiver = new Receiver()
Expand Down Expand Up @@ -162,7 +162,7 @@ export class ClientConnection {
reject(new Error("The connection has been closed"))
} else {
const events: DocumentEvent[] = []
const document = Document.from_json(doc_json, events)
const document = await Document.from_json(doc_json, events)

this.session = new ClientSession(this, document)

Expand All @@ -172,15 +172,15 @@ export class ClientConnection {
}

for (const msg of this._pending_messages) {
this.session.handle(msg)
await this.session.handle(msg)
}
this._pending_messages = []

logger.debug("Created a new session from new pulled doc")
resolve(this.session)
}
} else {
this.session.document.replace_with_json(doc_json)
await this.session.document.replace_with_json(doc_json)
logger.debug("Updated existing session with new pulled doc")
// Since the session already exists, we don't need to call `resolve` again.
}
Expand All @@ -193,12 +193,12 @@ export class ClientConnection {

protected _on_open(resolve: SessionResolver, reject: Rejecter): void {
logger.info(`Websocket connection ${this._number} is now open`)
this._current_handler = (message: Message<unknown>) => {
this._awaiting_ack_handler(message, resolve, reject)
this._current_handler = async (message: Message<unknown>) => {
await this._awaiting_ack_handler(message, resolve, reject)
}
}

protected _on_message(event: MessageEvent): void {
protected async _on_message(event: MessageEvent): Promise<void> {
if (this._current_handler == null) {
logger.error("Got a message with no current handler set")
}
Expand All @@ -216,7 +216,7 @@ export class ClientConnection {
this._close_bad_protocol(problem)
}

this._current_handler!(msg)
await this._current_handler!(msg)
}
}

Expand Down Expand Up @@ -248,9 +248,11 @@ export class ClientConnection {
} // 1002 = protocol error
}

protected _awaiting_ack_handler(message: Message<unknown>, resolve: SessionResolver, reject: Rejecter): void {
protected async _awaiting_ack_handler(message: Message<unknown>, resolve: SessionResolver, reject: Rejecter): Promise<void> {
if (message.msgtype() === "ACK") {
this._current_handler = (message) => this._steady_state_handler(message)
this._current_handler = async (message) => {
await this._steady_state_handler(message)
}

// Reload any sessions
void this._repull_session_doc(resolve, reject)
Expand All @@ -259,14 +261,14 @@ export class ClientConnection {
}
}

protected _steady_state_handler(message: Message<unknown>): void {
protected async _steady_state_handler(message: Message<unknown>): Promise<void> {
const reqid = message.reqid()
const pr = this._pending_replies.get(reqid)
if (pr != null) {
this._pending_replies.delete(reqid)
pr.resolve(message)
} else if (this.session != null) {
this.session.handle(message)
await this.session.handle(message)
} else if (message.msgtype() != "PATCH-DOC") {
// This branch can be executed only before we get the document.
// When we get the document, all of the patches will already be incorporated.
Expand Down
12 changes: 6 additions & 6 deletions bokehjs/src/lib/client/session.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {DocumentEventBatch} from "document"
import {ConnectionLost} from "core/bokeh_events"
import type {Patch, Document, DocumentEvent} from "document"
import type {InboundPatch, Document, DocumentEvent} from "document"
import {Message} from "protocol/message"
import type {ClientConnection} from "./connection"
import {logger} from "core/logging"

export type OkMsg = Message<{}>
export type ErrorMsg = Message<{text: string, traceback: string | null}>
export type PatchMsg = Message<Patch>
export type PatchMsg = Message<InboundPatch>

export class ClientSession {
protected _document_listener = (event: DocumentEvent) => {
Expand All @@ -23,12 +23,12 @@ export class ClientSession {
return this._connection.id
}

handle(message: Message<unknown>): void {
async handle(message: Message<unknown>): Promise<void> {
const msgtype = message.msgtype()

switch (msgtype) {
case "PATCH-DOC": {
this._handle_patch(message as PatchMsg)
await this._handle_patch(message as PatchMsg)
break
}
case "OK": {
Expand Down Expand Up @@ -96,8 +96,8 @@ export class ClientSession {
this._connection.send(message)
}

protected _handle_patch(message: PatchMsg): void {
this.document.apply_json_patch(message.content, message.buffers)
protected async _handle_patch(message: PatchMsg): Promise<void> {
await this.document.apply_json_patch(message.content, message.buffers)
}

protected _handle_ok(message: OkMsg): void {
Expand Down
6 changes: 6 additions & 0 deletions bokehjs/src/lib/core/util/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ export async function load_module<T>(module: Promise<T>): Promise<T | null> {
}
}
}

export async function import_url(url: string): Promise<unknown> {
// XXX: eval() to work around transpilation to require()
// https://github.com/microsoft/TypeScript/issues/43329
return await eval(`import("${url}")`)
}
80 changes: 74 additions & 6 deletions bokehjs/src/lib/document/document.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {default_resolver} from "../base"
import {version as js_version} from "../version"
import {settings} from "../core/settings"
import {logger} from "../core/logging"
import type {Class} from "core/class"
import type {HasProps} from "core/has_props"
Expand All @@ -20,6 +21,7 @@ import {entries, dict} from "core/util/object"
import * as sets from "core/util/set"
import type {CallbackLike} from "core/util/callbacks"
import {execute} from "core/util/callbacks"
import {assert} from "core/util/assert"
import {Model} from "model"
import type {ModelDef} from "./defs"
import {decode_def} from "./defs"
Expand All @@ -29,6 +31,8 @@ import type {DocumentEvent, DocumentChangedEvent, Decoded, DocumentChanged} from
import {DocumentEventBatch, RootRemovedEvent, TitleChangedEvent, MessageSentEvent, RootAddedEvent} from "./events"
import type {ViewManager} from "core/view_manager"

declare const base_url: string | null | undefined

Deserializer.register("model", decode_def)

export type Out<T> = T
Expand Down Expand Up @@ -63,6 +67,7 @@ export class EventManager {
export type DocJson = {
version?: string
title?: string
bundles?: string[]
defs?: ModelDef[]
roots: ModelRep[]
callbacks?: {[key: string]: ModelRep[]}
Expand All @@ -72,6 +77,10 @@ export type Patch = {
events: DocumentChanged[]
}

export type InboundPatch = Patch & {
bundles?: string[]
}

export const documents: Document[] = []

export const DEFAULT_TITLE = "Bokeh Application"
Expand Down Expand Up @@ -431,9 +440,9 @@ export class Document implements Equatable {
}
}

static from_json_string(s: string, events?: Out<DocumentEvent[]>): Document {
static async from_json_string(s: string, events?: Out<DocumentEvent[]>): Promise<Document> {
const json = JSON.parse(s)
return Document.from_json(json, events)
return await Document.from_json(json, events)
}

private static _handle_version(json: DocJson): void {
Expand All @@ -452,10 +461,65 @@ export class Document implements Equatable {
}
}

static from_json(doc_json: DocJson, events?: Out<DocumentEvent[]>): Document {
private static _known_bundles: Set<string> = new Set()

static async load_bundles(bundles: string[]): Promise<void> {

const is_absolute = (uri: string): boolean => {
return uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("/")
}

const load_url = async (url: string): Promise<boolean> => {
if (this._known_bundles.has(url)) {
return true
}

const response = await fetch(url)
if (response.ok) {
this._known_bundles.add(url)
const module = await response.text()
eval(module)
return true
} else {
return false
}
}

const build_url = (base_name: string, version?: string): string => {
assert(base_url != null)
const name = version != null ? `${base_name}-${version}` : base_name
const suffix = settings.dev ? ".min.js" : ".js"
return `${base_url}/${name}${suffix}`
}

for (const bundle of bundles) {
if (is_absolute(bundle)) {
if (await load_url(bundle)) {
continue
}
} else {
const url = build_url(bundle)
if (await load_url(url)) {
continue
}
const versioned_url = build_url(bundle, pyify_version(js_version))
if (await load_url(versioned_url)) {
continue
}
}

console.error(`failed to load '${bundle}' bundle`)
}
}

static async from_json(doc_json: DocJson, events?: Out<DocumentEvent[]>): Promise<Document> {
logger.debug("Creating Document from JSON")
Document._handle_version(doc_json)

if (doc_json.bundles != null) {
await this.load_bundles(doc_json.bundles)
}

const resolver = new ModelResolver(default_resolver)
if (doc_json.defs != null) {
const deserializer = new Deserializer(resolver)
Expand Down Expand Up @@ -498,8 +562,8 @@ export class Document implements Equatable {
return doc
}

replace_with_json(json: DocJson): void {
const replacement = Document.from_json(json)
async replace_with_json(json: DocJson): Promise<void> {
const replacement = await Document.from_json(json)
replacement.destructively_move(this)
}

Expand All @@ -524,7 +588,11 @@ export class Document implements Equatable {
return patch
}

apply_json_patch(patch: Patch, buffers: Map<ID, ArrayBuffer> = new Map()): void {
async apply_json_patch(patch: InboundPatch, buffers: Map<ID, ArrayBuffer> = new Map()): Promise<void> {
if (patch.bundles != null) {
await Document.load_bundles(patch.bundles)
}

this._push_all_models_freeze()

const deserializer = new Deserializer(this._resolver, this._all_models, (obj) => obj.attach_document(this))
Expand Down
2 changes: 1 addition & 1 deletion bokehjs/src/lib/embed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async function _embed_items(docs_json: string | DocsJson, render_items: RenderIt

const docs: {[key: string]: Document} = {}
for (const [docid, doc_json] of entries(docs_json)) {
docs[docid] = Document.from_json(doc_json)
docs[docid] = await Document.from_json(doc_json)
}

const views: ViewManager[] = []
Expand Down

0 comments on commit a3beb7d

Please sign in to comment.