From b45958cda2cd94210e4fcf655998c24e1aae09b8 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Wed, 3 Jun 2026 00:14:16 +0530 Subject: [PATCH 1/5] Add startup extensions for JavaScript kernels --- README.md | 45 +++++++ .../javascript-kernel-extension/package.json | 6 +- .../javascript-kernel-extension/src/index.ts | 120 +++++++++++++++-- packages/javascript-kernel/package.json | 1 + .../javascript-kernel/src/comm/manager.ts | 14 ++ packages/javascript-kernel/src/executor.ts | 12 ++ packages/javascript-kernel/src/index.ts | 1 + packages/javascript-kernel/src/kernel.ts | 124 +++++++++++++++++- .../javascript-kernel/src/runtime_backends.ts | 28 +++- .../src/runtime_evaluator.ts | 36 +++++ .../javascript-kernel/src/runtime_protocol.ts | 7 + .../javascript-kernel/src/runtime_remote.ts | 20 +++ packages/javascript-kernel/src/startup.ts | 24 ++++ yarn.lock | 2 + 14 files changed, 422 insertions(+), 18 deletions(-) create mode 100644 packages/javascript-kernel/src/startup.ts diff --git a/README.md b/README.md index d48635b..0682a13 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,51 @@ The extension currently registers two JavaScript kernelspecs: Pick either kernel from the notebook kernel selector in JupyterLite. +### Kernel startup extensions + +Frontend extensions can register startup work that runs before user code in +both runtime modes. Use this to preload runtime modules and register comm +targets without sending bootstrap code through `requestExecute`. + +```typescript +import type { JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { IJavaScriptKernelStartup } from '@jupyterlite/javascript-kernel'; + +const plugin: JupyterFrontEndPlugin = { + id: 'my-extension:javascript-startup', + autoStart: true, + requires: [IJavaScriptKernelStartup], + activate: (app, startup) => { + const runtimeBootstrap = new URL( + './runtime-bootstrap.js', + import.meta.url + ).toString(); + const lspCommTarget = new URL( + './lsp-comm-target.js', + import.meta.url + ).toString(); + + startup.registerStartupExtension({ + id: 'my-extension:lsp', + modulePreloads: [runtimeBootstrap], + commTargets: [ + { + targetName: 'my-extension:lsp', + module: lspCommTarget, + exportName: 'registerLspTarget' + } + ] + }); + } +}; +``` + +Each `commTargets` entry imports the module in the kernel runtime and passes +the exported handler to `Jupyter.comm.registerTarget(targetName, handler)`. +The default export is used when `exportName` is omitted. +Disposing a startup registration removes declared comm targets from active +kernels; use an optional `deactivate` callback for extra cleanup. + ### Worker mode limitations Web Workers do not expose DOM APIs. In `JavaScript (Web Worker)`, APIs such as `document`, direct element access, and other main-thread-only browser APIs are unavailable. diff --git a/packages/javascript-kernel-extension/package.json b/packages/javascript-kernel-extension/package.json index 673989c..7ab2150 100644 --- a/packages/javascript-kernel-extension/package.json +++ b/packages/javascript-kernel-extension/package.json @@ -41,7 +41,8 @@ "dependencies": { "@jupyterlab/application": "^4.5.5", "@jupyterlite/javascript-kernel": "^0.4.0-alpha.4", - "@jupyterlite/services": "^0.7.0" + "@jupyterlite/services": "^0.7.0", + "@lumino/disposable": "^2.1.5" }, "devDependencies": { "@jupyterlab/builder": "^4.5.5", @@ -56,6 +57,9 @@ "extension": true, "outputDir": "../../jupyterlite_javascript_kernel/labextension", "sharedPackages": { + "@jupyterlite/javascript-kernel": { + "singleton": true + }, "@jupyterlite/services": { "bundled": false, "singleton": true diff --git a/packages/javascript-kernel-extension/src/index.ts b/packages/javascript-kernel-extension/src/index.ts index 0ceaf1f..43e9d02 100644 --- a/packages/javascript-kernel-extension/src/index.ts +++ b/packages/javascript-kernel-extension/src/index.ts @@ -9,8 +9,13 @@ import { import type { IKernel } from '@jupyterlite/services'; import { IKernelSpecs } from '@jupyterlite/services'; +import { DisposableDelegate } from '@lumino/disposable'; +import type { IDisposable } from '@lumino/disposable'; -import { JavaScriptKernel } from '@jupyterlite/javascript-kernel'; +import { + IJavaScriptKernelStartup, + JavaScriptKernel +} from '@jupyterlite/javascript-kernel'; import type { RuntimeMode } from '@jupyterlite/javascript-kernel'; import jsLogo32 from '../style/icons/logo-32x32.png'; @@ -24,13 +29,14 @@ interface IRegisterKernelOptions { name: string; displayName: string; runtime: RuntimeMode; + startup: IJavaScriptKernelStartup; } const registerKernel = ( kernelspecs: IKernelSpecs, options: IRegisterKernelOptions ) => { - const { name, displayName, runtime } = options; + const { name, displayName, runtime, startup } = options; kernelspecs.register({ spec: { @@ -54,26 +60,111 @@ const registerKernel = ( } }, create: async (options: IKernel.IOptions): Promise => { - return new JavaScriptKernel({ + const kernel = new JavaScriptKernel({ ...options, - runtime + runtime, + startupExtensions: startup.startupExtensions } as JavaScriptKernel.IOptions); + if (startup instanceof JavaScriptKernelStartup) { + startup.trackKernel(kernel); + } + return kernel; } }); }; +/** + * In-memory registry for JavaScript kernel startup extensions. + */ +class JavaScriptKernelStartup implements IJavaScriptKernelStartup { + get startupExtensions(): readonly JavaScriptKernel.IStartupExtension[] { + return [...this._startupExtensions]; + } + + registerStartupExtension( + extension: JavaScriptKernel.IStartupExtension + ): IDisposable { + const existing = this._startupExtensions.findIndex( + item => item.id === extension.id + ); + + if (existing !== -1) { + throw new Error( + `JavaScript kernel startup extension "${extension.id}" is already registered` + ); + } + + this._startupExtensions.push(extension); + void Promise.all( + [...this._kernels].map(kernel => kernel.applyStartupExtension(extension)) + ).catch(error => { + console.error( + `[javascript-kernel] Failed to apply startup extension "${extension.id}".`, + error + ); + }); + + return new DisposableDelegate(() => { + const index = this._startupExtensions.indexOf(extension); + if (index !== -1) { + this._startupExtensions.splice(index, 1); + void Promise.all( + [...this._kernels].map(kernel => + kernel.removeStartupExtension(extension) + ) + ).catch(error => { + console.error( + `[javascript-kernel] Failed to remove startup extension "${extension.id}".`, + error + ); + }); + } + }); + } + + /** + * Track an active JavaScript kernel for late startup registrations. + */ + trackKernel(kernel: JavaScriptKernel): void { + this._kernels.add(kernel); + const untrackKernel = (sender: JavaScriptKernel): void => { + this._kernels.delete(sender); + sender.disposed.disconnect(untrackKernel); + }; + kernel.disposed.connect(untrackKernel); + } + + private _startupExtensions: JavaScriptKernel.IStartupExtension[] = []; + private _kernels = new Set(); +} + +/** + * Plugin providing the JavaScript kernel startup extension registry. + */ +const startupExtensions: JupyterFrontEndPlugin = { + id: '@jupyterlite/javascript-kernel-extension:startup-extensions', + autoStart: true, + provides: IJavaScriptKernelStartup, + activate: () => new JavaScriptKernelStartup() +}; + /** * Plugin registering the iframe JavaScript kernel. */ const kernelIFrame: JupyterFrontEndPlugin = { id: '@jupyterlite/javascript-kernel-extension:kernel-iframe', autoStart: true, - requires: [IKernelSpecs], - activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => { + requires: [IKernelSpecs, IJavaScriptKernelStartup], + activate: ( + app: JupyterFrontEnd, + kernelspecs: IKernelSpecs, + startup: IJavaScriptKernelStartup + ) => { registerKernel(kernelspecs, { name: 'javascript', displayName: 'JavaScript (IFrame)', - runtime: 'iframe' + runtime: 'iframe', + startup }); } }; @@ -84,16 +175,23 @@ const kernelIFrame: JupyterFrontEndPlugin = { const kernelWorker: JupyterFrontEndPlugin = { id: '@jupyterlite/javascript-kernel-extension:kernel-worker', autoStart: true, - requires: [IKernelSpecs], - activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => { + requires: [IKernelSpecs, IJavaScriptKernelStartup], + activate: ( + app: JupyterFrontEnd, + kernelspecs: IKernelSpecs, + startup: IJavaScriptKernelStartup + ) => { registerKernel(kernelspecs, { name: 'javascript-worker', displayName: 'JavaScript (Web Worker)', - runtime: 'worker' + runtime: 'worker', + startup }); } }; -const plugins: JupyterFrontEndPlugin[] = [kernelIFrame, kernelWorker]; +const plugins: Array< + JupyterFrontEndPlugin | JupyterFrontEndPlugin +> = [startupExtensions, kernelIFrame, kernelWorker]; export default plugins; diff --git a/packages/javascript-kernel/package.json b/packages/javascript-kernel/package.json index d03e554..ba844c5 100644 --- a/packages/javascript-kernel/package.json +++ b/packages/javascript-kernel/package.json @@ -47,6 +47,7 @@ "@jupyterlab/nbformat": "^4.5.0", "@jupyterlite/services": "^0.7.0", "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", "astring": "^1.9.0", "comlink": "^4.3.1", "meriyah": "^4.3.9" diff --git a/packages/javascript-kernel/src/comm/manager.ts b/packages/javascript-kernel/src/comm/manager.ts index 151067b..27ffc8f 100644 --- a/packages/javascript-kernel/src/comm/manager.ts +++ b/packages/javascript-kernel/src/comm/manager.ts @@ -76,6 +76,20 @@ export class CommManager { this._targets.set(targetName, handler); } + /** + * Whether a comm target handler is registered. + */ + hasTarget(targetName: string): boolean { + return this._targets.has(targetName); + } + + /** + * Remove a comm target handler registration. + */ + unregisterTarget(targetName: string): void { + this._targets.delete(targetName); + } + /** * Register a widget instance by comm ID. */ diff --git a/packages/javascript-kernel/src/executor.ts b/packages/javascript-kernel/src/executor.ts index 0919aa7..385344f 100644 --- a/packages/javascript-kernel/src/executor.ts +++ b/packages/javascript-kernel/src/executor.ts @@ -292,6 +292,18 @@ export class JavaScriptExecutor { return lines.join('\n'); } + /** + * Import a module using the executor's import source resolution. + */ + async importModule(source: string): Promise> { + const importSource = this._transformImportSource(source); + const importFunction = this._createScopedFunction( + 'source', + 'return import(source);' + ) as (source: string) => Promise>; + return importFunction.call(this._globalScope, importSource); + } + /** * Create a new empty code registry. */ diff --git a/packages/javascript-kernel/src/index.ts b/packages/javascript-kernel/src/index.ts index 5ee10b4..5df8403 100644 --- a/packages/javascript-kernel/src/index.ts +++ b/packages/javascript-kernel/src/index.ts @@ -9,3 +9,4 @@ export * from './runtime_backends'; export * from './runtime_evaluator'; export * from './comm'; export * from './widgets'; +export * from './startup'; diff --git a/packages/javascript-kernel/src/kernel.ts b/packages/javascript-kernel/src/kernel.ts index e7e0b33..944b1bd 100644 --- a/packages/javascript-kernel/src/kernel.ts +++ b/packages/javascript-kernel/src/kernel.ts @@ -27,6 +27,7 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { super(options); this._runtimeMode = options.runtime ?? 'iframe'; this._executorFactory = options.executorFactory; + this._startupExtensions = [...(options.startupExtensions ?? [])]; this._backend = this.createBackend(this._runtimeMode); } @@ -40,6 +41,8 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { this._comms.clear(); this._backend.dispose(); + this._runtimeReadyContext = null; + this._appliedStartupExtensions.clear(); super.dispose(); } @@ -260,9 +263,47 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { * Called once a runtime backend is initialized, before `ready` resolves. */ protected async onRuntimeReady( - _context: JavaScriptKernel.IRuntimeReadyContext + context: JavaScriptKernel.IRuntimeReadyContext ): Promise { - return Promise.resolve(); + this._runtimeReadyContext = context; + for (const extension of this._startupExtensions) { + await this._applyStartupExtension(extension, context); + } + } + + /** + * Apply a startup extension to an initialized runtime. + */ + async applyStartupExtension( + extension: JavaScriptKernel.IStartupExtension + ): Promise { + await this.ready; + const context = this._runtimeReadyContext; + if (!context) { + throw new Error('JavaScript runtime is not initialized'); + } + await this._applyStartupExtension(extension, context); + } + + /** + * Remove runtime registrations made by a startup extension. + */ + async removeStartupExtension( + extension: JavaScriptKernel.IStartupExtension + ): Promise { + await this.ready; + const context = this._runtimeReadyContext; + if (!context || !this._appliedStartupExtensions.has(extension.id)) { + return; + } + try { + await extension.deactivate?.(context); + } finally { + for (const target of extension.commTargets ?? []) { + await context.unregisterCommTarget(target.targetName); + } + this._appliedStartupExtensions.delete(extension.id); + } } /** @@ -287,7 +328,16 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { throw this._createRuntimeInitializationError(reply); } return reply; - } + }, + preloadModule: context.preloadModule, + registerCommTarget: async target => { + await context.registerCommTarget( + target.targetName, + target.module, + target.exportName + ); + }, + unregisterCommTarget: context.unregisterCommTarget }); } }); @@ -307,7 +357,16 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { throw this._createRuntimeInitializationError(reply); } return reply; - } + }, + preloadModule: context.preloadModule, + registerCommTarget: async target => { + await context.registerCommTarget( + target.targetName, + target.module, + target.exportName + ); + }, + unregisterCommTarget: context.unregisterCommTarget }); } }); @@ -451,10 +510,43 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { ); } + /** + * Apply a startup extension once to the given runtime context. + */ + private async _applyStartupExtension( + extension: JavaScriptKernel.IStartupExtension, + context: JavaScriptKernel.IRuntimeReadyContext + ): Promise { + if (this._appliedStartupExtensions.has(extension.id)) { + return; + } + const registeredTargets: JavaScriptKernel.IStartupCommTarget[] = []; + try { + for (const moduleName of extension.modulePreloads ?? []) { + await context.preloadModule(moduleName); + } + for (const target of extension.commTargets ?? []) { + await context.registerCommTarget(target); + registeredTargets.push(target); + } + await extension.activate?.(context); + this._appliedStartupExtensions.add(extension.id); + } catch (error) { + for (const target of registeredTargets.reverse()) { + await context.unregisterCommTarget(target.targetName); + } + throw error; + } + } + private _unsupportedControlMessages = new Set<'input_reply'>(); private _comms = new Map(); private _backend: IRuntimeBackend; private _executorFactory?: JavaScriptKernel.IExecutorFactory; + private _startupExtensions: JavaScriptKernel.IStartupExtension[]; + private _appliedStartupExtensions = new Set(); + private _runtimeReadyContext: JavaScriptKernel.IRuntimeReadyContext | null = + null; private _runtimeMode: RuntimeMode; } @@ -468,6 +560,9 @@ export namespace JavaScriptKernel { export interface IRuntimeReadyContextBase { runtime: RuntimeMode; execute: (code: string) => Promise; + preloadModule: (moduleName: string) => Promise; + registerCommTarget: (target: IStartupCommTarget) => Promise; + unregisterCommTarget: (targetName: string) => Promise; } /** @@ -493,6 +588,26 @@ export namespace JavaScriptKernel { | IIFrameRuntimeReadyContext | IWorkerRuntimeReadyContext; + /** + * A comm target handler exported by a startup module. + */ + export interface IStartupCommTarget { + targetName: string; + module: string; + exportName?: string; + } + + /** + * Startup extension executed before the kernel accepts user code. + */ + export interface IStartupExtension { + id: string; + modulePreloads?: readonly string[]; + commTargets?: readonly IStartupCommTarget[]; + activate?: (context: IRuntimeReadyContext) => Promise | void; + deactivate?: (context: IRuntimeReadyContext) => Promise | void; + } + /** * Factory used to customize iframe runtime evaluation behavior. */ @@ -506,5 +621,6 @@ export namespace JavaScriptKernel { export interface IOptions extends IKernel.IOptions { runtime?: RuntimeMode; executorFactory?: IExecutorFactory; + startupExtensions?: readonly IStartupExtension[]; } } diff --git a/packages/javascript-kernel/src/runtime_backends.ts b/packages/javascript-kernel/src/runtime_backends.ts index 44d856e..4479d31 100644 --- a/packages/javascript-kernel/src/runtime_backends.ts +++ b/packages/javascript-kernel/src/runtime_backends.ts @@ -359,7 +359,12 @@ export class IFrameRuntimeBackend extends AbstractRuntimeBackend { globalScope: this._globalScope, executor: this._executor, execute: (code, executionCount = 0) => - remote.execute(code, executionCount) + remote.execute(code, executionCount), + preloadModule: moduleName => remote.preloadModule(moduleName), + registerCommTarget: (targetName, moduleName, exportName) => + remote.registerCommTarget(targetName, moduleName, exportName), + unregisterCommTarget: targetName => + remote.unregisterCommTarget(targetName) }); this._ready.resolve(); @@ -412,6 +417,13 @@ export namespace IFrameRuntimeBackend { code: string, executionCount?: number ) => Promise; + preloadModule: (moduleName: string) => Promise; + registerCommTarget: ( + targetName: string, + moduleName: string, + exportName?: string + ) => Promise; + unregisterCommTarget: (targetName: string) => Promise; } /** @@ -510,7 +522,12 @@ export class WorkerRuntimeBackend extends AbstractRuntimeBackend { await this._options.onReady?.({ execute: (code, executionCount = 0) => - remote.execute(code, executionCount) + remote.execute(code, executionCount), + preloadModule: moduleName => remote.preloadModule(moduleName), + registerCommTarget: (targetName, moduleName, exportName) => + remote.registerCommTarget(targetName, moduleName, exportName), + unregisterCommTarget: targetName => + remote.unregisterCommTarget(targetName) }); this._ready.resolve(); @@ -555,6 +572,13 @@ export namespace WorkerRuntimeBackend { code: string, executionCount?: number ) => Promise; + preloadModule: (moduleName: string) => Promise; + registerCommTarget: ( + targetName: string, + moduleName: string, + exportName?: string + ) => Promise; + unregisterCommTarget: (targetName: string) => Promise; } /** diff --git a/packages/javascript-kernel/src/runtime_evaluator.ts b/packages/javascript-kernel/src/runtime_evaluator.ts index e332474..2f8c486 100644 --- a/packages/javascript-kernel/src/runtime_evaluator.ts +++ b/packages/javascript-kernel/src/runtime_evaluator.ts @@ -156,6 +156,42 @@ export class JavaScriptRuntimeEvaluator { return this._commManager; } + /** + * Preload a module in the runtime scope. + */ + async preloadModule(moduleName: string): Promise { + await this._executor.importModule(moduleName); + } + + /** + * Register a comm target handler exported by a runtime module. + */ + async registerCommTarget( + targetName: string, + moduleName: string, + exportName = 'default' + ): Promise { + if (this._commManager.hasTarget(targetName)) { + throw new Error(`Comm target ${targetName} is already registered`); + } + + const moduleExports = await this._executor.importModule(moduleName); + const handler = moduleExports[exportName]; + if (typeof handler !== 'function') { + throw new TypeError( + `Comm target ${targetName} must export a handler function` + ); + } + this._commManager.registerTarget(targetName, handler); + } + + /** + * Remove a comm target handler registration. + */ + unregisterCommTarget(targetName: string): void { + this._commManager.unregisterTarget(targetName); + } + /** * Handle a comm_open message from the frontend. */ diff --git a/packages/javascript-kernel/src/runtime_protocol.ts b/packages/javascript-kernel/src/runtime_protocol.ts index 37db748..91e851f 100644 --- a/packages/javascript-kernel/src/runtime_protocol.ts +++ b/packages/javascript-kernel/src/runtime_protocol.ts @@ -96,6 +96,13 @@ export interface IRemoteRuntimeApi { executionCount: number, parentMessageId?: string ): Promise; + preloadModule(moduleName: string): Promise; + registerCommTarget( + targetName: string, + moduleName: string, + exportName?: string + ): Promise; + unregisterCommTarget(targetName: string): Promise; complete( code: string, cursorPos: number diff --git a/packages/javascript-kernel/src/runtime_remote.ts b/packages/javascript-kernel/src/runtime_remote.ts index 5c85789..5be6e2e 100644 --- a/packages/javascript-kernel/src/runtime_remote.ts +++ b/packages/javascript-kernel/src/runtime_remote.ts @@ -59,6 +59,26 @@ export function createRemoteRuntimeApi( return ensureEvaluator().execute(code, executionCount, parentMessageId); }, + async preloadModule(moduleName: string): Promise { + await ensureEvaluator().preloadModule(moduleName); + }, + + async registerCommTarget( + targetName: string, + moduleName: string, + exportName?: string + ): Promise { + await ensureEvaluator().registerCommTarget( + targetName, + moduleName, + exportName + ); + }, + + async unregisterCommTarget(targetName: string): Promise { + ensureEvaluator().unregisterCommTarget(targetName); + }, + async complete(code: string, cursorPos: number) { return ensureEvaluator().complete(code, cursorPos); }, diff --git a/packages/javascript-kernel/src/startup.ts b/packages/javascript-kernel/src/startup.ts new file mode 100644 index 0000000..dbd31e4 --- /dev/null +++ b/packages/javascript-kernel/src/startup.ts @@ -0,0 +1,24 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { Token } from '@lumino/coreutils'; +import type { IDisposable } from '@lumino/disposable'; + +import type { JavaScriptKernel } from './kernel'; + +/** + * Registry for JavaScript kernel startup extensions. + */ +export interface IJavaScriptKernelStartup { + readonly startupExtensions: readonly JavaScriptKernel.IStartupExtension[]; + registerStartupExtension( + extension: JavaScriptKernel.IStartupExtension + ): IDisposable; +} + +/** + * Token for registering JavaScript kernel startup extensions. + */ +export const IJavaScriptKernelStartup = new Token( + '@jupyterlite/javascript-kernel:IJavaScriptKernelStartup' +); diff --git a/yarn.lock b/yarn.lock index d9230c0..e6ce3ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3648,6 +3648,7 @@ __metadata: "@jupyterlab/builder": ^4.5.5 "@jupyterlite/javascript-kernel": ^0.4.0-alpha.4 "@jupyterlite/services": ^0.7.0 + "@lumino/disposable": ^2.1.5 npm-run-all2: ^7.0.1 rimraf: ~5.0.1 typescript: ~5.0.2 @@ -3683,6 +3684,7 @@ __metadata: "@jupyterlab/testutils": ~4.5.0 "@jupyterlite/services": ^0.7.0 "@lumino/coreutils": ^2.2.2 + "@lumino/disposable": ^2.1.5 "@types/jest": ^26.0.10 astring: ^1.9.0 comlink: ^4.3.1 From 210fd9e2a3c0f6dd2b22ea399d0c3b6501cc8301 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Wed, 1 Jul 2026 01:46:16 +0530 Subject: [PATCH 2/5] add trackkernel function to the interface instead of checking the class implementation --- packages/javascript-kernel-extension/src/index.ts | 4 +--- packages/javascript-kernel/src/startup.ts | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/javascript-kernel-extension/src/index.ts b/packages/javascript-kernel-extension/src/index.ts index 43e9d02..dff4a76 100644 --- a/packages/javascript-kernel-extension/src/index.ts +++ b/packages/javascript-kernel-extension/src/index.ts @@ -65,9 +65,7 @@ const registerKernel = ( runtime, startupExtensions: startup.startupExtensions } as JavaScriptKernel.IOptions); - if (startup instanceof JavaScriptKernelStartup) { - startup.trackKernel(kernel); - } + startup.trackKernel(kernel); return kernel; } }); diff --git a/packages/javascript-kernel/src/startup.ts b/packages/javascript-kernel/src/startup.ts index dbd31e4..9b0bcd6 100644 --- a/packages/javascript-kernel/src/startup.ts +++ b/packages/javascript-kernel/src/startup.ts @@ -14,6 +14,7 @@ export interface IJavaScriptKernelStartup { registerStartupExtension( extension: JavaScriptKernel.IStartupExtension ): IDisposable; + trackKernel(kernel: JavaScriptKernel): void; } /** From 060eaa12d9ed10cb13b28def0c7211153f94636a Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Sun, 12 Jul 2026 01:54:22 +0530 Subject: [PATCH 3/5] Handle startup extension tracking races --- packages/javascript-kernel-extension/src/index.ts | 10 ++++++++++ packages/javascript-kernel/src/kernel.ts | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/javascript-kernel-extension/src/index.ts b/packages/javascript-kernel-extension/src/index.ts index dff4a76..e36d848 100644 --- a/packages/javascript-kernel-extension/src/index.ts +++ b/packages/javascript-kernel-extension/src/index.ts @@ -125,6 +125,16 @@ class JavaScriptKernelStartup implements IJavaScriptKernelStartup { */ trackKernel(kernel: JavaScriptKernel): void { this._kernels.add(kernel); + void Promise.all( + this._startupExtensions.map(extension => + kernel.applyStartupExtension(extension) + ) + ).catch(error => { + console.error( + '[javascript-kernel] Failed to apply startup extensions.', + error + ); + }); const untrackKernel = (sender: JavaScriptKernel): void => { this._kernels.delete(sender); sender.disposed.disconnect(untrackKernel); diff --git a/packages/javascript-kernel/src/kernel.ts b/packages/javascript-kernel/src/kernel.ts index 944b1bd..f36a1a8 100644 --- a/packages/javascript-kernel/src/kernel.ts +++ b/packages/javascript-kernel/src/kernel.ts @@ -533,7 +533,14 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { this._appliedStartupExtensions.add(extension.id); } catch (error) { for (const target of registeredTargets.reverse()) { - await context.unregisterCommTarget(target.targetName); + try { + await context.unregisterCommTarget(target.targetName); + } catch (rollbackError) { + console.warn( + `[javascript-kernel] Failed to rollback comm target "${target.targetName}" for startup extension "${extension.id}".`, + rollbackError + ); + } } throw error; } From d9520e8347ab4186953d82b298cedf71ab9c33e1 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Tue, 14 Jul 2026 16:48:59 +0530 Subject: [PATCH 4/5] apply suggestions --- README.md | 27 +++--- .../javascript-kernel-extension/package.json | 3 +- .../javascript-kernel-extension/src/index.ts | 94 ++++++++++--------- packages/javascript-kernel/src/kernel.ts | 33 +------ packages/javascript-kernel/src/startup.ts | 10 +- yarn.lock | 1 + 6 files changed, 76 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 0682a13..8cd4ff5 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,12 @@ targets without sending bootstrap code through `requestExecute`. ```typescript import type { JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { IJavaScriptKernelStartup } from '@jupyterlite/javascript-kernel'; +import { IJavaScriptKernelStartupRegistry } from '@jupyterlite/javascript-kernel'; const plugin: JupyterFrontEndPlugin = { id: 'my-extension:javascript-startup', autoStart: true, - requires: [IJavaScriptKernelStartup], + requires: [IJavaScriptKernelStartupRegistry], activate: (app, startup) => { const runtimeBootstrap = new URL( './runtime-bootstrap.js', @@ -66,24 +66,27 @@ const plugin: JupyterFrontEndPlugin = { startup.registerStartupExtension({ id: 'my-extension:lsp', - modulePreloads: [runtimeBootstrap], - commTargets: [ - { + activate: async context => { + await context.preloadModule(runtimeBootstrap); + await context.registerCommTarget({ targetName: 'my-extension:lsp', module: lspCommTarget, exportName: 'registerLspTarget' - } - ] + }); + }, + deactivate: async context => { + await context.unregisterCommTarget('my-extension:lsp'); + } }); } }; ``` -Each `commTargets` entry imports the module in the kernel runtime and passes -the exported handler to `Jupyter.comm.registerTarget(targetName, handler)`. -The default export is used when `exportName` is omitted. -Disposing a startup registration removes declared comm targets from active -kernels; use an optional `deactivate` callback for extra cleanup. +`context.registerCommTarget()` imports the module in the kernel runtime and +passes the exported handler to `Jupyter.comm.registerTarget(targetName, +handler)`. The default export is used when `exportName` is omitted. Disposing +a startup registration calls its optional `deactivate` callback for active +kernels. ### Worker mode limitations diff --git a/packages/javascript-kernel-extension/package.json b/packages/javascript-kernel-extension/package.json index 385dd56..7ea0241 100644 --- a/packages/javascript-kernel-extension/package.json +++ b/packages/javascript-kernel-extension/package.json @@ -42,7 +42,8 @@ "@jupyterlab/application": "^4.6.0", "@jupyterlite/javascript-kernel": "^0.4.0-alpha.4", "@jupyterlite/services": "^0.8.0", - "@lumino/disposable": "^2.1.5" + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5" }, "devDependencies": { "@jupyter/builder": "^1.0.2", diff --git a/packages/javascript-kernel-extension/src/index.ts b/packages/javascript-kernel-extension/src/index.ts index e36d848..324932e 100644 --- a/packages/javascript-kernel-extension/src/index.ts +++ b/packages/javascript-kernel-extension/src/index.ts @@ -11,9 +11,10 @@ import type { IKernel } from '@jupyterlite/services'; import { IKernelSpecs } from '@jupyterlite/services'; import { DisposableDelegate } from '@lumino/disposable'; import type { IDisposable } from '@lumino/disposable'; +import { Signal } from '@lumino/signaling'; import { - IJavaScriptKernelStartup, + IJavaScriptKernelStartupRegistry, JavaScriptKernel } from '@jupyterlite/javascript-kernel'; import type { RuntimeMode } from '@jupyterlite/javascript-kernel'; @@ -29,14 +30,13 @@ interface IRegisterKernelOptions { name: string; displayName: string; runtime: RuntimeMode; - startup: IJavaScriptKernelStartup; } const registerKernel = ( kernelspecs: IKernelSpecs, options: IRegisterKernelOptions ) => { - const { name, displayName, runtime, startup } = options; + const { name, displayName, runtime } = options; kernelspecs.register({ spec: { @@ -63,9 +63,9 @@ const registerKernel = ( const kernel = new JavaScriptKernel({ ...options, runtime, - startupExtensions: startup.startupExtensions + startupExtensions: Private.startupExtensions } as JavaScriptKernel.IOptions); - startup.trackKernel(kernel); + Private.kernelCreated.emit(kernel); return kernel; } }); @@ -74,15 +74,15 @@ const registerKernel = ( /** * In-memory registry for JavaScript kernel startup extensions. */ -class JavaScriptKernelStartup implements IJavaScriptKernelStartup { +class JavaScriptKernelStartupRegistry implements IJavaScriptKernelStartupRegistry { get startupExtensions(): readonly JavaScriptKernel.IStartupExtension[] { - return [...this._startupExtensions]; + return [...Private.startupExtensions]; } registerStartupExtension( extension: JavaScriptKernel.IStartupExtension ): IDisposable { - const existing = this._startupExtensions.findIndex( + const existing = Private.startupExtensions.findIndex( item => item.id === extension.id ); @@ -92,9 +92,11 @@ class JavaScriptKernelStartup implements IJavaScriptKernelStartup { ); } - this._startupExtensions.push(extension); + Private.startupExtensions.push(extension); void Promise.all( - [...this._kernels].map(kernel => kernel.applyStartupExtension(extension)) + [...Private.kernels].map(kernel => + kernel.applyStartupExtension(extension) + ) ).catch(error => { console.error( `[javascript-kernel] Failed to apply startup extension "${extension.id}".`, @@ -103,11 +105,11 @@ class JavaScriptKernelStartup implements IJavaScriptKernelStartup { }); return new DisposableDelegate(() => { - const index = this._startupExtensions.indexOf(extension); + const index = Private.startupExtensions.indexOf(extension); if (index !== -1) { - this._startupExtensions.splice(index, 1); + Private.startupExtensions.splice(index, 1); void Promise.all( - [...this._kernels].map(kernel => + [...Private.kernels].map(kernel => kernel.removeStartupExtension(extension) ) ).catch(error => { @@ -119,14 +121,25 @@ class JavaScriptKernelStartup implements IJavaScriptKernelStartup { } }); } +} + +namespace Private { + export const startupExtensions: JavaScriptKernel.IStartupExtension[] = []; + export const kernels = new Set(); + + const kernelCreatedOwner = {}; + export const kernelCreated = new Signal< + typeof kernelCreatedOwner, + JavaScriptKernel + >(kernelCreatedOwner); /** * Track an active JavaScript kernel for late startup registrations. */ - trackKernel(kernel: JavaScriptKernel): void { - this._kernels.add(kernel); + const trackKernel = (kernel: JavaScriptKernel): void => { + kernels.add(kernel); void Promise.all( - this._startupExtensions.map(extension => + startupExtensions.map(extension => kernel.applyStartupExtension(extension) ) ).catch(error => { @@ -136,25 +149,27 @@ class JavaScriptKernelStartup implements IJavaScriptKernelStartup { ); }); const untrackKernel = (sender: JavaScriptKernel): void => { - this._kernels.delete(sender); + kernels.delete(sender); sender.disposed.disconnect(untrackKernel); }; kernel.disposed.connect(untrackKernel); - } + }; - private _startupExtensions: JavaScriptKernel.IStartupExtension[] = []; - private _kernels = new Set(); + kernelCreated.connect((_sender, kernel) => { + trackKernel(kernel); + }); } /** * Plugin providing the JavaScript kernel startup extension registry. */ -const startupExtensions: JupyterFrontEndPlugin = { - id: '@jupyterlite/javascript-kernel-extension:startup-extensions', - autoStart: true, - provides: IJavaScriptKernelStartup, - activate: () => new JavaScriptKernelStartup() -}; +const startupExtensionsRegistry: JupyterFrontEndPlugin = + { + id: '@jupyterlite/javascript-kernel-extension:startup-extensions', + autoStart: true, + provides: IJavaScriptKernelStartupRegistry, + activate: () => new JavaScriptKernelStartupRegistry() + }; /** * Plugin registering the iframe JavaScript kernel. @@ -162,17 +177,12 @@ const startupExtensions: JupyterFrontEndPlugin = { const kernelIFrame: JupyterFrontEndPlugin = { id: '@jupyterlite/javascript-kernel-extension:kernel-iframe', autoStart: true, - requires: [IKernelSpecs, IJavaScriptKernelStartup], - activate: ( - app: JupyterFrontEnd, - kernelspecs: IKernelSpecs, - startup: IJavaScriptKernelStartup - ) => { + requires: [IKernelSpecs], + activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => { registerKernel(kernelspecs, { name: 'javascript', displayName: 'JavaScript (IFrame)', - runtime: 'iframe', - startup + runtime: 'iframe' }); } }; @@ -183,23 +193,19 @@ const kernelIFrame: JupyterFrontEndPlugin = { const kernelWorker: JupyterFrontEndPlugin = { id: '@jupyterlite/javascript-kernel-extension:kernel-worker', autoStart: true, - requires: [IKernelSpecs, IJavaScriptKernelStartup], - activate: ( - app: JupyterFrontEnd, - kernelspecs: IKernelSpecs, - startup: IJavaScriptKernelStartup - ) => { + requires: [IKernelSpecs], + activate: (app: JupyterFrontEnd, kernelspecs: IKernelSpecs) => { registerKernel(kernelspecs, { name: 'javascript-worker', displayName: 'JavaScript (Web Worker)', - runtime: 'worker', - startup + runtime: 'worker' }); } }; const plugins: Array< - JupyterFrontEndPlugin | JupyterFrontEndPlugin -> = [startupExtensions, kernelIFrame, kernelWorker]; + | JupyterFrontEndPlugin + | JupyterFrontEndPlugin +> = [startupExtensionsRegistry, kernelIFrame, kernelWorker]; export default plugins; diff --git a/packages/javascript-kernel/src/kernel.ts b/packages/javascript-kernel/src/kernel.ts index f36a1a8..a7785ca 100644 --- a/packages/javascript-kernel/src/kernel.ts +++ b/packages/javascript-kernel/src/kernel.ts @@ -299,9 +299,6 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { try { await extension.deactivate?.(context); } finally { - for (const target of extension.commTargets ?? []) { - await context.unregisterCommTarget(target.targetName); - } this._appliedStartupExtensions.delete(extension.id); } } @@ -520,30 +517,8 @@ export class JavaScriptKernel extends BaseKernel implements IKernel { if (this._appliedStartupExtensions.has(extension.id)) { return; } - const registeredTargets: JavaScriptKernel.IStartupCommTarget[] = []; - try { - for (const moduleName of extension.modulePreloads ?? []) { - await context.preloadModule(moduleName); - } - for (const target of extension.commTargets ?? []) { - await context.registerCommTarget(target); - registeredTargets.push(target); - } - await extension.activate?.(context); - this._appliedStartupExtensions.add(extension.id); - } catch (error) { - for (const target of registeredTargets.reverse()) { - try { - await context.unregisterCommTarget(target.targetName); - } catch (rollbackError) { - console.warn( - `[javascript-kernel] Failed to rollback comm target "${target.targetName}" for startup extension "${extension.id}".`, - rollbackError - ); - } - } - throw error; - } + await extension.activate(context); + this._appliedStartupExtensions.add(extension.id); } private _unsupportedControlMessages = new Set<'input_reply'>(); @@ -609,9 +584,7 @@ export namespace JavaScriptKernel { */ export interface IStartupExtension { id: string; - modulePreloads?: readonly string[]; - commTargets?: readonly IStartupCommTarget[]; - activate?: (context: IRuntimeReadyContext) => Promise | void; + activate: (context: IRuntimeReadyContext) => Promise | void; deactivate?: (context: IRuntimeReadyContext) => Promise | void; } diff --git a/packages/javascript-kernel/src/startup.ts b/packages/javascript-kernel/src/startup.ts index 9b0bcd6..8fb0275 100644 --- a/packages/javascript-kernel/src/startup.ts +++ b/packages/javascript-kernel/src/startup.ts @@ -9,17 +9,17 @@ import type { JavaScriptKernel } from './kernel'; /** * Registry for JavaScript kernel startup extensions. */ -export interface IJavaScriptKernelStartup { +export interface IJavaScriptKernelStartupRegistry { readonly startupExtensions: readonly JavaScriptKernel.IStartupExtension[]; registerStartupExtension( extension: JavaScriptKernel.IStartupExtension ): IDisposable; - trackKernel(kernel: JavaScriptKernel): void; } /** * Token for registering JavaScript kernel startup extensions. */ -export const IJavaScriptKernelStartup = new Token( - '@jupyterlite/javascript-kernel:IJavaScriptKernelStartup' -); +export const IJavaScriptKernelStartupRegistry = + new Token( + '@jupyterlite/javascript-kernel:IJavaScriptKernelStartupRegistry' + ); diff --git a/yarn.lock b/yarn.lock index deea86a..5138396 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2995,6 +2995,7 @@ __metadata: "@jupyterlite/javascript-kernel": ^0.4.0-alpha.4 "@jupyterlite/services": ^0.8.0 "@lumino/disposable": ^2.1.5 + "@lumino/signaling": ^2.1.5 npm-run-all2: ^7.0.1 rimraf: ~5.0.1 typescript: ~5.0.2 From 0dec7bcf7ea4dd741f9b9ae660771a05b1e40cc4 Mon Sep 17 00:00:00 2001 From: "anujkumar.singh" Date: Tue, 14 Jul 2026 17:11:18 +0530 Subject: [PATCH 5/5] remove any --- packages/javascript-kernel/src/executor.ts | 4 ++-- packages/javascript-kernel/src/runtime_evaluator.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/javascript-kernel/src/executor.ts b/packages/javascript-kernel/src/executor.ts index 385344f..49608be 100644 --- a/packages/javascript-kernel/src/executor.ts +++ b/packages/javascript-kernel/src/executor.ts @@ -295,12 +295,12 @@ export class JavaScriptExecutor { /** * Import a module using the executor's import source resolution. */ - async importModule(source: string): Promise> { + async importModule(source: string): Promise> { const importSource = this._transformImportSource(source); const importFunction = this._createScopedFunction( 'source', 'return import(source);' - ) as (source: string) => Promise>; + ) as (source: string) => Promise>; return importFunction.call(this._globalScope, importSource); } diff --git a/packages/javascript-kernel/src/runtime_evaluator.ts b/packages/javascript-kernel/src/runtime_evaluator.ts index 2f8c486..17119e9 100644 --- a/packages/javascript-kernel/src/runtime_evaluator.ts +++ b/packages/javascript-kernel/src/runtime_evaluator.ts @@ -6,6 +6,7 @@ import type { KernelMessage } from '@jupyterlab/services'; import { JavaScriptExecutor } from './executor'; import { normalizeError } from './errors'; import { CommManager } from './comm'; +import type { CommTargetHandler } from './comm'; import type { RuntimeOutputHandler } from './runtime_protocol'; import { Widget, createWidgetClasses } from './widgets'; @@ -182,7 +183,7 @@ export class JavaScriptRuntimeEvaluator { `Comm target ${targetName} must export a handler function` ); } - this._commManager.registerTarget(targetName, handler); + this._commManager.registerTarget(targetName, handler as CommTargetHandler); } /**