jupyterpack 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/lib/document/widgetFactory.js +4 -1
- package/lib/pythonServer/common/generatedPythonFiles.d.ts +2 -0
- package/lib/pythonServer/common/generatedPythonFiles.js +72 -0
- package/lib/pythonServer/dash/dashServer.d.ts +24 -0
- package/lib/pythonServer/dash/dashServer.js +39 -0
- package/lib/pythonServer/dash/generatedPythonFiles.d.ts +2 -0
- package/lib/pythonServer/dash/generatedPythonFiles.js +31 -0
- package/lib/pythonServer/index.d.ts +5 -0
- package/lib/pythonServer/index.js +9 -0
- package/lib/pythonServer/kernelExecutor.d.ts +66 -0
- package/lib/pythonServer/kernelExecutor.js +133 -0
- package/lib/pythonServer/streamlit/generatedPythonFiles.d.ts +2 -0
- package/lib/pythonServer/streamlit/generatedPythonFiles.js +147 -0
- package/lib/pythonServer/streamlit/streamlitServer.d.ts +33 -0
- package/lib/pythonServer/streamlit/streamlitServer.js +55 -0
- package/lib/pythonServer/tornado/generatedPythonFiles.d.ts +3 -0
- package/lib/pythonServer/tornado/generatedPythonFiles.js +456 -0
- package/lib/pythonServer/tornado/tornadoServer.d.ts +32 -0
- package/lib/pythonServer/tornado/tornadoServer.js +51 -0
- package/lib/pythonWidget/pythonWidget.d.ts +1 -0
- package/lib/pythonWidget/pythonWidget.js +9 -3
- package/lib/pythonWidget/pythonWidgetModel.d.ts +12 -3
- package/lib/pythonWidget/pythonWidgetModel.js +32 -10
- package/lib/swConnection/index.js +2 -2
- package/lib/{pythonWidget/connectionManager.d.ts → swConnection/mainConnectionManager.d.ts} +10 -0
- package/lib/swConnection/mainConnectionManager.js +93 -0
- package/lib/swConnection/sw.js +1 -1
- package/lib/swConnection/swCommManager.d.ts +11 -0
- package/lib/swConnection/{comm_manager.js → swCommManager.js} +5 -0
- package/lib/tools.d.ts +4 -0
- package/lib/tools.js +58 -0
- package/lib/type.d.ts +37 -2
- package/lib/type.js +2 -0
- package/lib/websocket/websocket.d.ts +0 -0
- package/lib/websocket/websocket.js +152 -0
- package/package.json +8 -5
- package/src/document/widgetFactory.ts +4 -1
- package/src/global.d.ts +4 -0
- package/src/pythonServer/common/generatedPythonFiles.ts +73 -0
- package/src/pythonServer/dash/dashServer.ts +57 -0
- package/src/pythonServer/dash/generatedPythonFiles.ts +32 -0
- package/src/pythonServer/index.ts +18 -0
- package/src/pythonServer/kernelExecutor.ts +229 -0
- package/src/pythonServer/streamlit/generatedPythonFiles.ts +148 -0
- package/src/pythonServer/streamlit/streamlitServer.ts +87 -0
- package/src/pythonServer/tornado/generatedPythonFiles.ts +457 -0
- package/src/pythonServer/tornado/tornadoServer.ts +80 -0
- package/src/pythonWidget/pythonWidget.ts +20 -3
- package/src/pythonWidget/pythonWidgetModel.ts +53 -19
- package/src/swConnection/index.ts +5 -2
- package/src/swConnection/mainConnectionManager.ts +121 -0
- package/src/swConnection/sw.ts +1 -1
- package/src/swConnection/{comm_manager.ts → swCommManager.ts} +6 -0
- package/src/tools.ts +69 -0
- package/src/type.ts +47 -3
- package/src/websocket/websocket.ts +216 -0
- package/lib/pythonWidget/connectionManager.js +0 -27
- package/lib/pythonWidget/kernelExecutor.d.ts +0 -27
- package/lib/pythonWidget/kernelExecutor.js +0 -104
- package/lib/swConnection/comm_manager.d.ts +0 -6
- package/lib/swConnection/connection_manager.d.ts +0 -18
- package/lib/swConnection/connection_manager.js +0 -27
- package/src/pythonWidget/connectionManager.ts +0 -43
- package/src/pythonWidget/kernelExecutor.ts +0 -140
- package/src/swConnection/connection_manager.ts +0 -43
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
1
2
|
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
2
|
-
import { IDisposable } from '@lumino/disposable';
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
Session,
|
|
4
|
+
Contents,
|
|
6
5
|
Kernel,
|
|
7
|
-
|
|
6
|
+
ServiceManager,
|
|
7
|
+
Session
|
|
8
8
|
} from '@jupyterlab/services';
|
|
9
9
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
10
|
+
import { IDisposable } from '@lumino/disposable';
|
|
11
|
+
|
|
12
|
+
import { PYTHON_SERVER } from '../pythonServer';
|
|
13
|
+
import {
|
|
14
|
+
IConnectionManager,
|
|
15
|
+
IJupyterPackFileFormat,
|
|
16
|
+
IKernelExecutor,
|
|
17
|
+
JupyterPackFramework
|
|
18
|
+
} from '../type';
|
|
13
19
|
|
|
14
20
|
export class PythonWidgetModel implements IDisposable {
|
|
15
21
|
constructor(options: PythonWidgetModel.IOptions) {
|
|
@@ -17,6 +23,7 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
17
23
|
this._manager = options.manager;
|
|
18
24
|
this._connectionManager = options.connectionManager;
|
|
19
25
|
this._contentsManager = options.contentsManager;
|
|
26
|
+
this._jpackModel = options.jpackModel;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
get isDisposed(): boolean {
|
|
@@ -25,19 +32,27 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
25
32
|
get connectionManager(): IConnectionManager {
|
|
26
33
|
return this._connectionManager;
|
|
27
34
|
}
|
|
28
|
-
async initialize(): Promise<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
async initialize(): Promise<
|
|
36
|
+
| {
|
|
37
|
+
success: true;
|
|
38
|
+
instanceId: string;
|
|
39
|
+
kernelClientId: string;
|
|
40
|
+
rootUrl: string;
|
|
41
|
+
framework: JupyterPackFramework;
|
|
42
|
+
}
|
|
43
|
+
| { success: false; error: string }
|
|
44
|
+
> {
|
|
32
45
|
if (this._kernelStarted) {
|
|
33
|
-
return
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: 'Server is called twice'
|
|
49
|
+
};
|
|
34
50
|
}
|
|
35
51
|
const filePath = this._context.localPath;
|
|
36
|
-
const spkContent =
|
|
37
|
-
this._context.model.toJSON() as any as IJupyterPackFileFormat;
|
|
52
|
+
const spkContent = this._jpackModel;
|
|
38
53
|
|
|
39
54
|
const entryPath = PathExt.join(PathExt.dirname(filePath), spkContent.entry);
|
|
40
|
-
|
|
55
|
+
const rootUrl = spkContent.rootUrl ?? '/';
|
|
41
56
|
const entryContent = await this._contentsManager.get(entryPath, {
|
|
42
57
|
content: true,
|
|
43
58
|
format: 'text'
|
|
@@ -47,7 +62,10 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
47
62
|
await this._manager.kernelspecs.ready;
|
|
48
63
|
const specs = this._manager.kernelspecs.specs;
|
|
49
64
|
if (!specs) {
|
|
50
|
-
return
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: 'Missing kernel spec'
|
|
68
|
+
};
|
|
51
69
|
}
|
|
52
70
|
const { kernelspecs } = specs;
|
|
53
71
|
let kernelName = Object.keys(kernelspecs)[0];
|
|
@@ -63,12 +81,21 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
63
81
|
},
|
|
64
82
|
type: 'notebook'
|
|
65
83
|
});
|
|
66
|
-
const
|
|
84
|
+
const framework = spkContent.framework;
|
|
85
|
+
const ServerClass = PYTHON_SERVER.get(framework);
|
|
86
|
+
if (!ServerClass) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: `Framework "${framework}" is not supported. Please check your .spk file.`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const executor = (this._executor = new ServerClass({
|
|
67
93
|
sessionConnection: this._sessionConnection
|
|
68
|
-
});
|
|
94
|
+
}));
|
|
69
95
|
const data = await this._connectionManager.registerConnection(executor);
|
|
70
96
|
await executor.init({
|
|
71
97
|
initCode: entryContent.content,
|
|
98
|
+
entryPath: spkContent.entry,
|
|
72
99
|
...data
|
|
73
100
|
});
|
|
74
101
|
const finish = new PromiseDelegate<void>();
|
|
@@ -82,9 +109,13 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
82
109
|
|
|
83
110
|
await finish.promise;
|
|
84
111
|
this._kernelStarted = true;
|
|
85
|
-
return data;
|
|
112
|
+
return { ...data, rootUrl, framework, success: true };
|
|
86
113
|
}
|
|
87
114
|
dispose(): void {
|
|
115
|
+
if (this._isDisposed) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
void this._executor?.disposePythonServer();
|
|
88
119
|
this._isDisposed = true;
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -95,10 +126,13 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
95
126
|
private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
96
127
|
private _connectionManager: IConnectionManager;
|
|
97
128
|
private _contentsManager: Contents.IManager;
|
|
129
|
+
private _jpackModel: IJupyterPackFileFormat;
|
|
130
|
+
private _executor?: IKernelExecutor;
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
export namespace PythonWidgetModel {
|
|
101
134
|
export interface IOptions {
|
|
135
|
+
jpackModel: IJupyterPackFileFormat;
|
|
102
136
|
context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
103
137
|
manager: ServiceManager.IManager;
|
|
104
138
|
connectionManager: IConnectionManager;
|
|
@@ -8,7 +8,7 @@ import { expose } from 'comlink';
|
|
|
8
8
|
|
|
9
9
|
import { IConnectionManagerToken } from '../token';
|
|
10
10
|
import { IConnectionManager, MessageAction } from '../type';
|
|
11
|
-
import { ConnectionManager } from './
|
|
11
|
+
import { ConnectionManager } from './mainConnectionManager';
|
|
12
12
|
|
|
13
13
|
const fullLabextensionsUrl = PageConfig.getOption('fullLabextensionsUrl');
|
|
14
14
|
const SCOPE = `${fullLabextensionsUrl}/jupyterpack/static`;
|
|
@@ -66,7 +66,6 @@ export const swPlugin: JupyterFrontEndPlugin<IConnectionManager> = {
|
|
|
66
66
|
autoStart: true,
|
|
67
67
|
provides: IConnectionManagerToken,
|
|
68
68
|
activate: async (app: JupyterFrontEnd): Promise<IConnectionManager> => {
|
|
69
|
-
console.log('Activating jupyterpack service worker');
|
|
70
69
|
const serviceWorker = await initServiceWorker();
|
|
71
70
|
if (!serviceWorker) {
|
|
72
71
|
throw new Error(
|
|
@@ -75,6 +74,10 @@ export const swPlugin: JupyterFrontEndPlugin<IConnectionManager> = {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
const instanceId = UUID.uuid4();
|
|
77
|
+
console.log(
|
|
78
|
+
'Activating jupyterpack service worker with instance id',
|
|
79
|
+
instanceId
|
|
80
|
+
);
|
|
78
81
|
const { port1: mainToServiceWorker, port2: serviceWorkerToMain } =
|
|
79
82
|
new MessageChannel();
|
|
80
83
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { arrayBufferToBase64 } from '../tools';
|
|
2
|
+
import {
|
|
3
|
+
IBroadcastMessage,
|
|
4
|
+
IConnectionManager,
|
|
5
|
+
IDict,
|
|
6
|
+
IKernelExecutor
|
|
7
|
+
} from '../type';
|
|
8
|
+
import { UUID } from '@lumino/coreutils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages connections between clients and kernel executors.
|
|
12
|
+
* This class handles the registration of kernel executors and the generation of responses
|
|
13
|
+
* for client requests. It maintains a mapping of kernel client IDs to their respective executors.
|
|
14
|
+
* The HTTP requests intercepted by the service worker are forwarded to the appropriate kernel executor.
|
|
15
|
+
* The websocket messages forwarded from the broadcast channel are also forwarded to the appropriate kernel executor.
|
|
16
|
+
* It's running on the main thread
|
|
17
|
+
*/
|
|
18
|
+
export class ConnectionManager implements IConnectionManager {
|
|
19
|
+
constructor(public instanceId: string) {
|
|
20
|
+
this._wsBroadcastChannel = new BroadcastChannel(
|
|
21
|
+
`/jupyterpack/ws/${instanceId}`
|
|
22
|
+
);
|
|
23
|
+
this._initWsChannel();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async registerConnection(
|
|
27
|
+
kernelExecutor: IKernelExecutor
|
|
28
|
+
): Promise<{ instanceId: string; kernelClientId: string }> {
|
|
29
|
+
const uuid = UUID.uuid4();
|
|
30
|
+
|
|
31
|
+
this._kernelExecutors.set(uuid, kernelExecutor);
|
|
32
|
+
|
|
33
|
+
return { instanceId: this.instanceId, kernelClientId: uuid };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async generateResponse(options: {
|
|
37
|
+
kernelClientId: string;
|
|
38
|
+
urlPath: string;
|
|
39
|
+
method: string;
|
|
40
|
+
headers: IDict;
|
|
41
|
+
requestBody?: ArrayBuffer;
|
|
42
|
+
params?: string;
|
|
43
|
+
}): Promise<IDict | null> {
|
|
44
|
+
const { urlPath, kernelClientId, method, params, requestBody, headers } =
|
|
45
|
+
options;
|
|
46
|
+
const executor = this._kernelExecutors.get(kernelClientId);
|
|
47
|
+
if (!executor) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await executor.getResponse({
|
|
52
|
+
urlPath,
|
|
53
|
+
method,
|
|
54
|
+
params,
|
|
55
|
+
headers,
|
|
56
|
+
requestBody
|
|
57
|
+
});
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
private _initWsChannel() {
|
|
61
|
+
this._wsBroadcastChannel.onmessage = event => {
|
|
62
|
+
const rawData = event.data;
|
|
63
|
+
let data: IBroadcastMessage;
|
|
64
|
+
if (typeof rawData === 'string') {
|
|
65
|
+
data = JSON.parse(rawData);
|
|
66
|
+
} else {
|
|
67
|
+
data = rawData;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { action, dest, wsUrl, payload } = data;
|
|
71
|
+
const executor = this._kernelExecutors.get(dest);
|
|
72
|
+
if (!executor) {
|
|
73
|
+
console.error(
|
|
74
|
+
'Missing kernel handle for message',
|
|
75
|
+
data,
|
|
76
|
+
dest,
|
|
77
|
+
this._kernelExecutors
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switch (action) {
|
|
83
|
+
case 'open': {
|
|
84
|
+
executor.openWebsocket({
|
|
85
|
+
instanceId: this.instanceId,
|
|
86
|
+
kernelId: dest,
|
|
87
|
+
wsUrl,
|
|
88
|
+
protocol: payload.protocol
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'send': {
|
|
93
|
+
let serializedData: string;
|
|
94
|
+
let isBinary: boolean;
|
|
95
|
+
if (payload instanceof ArrayBuffer || ArrayBuffer.isView(payload)) {
|
|
96
|
+
// Convert data to base64 string
|
|
97
|
+
serializedData = arrayBufferToBase64(payload as any);
|
|
98
|
+
isBinary = true;
|
|
99
|
+
} else if (typeof payload === 'string') {
|
|
100
|
+
serializedData = payload;
|
|
101
|
+
isBinary = false;
|
|
102
|
+
} else {
|
|
103
|
+
console.error('Unknown message type', payload);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
executor.sendWebsocketMessage({
|
|
107
|
+
instanceId: this.instanceId,
|
|
108
|
+
kernelId: dest,
|
|
109
|
+
wsUrl,
|
|
110
|
+
message: JSON.stringify({ isBinary, data: serializedData })
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
private _kernelExecutors = new Map<string, IKernelExecutor>();
|
|
120
|
+
private _wsBroadcastChannel: BroadcastChannel;
|
|
121
|
+
}
|
package/src/swConnection/sw.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { IConnectionManager, IDict } from '../type';
|
|
2
2
|
import { wrap, transfer } from 'comlink';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages communication between different components using Comlink's MessagePort-based communication.
|
|
6
|
+
* This class handles registration of communication channels and processing of requests.
|
|
7
|
+
* It's running on the service worker thread
|
|
8
|
+
*/
|
|
3
9
|
export class CommManager {
|
|
4
10
|
constructor() {}
|
|
5
11
|
registerComm(instanceId: string, port: MessagePort): void {
|
package/src/tools.ts
CHANGED
|
@@ -16,3 +16,72 @@ export function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
|
16
16
|
}
|
|
17
17
|
return btoa(binary);
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
export function base64ToArrayBuffer(base64: string): Uint8Array {
|
|
21
|
+
const binaryString = atob(base64);
|
|
22
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
23
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
24
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
25
|
+
}
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function base64ToString(base64: string): string {
|
|
30
|
+
const bytes = base64ToArrayBuffer(base64);
|
|
31
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function stringOrNone(content?: string) {
|
|
35
|
+
return content ? `"${content}"` : 'None';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isBinaryContentType(contentType?: string) {
|
|
39
|
+
if (!contentType) {
|
|
40
|
+
// no Content-Type → assume binary for safety
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
contentType = contentType.toLowerCase().trim();
|
|
45
|
+
|
|
46
|
+
const textTypes = [
|
|
47
|
+
'text/',
|
|
48
|
+
'application/json',
|
|
49
|
+
'application/javascript',
|
|
50
|
+
'application/xml',
|
|
51
|
+
'application/xhtml+xml',
|
|
52
|
+
'application/x-www-form-urlencoded',
|
|
53
|
+
'application/sql',
|
|
54
|
+
'application/graphql',
|
|
55
|
+
'application/yaml'
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const binaryIndicators = [
|
|
59
|
+
'image/',
|
|
60
|
+
'audio/',
|
|
61
|
+
'video/',
|
|
62
|
+
'font/',
|
|
63
|
+
'application/octet-stream',
|
|
64
|
+
'application/pdf',
|
|
65
|
+
'application/zip',
|
|
66
|
+
'application/x-protobuf',
|
|
67
|
+
'application/vnd'
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// Starts with text/ or one of the textual types
|
|
71
|
+
if (textTypes.some(t => contentType.startsWith(t))) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Starts with binary-indicating prefix
|
|
76
|
+
if (binaryIndicators.some(t => contentType.startsWith(t))) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If charset is specified → text
|
|
81
|
+
if (contentType.includes('charset=')) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Default: assume binary
|
|
86
|
+
return true;
|
|
87
|
+
}
|
package/src/type.ts
CHANGED
|
@@ -5,15 +5,32 @@ export interface IDict<T = any> {
|
|
|
5
5
|
[key: string]: T;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface IBroadcastMessage {
|
|
9
|
+
action:
|
|
10
|
+
| 'message'
|
|
11
|
+
| 'open'
|
|
12
|
+
| 'close'
|
|
13
|
+
| 'error'
|
|
14
|
+
| 'send'
|
|
15
|
+
| 'connected'
|
|
16
|
+
| 'backend_message';
|
|
17
|
+
dest: string;
|
|
18
|
+
wsUrl: string;
|
|
19
|
+
payload?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export enum JupyterPackFramework {
|
|
9
23
|
REACT = 'react',
|
|
10
|
-
DASH = 'dash'
|
|
24
|
+
DASH = 'dash',
|
|
25
|
+
STREAMLIT = 'streamlit',
|
|
26
|
+
TORNADO = 'tornado'
|
|
11
27
|
}
|
|
12
28
|
export interface IJupyterPackFileFormat {
|
|
13
29
|
entry: string;
|
|
14
30
|
framework: JupyterPackFramework;
|
|
15
31
|
name?: string;
|
|
16
32
|
metadata?: IDict;
|
|
33
|
+
rootUrl?: string;
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
export enum MessageAction {
|
|
@@ -29,9 +46,36 @@ export interface IKernelExecutorParams {
|
|
|
29
46
|
}
|
|
30
47
|
export interface IKernelExecutor extends IDisposable {
|
|
31
48
|
getResponse(options: IKernelExecutorParams): Promise<IDict>;
|
|
49
|
+
openWebsocket(options: {
|
|
50
|
+
instanceId: string;
|
|
51
|
+
kernelId: string;
|
|
52
|
+
wsUrl: string;
|
|
53
|
+
protocol?: string;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
sendWebsocketMessage(options: {
|
|
56
|
+
instanceId: string;
|
|
57
|
+
kernelId: string;
|
|
58
|
+
wsUrl: string;
|
|
59
|
+
message: string;
|
|
60
|
+
}): Promise<void>;
|
|
32
61
|
executeCode(
|
|
33
|
-
code: KernelMessage.IExecuteRequestMsg['content']
|
|
34
|
-
|
|
62
|
+
code: KernelMessage.IExecuteRequestMsg['content'],
|
|
63
|
+
waitForResult?: boolean
|
|
64
|
+
): Promise<string | null>;
|
|
65
|
+
init(options: {
|
|
66
|
+
entryPath?: string;
|
|
67
|
+
initCode?: string;
|
|
68
|
+
instanceId: string;
|
|
69
|
+
kernelClientId: string;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
disposePythonServer(): Promise<void>;
|
|
72
|
+
getResponseFunctionFactory(options: {
|
|
73
|
+
urlPath: string;
|
|
74
|
+
method: string;
|
|
75
|
+
headers: IDict;
|
|
76
|
+
params?: string;
|
|
77
|
+
content?: string;
|
|
78
|
+
}): string;
|
|
35
79
|
}
|
|
36
80
|
|
|
37
81
|
export interface IConnectionManager {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const urlPath = new URL(window.location.href).pathname;
|
|
3
|
+
const pathAfterExtensionName: string | undefined = urlPath.split(
|
|
4
|
+
'/jupyterpack/static'
|
|
5
|
+
)[1];
|
|
6
|
+
const pathList = pathAfterExtensionName?.split('/').filter(Boolean);
|
|
7
|
+
const instanceId = pathList?.[0];
|
|
8
|
+
const kernelClientId = pathList?.[2];
|
|
9
|
+
if (!instanceId || !kernelClientId) {
|
|
10
|
+
throw new Error('Missing instance Id or kernelClient Id');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface IBroadcastMessage {
|
|
14
|
+
action:
|
|
15
|
+
| 'message'
|
|
16
|
+
| 'open'
|
|
17
|
+
| 'close'
|
|
18
|
+
| 'error'
|
|
19
|
+
| 'send'
|
|
20
|
+
| 'connected'
|
|
21
|
+
| 'backend_message';
|
|
22
|
+
dest: string;
|
|
23
|
+
wsUrl: string;
|
|
24
|
+
payload?: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sendTypedMessage = (msg: Omit<IBroadcastMessage, 'dest'>) => {
|
|
28
|
+
bcWsChannel.postMessage({ ...msg, dest: kernelClientId });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function base64ToBinary(base64: string, dataType: BinaryType) {
|
|
32
|
+
const binary = atob(base64); // decode base64
|
|
33
|
+
const len = binary.length;
|
|
34
|
+
const bytes = new Uint8Array(len);
|
|
35
|
+
for (let i = 0; i < len; i++) {
|
|
36
|
+
bytes[i] = binary.charCodeAt(i);
|
|
37
|
+
}
|
|
38
|
+
if (dataType === 'arraybuffer') {
|
|
39
|
+
return bytes.buffer;
|
|
40
|
+
} else if (dataType === 'blob') {
|
|
41
|
+
return new Blob([bytes], { type: 'application/octet-stream' });
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error("Unsupported type: use 'arraybuffer' or 'blob'");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const decodeServerMessage = (
|
|
48
|
+
payload: {
|
|
49
|
+
data: string;
|
|
50
|
+
isBinary: boolean;
|
|
51
|
+
},
|
|
52
|
+
binaryType: BinaryType
|
|
53
|
+
) => {
|
|
54
|
+
const { data, isBinary } = payload;
|
|
55
|
+
if (isBinary) {
|
|
56
|
+
// Decode base64 string to array buffer or blob
|
|
57
|
+
|
|
58
|
+
return base64ToBinary(data, binaryType);
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
};
|
|
62
|
+
const bcWsChannel = new BroadcastChannel(`/jupyterpack/ws/${instanceId}`);
|
|
63
|
+
|
|
64
|
+
class BroadcastChannelWebSocket implements WebSocket {
|
|
65
|
+
constructor(url: string | URL, protocols?: string | string[]) {
|
|
66
|
+
const urlObj = new URL(url);
|
|
67
|
+
this.url = urlObj.pathname + urlObj.search + urlObj.hash;
|
|
68
|
+
|
|
69
|
+
if (protocols) {
|
|
70
|
+
this.protocol = Array.isArray(protocols)
|
|
71
|
+
? protocols.join(',')
|
|
72
|
+
: protocols;
|
|
73
|
+
} else {
|
|
74
|
+
this.protocol = '';
|
|
75
|
+
}
|
|
76
|
+
this.binaryType = 'blob';
|
|
77
|
+
this.bufferedAmount = 0;
|
|
78
|
+
this.extensions = '';
|
|
79
|
+
|
|
80
|
+
this.readyState = this.CONNECTING;
|
|
81
|
+
this._open();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onclose: ((this: WebSocket, ev?: CloseEvent) => any) | null = () => {
|
|
85
|
+
// no-op
|
|
86
|
+
};
|
|
87
|
+
onerror: ((this: WebSocket, ev: Event) => any) | null = () => {
|
|
88
|
+
// no-op
|
|
89
|
+
};
|
|
90
|
+
onmessage:
|
|
91
|
+
| ((this: WebSocket, ev: MessageEvent | { data: any }) => any)
|
|
92
|
+
| null = () => {
|
|
93
|
+
// no-op
|
|
94
|
+
};
|
|
95
|
+
onopen: ((this: WebSocket, ev: Event | { data: any }) => any) | null =
|
|
96
|
+
() => {
|
|
97
|
+
// no-op
|
|
98
|
+
};
|
|
99
|
+
close(code?: unknown, reason?: unknown): void {
|
|
100
|
+
if (this.readyState === this.CLOSED) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.onclose) {
|
|
105
|
+
this.onclose();
|
|
106
|
+
}
|
|
107
|
+
while (this._eventHandlers['close'].length) {
|
|
108
|
+
const cb = this._eventHandlers['close'].pop();
|
|
109
|
+
cb();
|
|
110
|
+
}
|
|
111
|
+
this._eventHandlers['close'] = [];
|
|
112
|
+
bcWsChannel.removeEventListener('message', this._bcMessageHandler);
|
|
113
|
+
|
|
114
|
+
this.readyState = this.CLOSED;
|
|
115
|
+
}
|
|
116
|
+
send(data: unknown): void {
|
|
117
|
+
sendTypedMessage({
|
|
118
|
+
action: 'send',
|
|
119
|
+
payload: data,
|
|
120
|
+
wsUrl: this.url
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
addEventListener(
|
|
125
|
+
type: 'message' | 'open' | 'close' | 'error',
|
|
126
|
+
listener: unknown,
|
|
127
|
+
options?: unknown
|
|
128
|
+
): void {
|
|
129
|
+
this._eventHandlers[type].push(listener);
|
|
130
|
+
}
|
|
131
|
+
removeEventListener(
|
|
132
|
+
type: 'message' | 'open' | 'close' | 'error',
|
|
133
|
+
listener: unknown,
|
|
134
|
+
options?: unknown
|
|
135
|
+
): void {
|
|
136
|
+
this._eventHandlers[type] = this._eventHandlers[type].filter(
|
|
137
|
+
handler => handler !== listener
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dispatchEvent(event: unknown): boolean {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
readyState: number;
|
|
145
|
+
url: string;
|
|
146
|
+
protocol: string;
|
|
147
|
+
binaryType: BinaryType;
|
|
148
|
+
bufferedAmount: number;
|
|
149
|
+
extensions: string;
|
|
150
|
+
|
|
151
|
+
readonly CONNECTING = 0;
|
|
152
|
+
readonly OPEN = 1;
|
|
153
|
+
readonly CLOSING = 2;
|
|
154
|
+
readonly CLOSED = 3;
|
|
155
|
+
|
|
156
|
+
private _eventHandlers: {
|
|
157
|
+
message: any[];
|
|
158
|
+
open: any[];
|
|
159
|
+
close: any[];
|
|
160
|
+
error: any[];
|
|
161
|
+
} = { message: [], open: [], close: [], error: [] };
|
|
162
|
+
|
|
163
|
+
private _bcMessageHandler = async (event: MessageEvent) => {
|
|
164
|
+
const rawData = event.data;
|
|
165
|
+
let data: IBroadcastMessage;
|
|
166
|
+
if (typeof rawData === 'string') {
|
|
167
|
+
data = JSON.parse(rawData);
|
|
168
|
+
} else {
|
|
169
|
+
data = rawData;
|
|
170
|
+
}
|
|
171
|
+
const { action, dest, wsUrl, payload } = data;
|
|
172
|
+
if (dest !== kernelClientId || wsUrl !== this.url) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
switch (action) {
|
|
176
|
+
case 'connected': {
|
|
177
|
+
this.readyState = this.OPEN;
|
|
178
|
+
|
|
179
|
+
if (this.onopen) {
|
|
180
|
+
this.onopen(event);
|
|
181
|
+
}
|
|
182
|
+
this._eventHandlers.open.forEach(handler =>
|
|
183
|
+
handler({ data: payload })
|
|
184
|
+
);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case 'backend_message': {
|
|
189
|
+
const decoded = decodeServerMessage(payload, this.binaryType);
|
|
190
|
+
if (this.onmessage) {
|
|
191
|
+
this.onmessage({ data: decoded });
|
|
192
|
+
}
|
|
193
|
+
this._eventHandlers.message.forEach(handler =>
|
|
194
|
+
handler({ data: decoded })
|
|
195
|
+
);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
private _open = () => {
|
|
203
|
+
sendTypedMessage({
|
|
204
|
+
action: 'open',
|
|
205
|
+
payload: {
|
|
206
|
+
protocol: this.protocol
|
|
207
|
+
},
|
|
208
|
+
wsUrl: this.url
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
bcWsChannel.addEventListener('message', this._bcMessageHandler);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
window.WebSocket = BroadcastChannelWebSocket as any;
|
|
216
|
+
})();
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { UUID } from '@lumino/coreutils';
|
|
2
|
-
export class ConnectionManager {
|
|
3
|
-
constructor(instanceId) {
|
|
4
|
-
this.instanceId = instanceId;
|
|
5
|
-
this._kernelExecutors = new Map();
|
|
6
|
-
}
|
|
7
|
-
async registerConnection(kernelExecutor) {
|
|
8
|
-
const uuid = UUID.uuid4();
|
|
9
|
-
this._kernelExecutors.set(uuid, kernelExecutor);
|
|
10
|
-
return { instanceId: this.instanceId, kernelClientId: uuid };
|
|
11
|
-
}
|
|
12
|
-
async generateResponse(options) {
|
|
13
|
-
const { urlPath, kernelClientId, method, params, requestBody, headers } = options;
|
|
14
|
-
const executor = this._kernelExecutors.get(kernelClientId);
|
|
15
|
-
if (!executor) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
const response = await executor.getResponse({
|
|
19
|
-
urlPath,
|
|
20
|
-
method,
|
|
21
|
-
params,
|
|
22
|
-
headers,
|
|
23
|
-
requestBody
|
|
24
|
-
});
|
|
25
|
-
return response;
|
|
26
|
-
}
|
|
27
|
-
}
|