jupyterpack 0.2.1 → 0.4.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/commands.d.ts +8 -0
- package/lib/document/commands.js +76 -0
- package/lib/document/iframePanel.d.ts +4 -1
- package/lib/document/plugin.d.ts +2 -1
- package/lib/document/plugin.js +22 -4
- package/lib/document/toolbar.d.ts +9 -0
- package/lib/document/toolbar.js +20 -0
- package/lib/document/widgetFactory.d.ts +2 -1
- package/lib/document/widgetFactory.js +15 -6
- package/lib/index.d.ts +1 -1
- package/lib/pythonServer/dash/dashServer.d.ts +22 -0
- package/lib/pythonServer/dash/dashServer.js +47 -0
- package/lib/pythonServer/index.d.ts +5 -0
- package/lib/pythonServer/index.js +9 -0
- package/lib/pythonServer/kernelExecutor.d.ts +71 -0
- package/lib/pythonServer/kernelExecutor.js +140 -0
- package/lib/pythonServer/streamlit/streamlitServer.d.ts +14 -0
- package/lib/pythonServer/streamlit/streamlitServer.js +51 -0
- package/lib/pythonServer/tornado/tornadoServer.d.ts +34 -0
- package/lib/pythonServer/tornado/tornadoServer.js +68 -0
- package/lib/pythonWidget/comm.d.ts +11 -0
- package/lib/pythonWidget/comm.js +52 -0
- package/lib/pythonWidget/pythonWidget.d.ts +6 -0
- package/lib/pythonWidget/pythonWidget.js +28 -3
- package/lib/pythonWidget/pythonWidgetModel.d.ts +27 -6
- package/lib/pythonWidget/pythonWidgetModel.js +101 -15
- package/lib/sandpackWidget/sandpackFilesModel.d.ts +6 -2
- package/lib/sandpackWidget/sandpackFilesModel.js +13 -2
- package/lib/sandpackWidget/sandpackPanel.d.ts +10 -1
- package/lib/sandpackWidget/sandpackPanel.js +38 -3
- 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/token.d.ts +2 -1
- package/lib/token.js +1 -0
- package/lib/tools.d.ts +9 -0
- package/lib/tools.js +75 -0
- package/lib/type.d.ts +64 -3
- 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/commands.ts +91 -0
- package/src/document/iframePanel.ts +4 -1
- package/src/document/plugin.ts +28 -7
- package/src/document/toolbar.ts +39 -0
- package/src/document/widgetFactory.ts +17 -6
- package/src/global.d.ts +9 -0
- package/src/pythonServer/dash/dashServer.ts +66 -0
- package/src/pythonServer/index.ts +18 -0
- package/src/pythonServer/kernelExecutor.ts +243 -0
- package/src/pythonServer/streamlit/streamlitServer.ts +67 -0
- package/src/pythonServer/tornado/tornadoServer.ts +97 -0
- package/src/pythonWidget/comm.ts +65 -0
- package/src/pythonWidget/pythonWidget.ts +38 -3
- package/src/pythonWidget/pythonWidgetModel.ts +155 -34
- package/src/sandpackWidget/sandpackFilesModel.ts +17 -3
- package/src/sandpackWidget/sandpackPanel.ts +45 -4
- 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/token.ts +5 -1
- package/src/tools.ts +91 -0
- package/src/type.ts +76 -4
- package/src/websocket/websocket.ts +216 -0
- package/style/base.css +7 -0
- package/style/icons/autoreload.svg +16 -0
- package/style/icons/box.svg +12 -0
- package/style/icons/externallink.svg +10 -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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Kernel, KernelMessage } from '@jupyterlab/services';
|
|
2
|
+
|
|
3
|
+
const COMM_NAME = 'jupyterpack:broadcast:comm';
|
|
4
|
+
|
|
5
|
+
export class CommBroadcastManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._kernels = new Map();
|
|
8
|
+
this._comms = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
registerKernel(kernel: Kernel.IKernelConnection) {
|
|
12
|
+
this._kernels.set(kernel.id, kernel);
|
|
13
|
+
kernel.registerCommTarget(COMM_NAME, (comm, msg) =>
|
|
14
|
+
this._handle_comm_open(comm, msg, kernel.id)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
unregisterKernel(kernelId?: string) {
|
|
18
|
+
if (kernelId) {
|
|
19
|
+
this._kernels.delete(kernelId);
|
|
20
|
+
const comms = this._comms.get(kernelId) ?? [];
|
|
21
|
+
comms.forEach(comm => comm.dispose());
|
|
22
|
+
this._comms.delete(kernelId);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private _handle_comm_open = async (
|
|
27
|
+
comm: Kernel.IComm,
|
|
28
|
+
msg: KernelMessage.ICommOpenMsg,
|
|
29
|
+
kernelId: string
|
|
30
|
+
): Promise<void> => {
|
|
31
|
+
if (this._comms.has(kernelId)) {
|
|
32
|
+
this._comms.get(kernelId)?.push(comm);
|
|
33
|
+
} else {
|
|
34
|
+
this._comms.set(kernelId, [comm]);
|
|
35
|
+
}
|
|
36
|
+
const channelName = msg.metadata.channel_name as string | undefined;
|
|
37
|
+
if (!channelName) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!this._broadcastChannels.has(channelName)) {
|
|
41
|
+
this._broadcastChannels.set(
|
|
42
|
+
channelName,
|
|
43
|
+
new BroadcastChannel(channelName)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const broadcastChannel = this._broadcastChannels.get(channelName)!;
|
|
47
|
+
comm.onMsg = commMsg => {
|
|
48
|
+
const { data } = commMsg.content;
|
|
49
|
+
broadcastChannel.postMessage(data);
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
dispose() {
|
|
54
|
+
this._kernels.clear();
|
|
55
|
+
this._comms.clear();
|
|
56
|
+
this._broadcastChannels.forEach(it => {
|
|
57
|
+
it.close();
|
|
58
|
+
});
|
|
59
|
+
this._broadcastChannels.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _kernels: Map<string, Kernel.IKernelConnection> = new Map();
|
|
63
|
+
private _comms: Map<string, Kernel.IComm[]> = new Map();
|
|
64
|
+
private _broadcastChannels: Map<string, BroadcastChannel> = new Map();
|
|
65
|
+
}
|
|
@@ -1,29 +1,64 @@
|
|
|
1
1
|
import { PythonWidgetModel } from './pythonWidgetModel';
|
|
2
|
-
import { PageConfig } from '@jupyterlab/coreutils';
|
|
2
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
3
3
|
import { IFramePanel } from '../document/iframePanel';
|
|
4
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
4
5
|
|
|
5
6
|
export class PythonWidget extends IFramePanel {
|
|
6
7
|
constructor(options: PythonWidget.IOptions) {
|
|
7
8
|
super();
|
|
8
9
|
this._model = options.model;
|
|
9
10
|
this._model.initialize().then(connectionData => {
|
|
10
|
-
if (!connectionData) {
|
|
11
|
+
if (!connectionData.success) {
|
|
12
|
+
this.toggleSpinner(false);
|
|
13
|
+
this._iframe.contentDocument!.body.innerText = `Failed to start server: ${connectionData.error}`;
|
|
11
14
|
return;
|
|
12
15
|
}
|
|
16
|
+
this._isReady.resolve();
|
|
13
17
|
const iframe = this._iframe;
|
|
14
18
|
const fullLabextensionsUrl = PageConfig.getOption('fullLabextensionsUrl');
|
|
15
|
-
|
|
19
|
+
|
|
20
|
+
const iframeUrl = URLExt.join(
|
|
21
|
+
fullLabextensionsUrl,
|
|
22
|
+
'jupyterpack/static',
|
|
23
|
+
connectionData.instanceId,
|
|
24
|
+
connectionData.framework,
|
|
25
|
+
connectionData.kernelClientId,
|
|
26
|
+
connectionData.rootUrl
|
|
27
|
+
);
|
|
28
|
+
iframe.src = iframeUrl;
|
|
29
|
+
|
|
16
30
|
iframe.addEventListener('load', () => {
|
|
17
31
|
this.toggleSpinner(false);
|
|
18
32
|
});
|
|
33
|
+
this._model.serverReloaded.connect(() => {
|
|
34
|
+
this._iframe?.contentWindow?.location?.reload();
|
|
35
|
+
});
|
|
19
36
|
});
|
|
20
37
|
}
|
|
21
38
|
|
|
39
|
+
get autoreload() {
|
|
40
|
+
return this._model.autoreload;
|
|
41
|
+
}
|
|
42
|
+
set autoreload(value: boolean) {
|
|
43
|
+
this._model.autoreload = value;
|
|
44
|
+
}
|
|
45
|
+
get isReady(): Promise<void> {
|
|
46
|
+
return this._isReady.promise;
|
|
47
|
+
}
|
|
22
48
|
get model(): PythonWidgetModel {
|
|
23
49
|
return this._model;
|
|
24
50
|
}
|
|
25
51
|
|
|
52
|
+
async reload(): Promise<void> {
|
|
53
|
+
await this._model.reload();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
dispose(): void {
|
|
57
|
+
this._model.dispose();
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
private _model: PythonWidgetModel;
|
|
61
|
+
private _isReady = new PromiseDelegate<void>();
|
|
27
62
|
}
|
|
28
63
|
|
|
29
64
|
export namespace PythonWidget {
|
|
@@ -1,53 +1,98 @@
|
|
|
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 { IConnectionManager, IJupyterPackFileFormat } from '../type';
|
|
11
|
-
import { KernelExecutor } from './kernelExecutor';
|
|
12
|
-
import { PathExt } from '@jupyterlab/coreutils';
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
import { PYTHON_SERVER } from '../pythonServer';
|
|
12
|
+
import { Signal } from '@lumino/signaling';
|
|
13
|
+
import {
|
|
14
|
+
IConnectionManager,
|
|
15
|
+
IJupyterPackFileFormat,
|
|
16
|
+
IKernelExecutor,
|
|
17
|
+
IPythonWidgetModel,
|
|
18
|
+
JupyterPackFramework
|
|
19
|
+
} from '../type';
|
|
20
|
+
import { CommBroadcastManager } from './comm';
|
|
21
|
+
import { IS_LITE } from '../tools';
|
|
22
|
+
|
|
23
|
+
export class PythonWidgetModel implements IPythonWidgetModel {
|
|
15
24
|
constructor(options: PythonWidgetModel.IOptions) {
|
|
16
25
|
this._context = options.context;
|
|
17
26
|
this._manager = options.manager;
|
|
18
27
|
this._connectionManager = options.connectionManager;
|
|
19
28
|
this._contentsManager = options.contentsManager;
|
|
29
|
+
this._jpackModel = options.jpackModel;
|
|
30
|
+
this._localPath = PathExt.dirname(this._context.localPath);
|
|
31
|
+
this._autoreload = Boolean(this._jpackModel?.metadata?.autoreload);
|
|
32
|
+
|
|
33
|
+
this._contentsManager.fileChanged.connect(this._onFileChanged, this);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get autoreload() {
|
|
37
|
+
return this._autoreload;
|
|
20
38
|
}
|
|
21
39
|
|
|
40
|
+
set autoreload(val: boolean) {
|
|
41
|
+
this._autoreload = val;
|
|
42
|
+
}
|
|
22
43
|
get isDisposed(): boolean {
|
|
23
44
|
return this._isDisposed;
|
|
24
45
|
}
|
|
25
46
|
get connectionManager(): IConnectionManager {
|
|
26
47
|
return this._connectionManager;
|
|
27
48
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
49
|
+
get serverReloaded() {
|
|
50
|
+
return this._serverReloaded;
|
|
51
|
+
}
|
|
52
|
+
get kernelStatusChanged() {
|
|
53
|
+
return this._kernelStatusChanged;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async reload() {
|
|
57
|
+
if (!this._kernelStarted) {
|
|
58
|
+
return;
|
|
34
59
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
const { spkContent, entryContent } = await this._loadData();
|
|
61
|
+
await this._executor?.reloadPythonServer({
|
|
62
|
+
entryPath: spkContent.entry,
|
|
63
|
+
initCode: entryContent.content
|
|
64
|
+
});
|
|
65
|
+
this._serverReloaded.emit();
|
|
66
|
+
}
|
|
38
67
|
|
|
39
|
-
|
|
68
|
+
async initialize(): Promise<
|
|
69
|
+
| {
|
|
70
|
+
success: true;
|
|
71
|
+
instanceId: string;
|
|
72
|
+
kernelClientId: string;
|
|
73
|
+
rootUrl: string;
|
|
74
|
+
framework: JupyterPackFramework;
|
|
75
|
+
}
|
|
76
|
+
| { success: false; error: string }
|
|
77
|
+
> {
|
|
78
|
+
if (this._kernelStarted) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'Server is called twice'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
40
84
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
format: 'text'
|
|
44
|
-
});
|
|
85
|
+
const { filePath, spkContent, rootUrl, entryContent } =
|
|
86
|
+
await this._loadData();
|
|
45
87
|
const sessionManager = this._manager.sessions;
|
|
46
88
|
await sessionManager.ready;
|
|
47
89
|
await this._manager.kernelspecs.ready;
|
|
48
90
|
const specs = this._manager.kernelspecs.specs;
|
|
49
91
|
if (!specs) {
|
|
50
|
-
return
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: 'Missing kernel spec'
|
|
95
|
+
};
|
|
51
96
|
}
|
|
52
97
|
const { kernelspecs } = specs;
|
|
53
98
|
let kernelName = Object.keys(kernelspecs)[0];
|
|
@@ -55,20 +100,44 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
55
100
|
kernelName = specs.default;
|
|
56
101
|
}
|
|
57
102
|
|
|
58
|
-
this._sessionConnection = await sessionManager.startNew(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
this._sessionConnection = await sessionManager.startNew(
|
|
104
|
+
{
|
|
105
|
+
name: filePath,
|
|
106
|
+
path: filePath,
|
|
107
|
+
kernel: {
|
|
108
|
+
name: kernelName
|
|
109
|
+
},
|
|
110
|
+
type: 'notebook'
|
|
63
111
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
112
|
+
{
|
|
113
|
+
kernelConnectionOptions: { handleComms: true }
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
const kernel = this._sessionConnection.kernel;
|
|
117
|
+
if (kernel) {
|
|
118
|
+
this._kernelStatusChanged.emit('started');
|
|
119
|
+
this._commBroadcastManager.registerKernel(kernel);
|
|
120
|
+
kernel.disposed.connect(() => {
|
|
121
|
+
this._kernelStatusChanged.emit('stopped');
|
|
122
|
+
this._commBroadcastManager.unregisterKernel(kernel.id);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const framework = spkContent.framework;
|
|
127
|
+
const ServerClass = PYTHON_SERVER.get(framework);
|
|
128
|
+
if (!ServerClass) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: `Framework "${framework}" is not supported. Please check your .spk file.`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const executor = (this._executor = new ServerClass({
|
|
67
135
|
sessionConnection: this._sessionConnection
|
|
68
|
-
});
|
|
136
|
+
}));
|
|
69
137
|
const data = await this._connectionManager.registerConnection(executor);
|
|
70
138
|
await executor.init({
|
|
71
139
|
initCode: entryContent.content,
|
|
140
|
+
entryPath: spkContent.entry,
|
|
72
141
|
...data
|
|
73
142
|
});
|
|
74
143
|
const finish = new PromiseDelegate<void>();
|
|
@@ -82,12 +151,50 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
82
151
|
|
|
83
152
|
await finish.promise;
|
|
84
153
|
this._kernelStarted = true;
|
|
85
|
-
return data;
|
|
154
|
+
return { ...data, rootUrl, framework, success: true };
|
|
86
155
|
}
|
|
87
|
-
dispose(): void {
|
|
156
|
+
async dispose(): Promise<void> {
|
|
157
|
+
if (this._isDisposed) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!IS_LITE) {
|
|
161
|
+
this._sessionConnection?.kernel?.shutdown();
|
|
162
|
+
}
|
|
163
|
+
void this._executor?.disposePythonServer();
|
|
164
|
+
this._contentsManager.fileChanged.disconnect(this._onFileChanged);
|
|
165
|
+
this._commBroadcastManager.dispose();
|
|
88
166
|
this._isDisposed = true;
|
|
89
167
|
}
|
|
90
168
|
|
|
169
|
+
private async _loadData() {
|
|
170
|
+
const filePath = this._context.localPath;
|
|
171
|
+
const spkContent = this._jpackModel;
|
|
172
|
+
|
|
173
|
+
const entryPath = PathExt.join(PathExt.dirname(filePath), spkContent.entry);
|
|
174
|
+
const rootUrl = spkContent.rootUrl ?? '/';
|
|
175
|
+
const entryContent = await this._contentsManager.get(entryPath, {
|
|
176
|
+
content: true,
|
|
177
|
+
format: 'text'
|
|
178
|
+
});
|
|
179
|
+
return { filePath, spkContent, rootUrl, entryContent };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async _onFileChanged(
|
|
183
|
+
sender: Contents.IManager,
|
|
184
|
+
args: Contents.IChangedArgs
|
|
185
|
+
) {
|
|
186
|
+
if (this._autoreload && args.type === 'save') {
|
|
187
|
+
if (
|
|
188
|
+
args.newValue?.path &&
|
|
189
|
+
args.newValue.path.startsWith(this._localPath)
|
|
190
|
+
) {
|
|
191
|
+
await this.reload();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private _commBroadcastManager = new CommBroadcastManager();
|
|
197
|
+
|
|
91
198
|
private _isDisposed = false;
|
|
92
199
|
private _kernelStarted = false;
|
|
93
200
|
private _sessionConnection: Session.ISessionConnection | undefined;
|
|
@@ -95,10 +202,24 @@ export class PythonWidgetModel implements IDisposable {
|
|
|
95
202
|
private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
96
203
|
private _connectionManager: IConnectionManager;
|
|
97
204
|
private _contentsManager: Contents.IManager;
|
|
205
|
+
private _jpackModel: IJupyterPackFileFormat;
|
|
206
|
+
private _executor?: IKernelExecutor;
|
|
207
|
+
private _localPath: string;
|
|
208
|
+
|
|
209
|
+
private _serverReloaded: Signal<IPythonWidgetModel, void> = new Signal<
|
|
210
|
+
IPythonWidgetModel,
|
|
211
|
+
void
|
|
212
|
+
>(this);
|
|
213
|
+
private _kernelStatusChanged: Signal<
|
|
214
|
+
IPythonWidgetModel,
|
|
215
|
+
'started' | 'stopped'
|
|
216
|
+
> = new Signal(this);
|
|
217
|
+
private _autoreload: boolean;
|
|
98
218
|
}
|
|
99
219
|
|
|
100
220
|
export namespace PythonWidgetModel {
|
|
101
221
|
export interface IOptions {
|
|
222
|
+
jpackModel: IJupyterPackFileFormat;
|
|
102
223
|
context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
103
224
|
manager: ServiceManager.IManager;
|
|
104
225
|
connectionManager: IConnectionManager;
|
|
@@ -3,16 +3,17 @@ import { ISignal, Signal } from '@lumino/signaling';
|
|
|
3
3
|
|
|
4
4
|
import { IDict } from '../type';
|
|
5
5
|
import { removePrefix } from '../tools';
|
|
6
|
+
import { IDisposable } from '@lumino/disposable';
|
|
6
7
|
|
|
7
|
-
export class SandpackFilesModel {
|
|
8
|
+
export class SandpackFilesModel implements IDisposable {
|
|
8
9
|
constructor(options: { contentsManager: Contents.IManager; path: string }) {
|
|
9
10
|
this._contentManager = options.contentsManager;
|
|
10
11
|
this._path = options.path;
|
|
11
12
|
this._contentManager.fileChanged.connect(this._onFileChanged, this);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
async getAllFiles(): Promise<IDict<{ code: string }>> {
|
|
15
|
-
if (!this._allFiles) {
|
|
15
|
+
async getAllFiles(force = false): Promise<IDict<{ code: string }>> {
|
|
16
|
+
if (!this._allFiles || force) {
|
|
16
17
|
const files = await this._contentManager.get(this._path, {
|
|
17
18
|
content: true
|
|
18
19
|
});
|
|
@@ -21,6 +22,9 @@ export class SandpackFilesModel {
|
|
|
21
22
|
|
|
22
23
|
return this._allFiles;
|
|
23
24
|
}
|
|
25
|
+
get isDisposed(): boolean {
|
|
26
|
+
return this._isDisposed;
|
|
27
|
+
}
|
|
24
28
|
|
|
25
29
|
get fileChanged(): ISignal<
|
|
26
30
|
SandpackFilesModel,
|
|
@@ -29,6 +33,14 @@ export class SandpackFilesModel {
|
|
|
29
33
|
return this._fileChanged;
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
dispose(): void {
|
|
37
|
+
if (this._isDisposed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this._isDisposed = true;
|
|
41
|
+
this._contentManager.fileChanged.disconnect(this._onFileChanged);
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
async flattenDirectory(
|
|
33
45
|
dirContent: Contents.IModel
|
|
34
46
|
): Promise<IDict<{ code: string }>> {
|
|
@@ -138,4 +150,6 @@ export class SandpackFilesModel {
|
|
|
138
150
|
private _contentManager: Contents.IManager;
|
|
139
151
|
|
|
140
152
|
private _allFiles?: IDict<{ code: string }>;
|
|
153
|
+
|
|
154
|
+
private _isDisposed = false;
|
|
141
155
|
}
|
|
@@ -7,8 +7,9 @@ import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
|
7
7
|
import { Contents } from '@jupyterlab/services';
|
|
8
8
|
|
|
9
9
|
import { IFramePanel } from '../document/iframePanel';
|
|
10
|
-
import { IDict } from '../type';
|
|
10
|
+
import { IDict, IJupyterPackFileFormat } from '../type';
|
|
11
11
|
import { SandpackFilesModel } from './sandpackFilesModel';
|
|
12
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
12
13
|
|
|
13
14
|
export class SandpackPanel extends IFramePanel {
|
|
14
15
|
constructor(options: {
|
|
@@ -17,17 +18,44 @@ export class SandpackPanel extends IFramePanel {
|
|
|
17
18
|
}) {
|
|
18
19
|
super();
|
|
19
20
|
this._contentsManager = options.contentsManager;
|
|
21
|
+
const { context } = options;
|
|
20
22
|
options.context.ready.then(async () => {
|
|
21
|
-
|
|
23
|
+
const jpackModel =
|
|
24
|
+
context.model.toJSON() as any as IJupyterPackFileFormat;
|
|
25
|
+
await this.init(context.localPath, jpackModel);
|
|
22
26
|
});
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
dispose(): void {
|
|
30
|
+
this._fileModel?.fileChanged.disconnect(this._onFileChanged);
|
|
31
|
+
this._fileModel?.dispose();
|
|
32
|
+
this._spClient?.destroy();
|
|
33
|
+
super.dispose();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isReady() {
|
|
37
|
+
return this._isReady.promise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get autoreload(): boolean {
|
|
41
|
+
return this._autoreload;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set autoreload(val: boolean) {
|
|
45
|
+
this._autoreload = val;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async init(localPath: string, jpackModel: IJupyterPackFileFormat) {
|
|
49
|
+
if (jpackModel?.metadata?.autoreload === true) {
|
|
50
|
+
this._autoreload = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
const currentDir = localPath.split('/').slice(0, -1).join('/');
|
|
27
54
|
const filesModel = new SandpackFilesModel({
|
|
28
55
|
contentsManager: this._contentsManager,
|
|
29
56
|
path: currentDir
|
|
30
57
|
});
|
|
58
|
+
this._fileModel = filesModel;
|
|
31
59
|
const allFiles = await filesModel.getAllFiles();
|
|
32
60
|
|
|
33
61
|
const options: ClientOptions = {
|
|
@@ -43,6 +71,7 @@ export class SandpackPanel extends IFramePanel {
|
|
|
43
71
|
options
|
|
44
72
|
);
|
|
45
73
|
await this.connectSignals(filesModel, this._spClient);
|
|
74
|
+
this._isReady.resolve();
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
async connectSignals(
|
|
@@ -66,11 +95,20 @@ export class SandpackPanel extends IFramePanel {
|
|
|
66
95
|
});
|
|
67
96
|
}
|
|
68
97
|
|
|
98
|
+
async reload(): Promise<void> {
|
|
99
|
+
if (this._spClient && this._fileModel) {
|
|
100
|
+
const allFiles = await this._fileModel.getAllFiles();
|
|
101
|
+
this._spClient.updateSandbox({
|
|
102
|
+
files: allFiles
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
private _onFileChanged(
|
|
70
108
|
sender: SandpackFilesModel,
|
|
71
109
|
{ allFiles }: { allFiles: IDict<{ code: string }> }
|
|
72
110
|
) {
|
|
73
|
-
if (this._spClient) {
|
|
111
|
+
if (this._autoreload && this._spClient) {
|
|
74
112
|
this._spClient.updateSandbox({
|
|
75
113
|
files: allFiles
|
|
76
114
|
});
|
|
@@ -79,4 +117,7 @@ export class SandpackPanel extends IFramePanel {
|
|
|
79
117
|
|
|
80
118
|
private _spClient?: SandpackClient;
|
|
81
119
|
private _contentsManager: Contents.IManager;
|
|
120
|
+
private _fileModel: SandpackFilesModel | undefined;
|
|
121
|
+
private _autoreload = false;
|
|
122
|
+
private _isReady = new PromiseDelegate<void>();
|
|
82
123
|
}
|
|
@@ -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
|
+
}
|