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

Add support for deferred loading of bokehjs' bundles #13547

Open
wants to merge 1 commit into
base: branch-3.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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