jupyterpack 0.2.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/LICENSE +29 -0
- package/README.md +79 -0
- package/lib/document/iframePanel.d.ts +7 -0
- package/lib/document/iframePanel.js +20 -0
- package/lib/document/jupyterpackDocWidget.d.ts +9 -0
- package/lib/document/jupyterpackDocWidget.js +16 -0
- package/lib/document/plugin.d.ts +2 -0
- package/lib/document/plugin.js +31 -0
- package/lib/document/widgetFactory.d.ts +23 -0
- package/lib/document/widgetFactory.js +60 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/pythonWidget/connectionManager.d.ts +18 -0
- package/lib/pythonWidget/connectionManager.js +27 -0
- package/lib/pythonWidget/kernelExecutor.d.ts +27 -0
- package/lib/pythonWidget/kernelExecutor.js +104 -0
- package/lib/pythonWidget/pythonWidget.d.ts +13 -0
- package/lib/pythonWidget/pythonWidget.js +22 -0
- package/lib/pythonWidget/pythonWidgetModel.d.ts +29 -0
- package/lib/pythonWidget/pythonWidgetModel.js +75 -0
- package/lib/sandpackWidget/sandpackFilesModel.d.ts +27 -0
- package/lib/sandpackWidget/sandpackFilesModel.js +114 -0
- package/lib/sandpackWidget/sandpackPanel.d.ts +16 -0
- package/lib/sandpackWidget/sandpackPanel.js +52 -0
- package/lib/swConnection/comm_manager.d.ts +6 -0
- package/lib/swConnection/comm_manager.js +46 -0
- package/lib/swConnection/connection_manager.d.ts +18 -0
- package/lib/swConnection/connection_manager.js +27 -0
- package/lib/swConnection/index.d.ts +3 -0
- package/lib/swConnection/index.js +68 -0
- package/lib/swConnection/sw.d.ts +1 -0
- package/lib/swConnection/sw.js +49 -0
- package/lib/token.d.ts +3 -0
- package/lib/token.js +2 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +17 -0
- package/lib/type.d.ts +38 -0
- package/lib/type.js +9 -0
- package/package.json +199 -0
- package/src/document/iframePanel.ts +25 -0
- package/src/document/jupyterpackDocWidget.ts +19 -0
- package/src/document/plugin.ts +44 -0
- package/src/document/widgetFactory.ts +79 -0
- package/src/index.ts +4 -0
- package/src/pythonWidget/connectionManager.ts +43 -0
- package/src/pythonWidget/kernelExecutor.ts +140 -0
- package/src/pythonWidget/pythonWidget.ts +34 -0
- package/src/pythonWidget/pythonWidgetModel.ts +107 -0
- package/src/sandpackWidget/sandpackFilesModel.ts +141 -0
- package/src/sandpackWidget/sandpackPanel.ts +82 -0
- package/src/swConnection/comm_manager.ts +53 -0
- package/src/swConnection/connection_manager.ts +43 -0
- package/src/swConnection/index.ts +93 -0
- package/src/swConnection/sw.ts +57 -0
- package/src/token.ts +6 -0
- package/src/tools.ts +18 -0
- package/src/type.ts +44 -0
- package/style/base.css +51 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
2
|
+
import { IDisposable } from '@lumino/disposable';
|
|
3
|
+
import {
|
|
4
|
+
ServiceManager,
|
|
5
|
+
Session,
|
|
6
|
+
Kernel,
|
|
7
|
+
Contents
|
|
8
|
+
} from '@jupyterlab/services';
|
|
9
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
10
|
+
import { IConnectionManager, IJupyterPackFileFormat } from '../type';
|
|
11
|
+
import { KernelExecutor } from './kernelExecutor';
|
|
12
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
13
|
+
|
|
14
|
+
export class PythonWidgetModel implements IDisposable {
|
|
15
|
+
constructor(options: PythonWidgetModel.IOptions) {
|
|
16
|
+
this._context = options.context;
|
|
17
|
+
this._manager = options.manager;
|
|
18
|
+
this._connectionManager = options.connectionManager;
|
|
19
|
+
this._contentsManager = options.contentsManager;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get isDisposed(): boolean {
|
|
23
|
+
return this._isDisposed;
|
|
24
|
+
}
|
|
25
|
+
get connectionManager(): IConnectionManager {
|
|
26
|
+
return this._connectionManager;
|
|
27
|
+
}
|
|
28
|
+
async initialize(): Promise<{
|
|
29
|
+
instanceId: string;
|
|
30
|
+
kernelClientId: string;
|
|
31
|
+
} | null> {
|
|
32
|
+
if (this._kernelStarted) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const filePath = this._context.localPath;
|
|
36
|
+
const spkContent =
|
|
37
|
+
this._context.model.toJSON() as any as IJupyterPackFileFormat;
|
|
38
|
+
|
|
39
|
+
const entryPath = PathExt.join(PathExt.dirname(filePath), spkContent.entry);
|
|
40
|
+
|
|
41
|
+
const entryContent = await this._contentsManager.get(entryPath, {
|
|
42
|
+
content: true,
|
|
43
|
+
format: 'text'
|
|
44
|
+
});
|
|
45
|
+
const sessionManager = this._manager.sessions;
|
|
46
|
+
await sessionManager.ready;
|
|
47
|
+
await this._manager.kernelspecs.ready;
|
|
48
|
+
const specs = this._manager.kernelspecs.specs;
|
|
49
|
+
if (!specs) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const { kernelspecs } = specs;
|
|
53
|
+
let kernelName = Object.keys(kernelspecs)[0];
|
|
54
|
+
if (kernelspecs[specs.default]) {
|
|
55
|
+
kernelName = specs.default;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this._sessionConnection = await sessionManager.startNew({
|
|
59
|
+
name: filePath,
|
|
60
|
+
path: filePath,
|
|
61
|
+
kernel: {
|
|
62
|
+
name: kernelName
|
|
63
|
+
},
|
|
64
|
+
type: 'notebook'
|
|
65
|
+
});
|
|
66
|
+
const executor = new KernelExecutor({
|
|
67
|
+
sessionConnection: this._sessionConnection
|
|
68
|
+
});
|
|
69
|
+
const data = await this._connectionManager.registerConnection(executor);
|
|
70
|
+
await executor.init({
|
|
71
|
+
initCode: entryContent.content,
|
|
72
|
+
...data
|
|
73
|
+
});
|
|
74
|
+
const finish = new PromiseDelegate<void>();
|
|
75
|
+
const cb = (_: Kernel.IKernelConnection, status: Kernel.Status) => {
|
|
76
|
+
if (status === 'idle') {
|
|
77
|
+
this._sessionConnection!.kernel?.statusChanged.disconnect(cb);
|
|
78
|
+
finish.resolve();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
this._sessionConnection.kernel?.statusChanged.connect(cb);
|
|
82
|
+
|
|
83
|
+
await finish.promise;
|
|
84
|
+
this._kernelStarted = true;
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
dispose(): void {
|
|
88
|
+
this._isDisposed = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _isDisposed = false;
|
|
92
|
+
private _kernelStarted = false;
|
|
93
|
+
private _sessionConnection: Session.ISessionConnection | undefined;
|
|
94
|
+
private _manager: ServiceManager.IManager;
|
|
95
|
+
private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
96
|
+
private _connectionManager: IConnectionManager;
|
|
97
|
+
private _contentsManager: Contents.IManager;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export namespace PythonWidgetModel {
|
|
101
|
+
export interface IOptions {
|
|
102
|
+
context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
103
|
+
manager: ServiceManager.IManager;
|
|
104
|
+
connectionManager: IConnectionManager;
|
|
105
|
+
contentsManager: Contents.IManager;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Contents } from '@jupyterlab/services';
|
|
2
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
3
|
+
|
|
4
|
+
import { IDict } from '../type';
|
|
5
|
+
import { removePrefix } from '../tools';
|
|
6
|
+
|
|
7
|
+
export class SandpackFilesModel {
|
|
8
|
+
constructor(options: { contentsManager: Contents.IManager; path: string }) {
|
|
9
|
+
this._contentManager = options.contentsManager;
|
|
10
|
+
this._path = options.path;
|
|
11
|
+
this._contentManager.fileChanged.connect(this._onFileChanged, this);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getAllFiles(): Promise<IDict<{ code: string }>> {
|
|
15
|
+
if (!this._allFiles) {
|
|
16
|
+
const files = await this._contentManager.get(this._path, {
|
|
17
|
+
content: true
|
|
18
|
+
});
|
|
19
|
+
this._allFiles = await this.flattenDirectory(files);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return this._allFiles;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get fileChanged(): ISignal<
|
|
26
|
+
SandpackFilesModel,
|
|
27
|
+
{ allFiles: IDict<{ code: string }> }
|
|
28
|
+
> {
|
|
29
|
+
return this._fileChanged;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async flattenDirectory(
|
|
33
|
+
dirContent: Contents.IModel
|
|
34
|
+
): Promise<IDict<{ code: string }>> {
|
|
35
|
+
const flatDict: IDict<{ code: string }> = {};
|
|
36
|
+
const content = dirContent.content as Contents.IModel[];
|
|
37
|
+
|
|
38
|
+
for (const item of content) {
|
|
39
|
+
if (item.type === 'file') {
|
|
40
|
+
const pathWithoutRoot = this._removeRoot(item.path);
|
|
41
|
+
let itemContent = item;
|
|
42
|
+
if (!itemContent.content) {
|
|
43
|
+
itemContent = await this._getContent(item.path);
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
itemContent.mimetype === 'application/json' &&
|
|
47
|
+
typeof itemContent.content !== 'string'
|
|
48
|
+
) {
|
|
49
|
+
flatDict[pathWithoutRoot] = {
|
|
50
|
+
code: JSON.stringify(itemContent.content)
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
flatDict[pathWithoutRoot] = { code: itemContent.content };
|
|
54
|
+
}
|
|
55
|
+
} else if (item.type === 'directory') {
|
|
56
|
+
// If it's a directory, recursively flatten its content
|
|
57
|
+
const nestedContent = await this._getContent(item.path);
|
|
58
|
+
const nestedDict = await this.flattenDirectory(nestedContent);
|
|
59
|
+
Object.assign(flatDict, nestedDict);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return flatDict;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async _onFileChanged(
|
|
66
|
+
sender: Contents.IManager,
|
|
67
|
+
args: Contents.IChangedArgs
|
|
68
|
+
) {
|
|
69
|
+
switch (args.type) {
|
|
70
|
+
case 'save': {
|
|
71
|
+
if (args.newValue?.path) {
|
|
72
|
+
const newContent = await this._getContent(args.newValue.path);
|
|
73
|
+
const pathWithoutRoot = this._removeRoot(args.newValue.path);
|
|
74
|
+
if (this._allFiles) {
|
|
75
|
+
this._allFiles[pathWithoutRoot] = {
|
|
76
|
+
code: newContent.content
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'delete': {
|
|
83
|
+
if (args.oldValue?.path) {
|
|
84
|
+
const pathWithoutRoot = this._removeRoot(args.oldValue.path);
|
|
85
|
+
if (this._allFiles) {
|
|
86
|
+
delete this._allFiles[pathWithoutRoot];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case 'rename': {
|
|
92
|
+
if (args.oldValue?.path && args.newValue?.path) {
|
|
93
|
+
const oldPathWithoutRoot = this._removeRoot(args.oldValue.path);
|
|
94
|
+
const newPathWithoutRoot = this._removeRoot(args.newValue.path);
|
|
95
|
+
if (this._allFiles) {
|
|
96
|
+
this._allFiles[newPathWithoutRoot] =
|
|
97
|
+
this._allFiles[oldPathWithoutRoot];
|
|
98
|
+
delete this._allFiles[newPathWithoutRoot];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'new': {
|
|
104
|
+
if (args.newValue?.path) {
|
|
105
|
+
const newContent = await this._getContent(args.newValue.path);
|
|
106
|
+
const pathWithoutRoot = this._removeRoot(args.newValue.path);
|
|
107
|
+
if (this._allFiles) {
|
|
108
|
+
this._allFiles[pathWithoutRoot] = {
|
|
109
|
+
code: newContent.content
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (this._allFiles) {
|
|
120
|
+
this._fileChanged.emit({
|
|
121
|
+
allFiles: this._allFiles
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _removeRoot(path: string): string {
|
|
127
|
+
return removePrefix(path, this._path);
|
|
128
|
+
}
|
|
129
|
+
private _getContent(path: string): Promise<Contents.IModel> {
|
|
130
|
+
return this._contentManager.get(path, { content: true });
|
|
131
|
+
}
|
|
132
|
+
private _path: string;
|
|
133
|
+
private _fileChanged = new Signal<
|
|
134
|
+
this,
|
|
135
|
+
{ allFiles: IDict<{ code: string }> }
|
|
136
|
+
>(this);
|
|
137
|
+
|
|
138
|
+
private _contentManager: Contents.IManager;
|
|
139
|
+
|
|
140
|
+
private _allFiles?: IDict<{ code: string }>;
|
|
141
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientOptions,
|
|
3
|
+
loadSandpackClient,
|
|
4
|
+
SandpackClient
|
|
5
|
+
} from '@codesandbox/sandpack-client';
|
|
6
|
+
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
7
|
+
import { Contents } from '@jupyterlab/services';
|
|
8
|
+
|
|
9
|
+
import { IFramePanel } from '../document/iframePanel';
|
|
10
|
+
import { IDict } from '../type';
|
|
11
|
+
import { SandpackFilesModel } from './sandpackFilesModel';
|
|
12
|
+
|
|
13
|
+
export class SandpackPanel extends IFramePanel {
|
|
14
|
+
constructor(options: {
|
|
15
|
+
context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
16
|
+
contentsManager: Contents.IManager;
|
|
17
|
+
}) {
|
|
18
|
+
super();
|
|
19
|
+
this._contentsManager = options.contentsManager;
|
|
20
|
+
options.context.ready.then(async () => {
|
|
21
|
+
await this.init(options.context.localPath);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init(localPath: string) {
|
|
26
|
+
const currentDir = localPath.split('/').slice(0, -1).join('/');
|
|
27
|
+
const filesModel = new SandpackFilesModel({
|
|
28
|
+
contentsManager: this._contentsManager,
|
|
29
|
+
path: currentDir
|
|
30
|
+
});
|
|
31
|
+
const allFiles = await filesModel.getAllFiles();
|
|
32
|
+
|
|
33
|
+
const options: ClientOptions = {
|
|
34
|
+
showLoadingScreen: true,
|
|
35
|
+
showOpenInCodeSandbox: false
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this._spClient = await loadSandpackClient(
|
|
39
|
+
this._iframe,
|
|
40
|
+
{
|
|
41
|
+
files: allFiles
|
|
42
|
+
},
|
|
43
|
+
options
|
|
44
|
+
);
|
|
45
|
+
await this.connectSignals(filesModel, this._spClient);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async connectSignals(
|
|
49
|
+
filesModel: SandpackFilesModel,
|
|
50
|
+
sandpackClient: SandpackClient
|
|
51
|
+
) {
|
|
52
|
+
filesModel.fileChanged.connect(this._onFileChanged, this);
|
|
53
|
+
sandpackClient.listen(msg => {
|
|
54
|
+
switch (msg.type) {
|
|
55
|
+
case 'start': {
|
|
56
|
+
this.toggleSpinner(true);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'success': {
|
|
60
|
+
this.toggleSpinner(false);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private _onFileChanged(
|
|
70
|
+
sender: SandpackFilesModel,
|
|
71
|
+
{ allFiles }: { allFiles: IDict<{ code: string }> }
|
|
72
|
+
) {
|
|
73
|
+
if (this._spClient) {
|
|
74
|
+
this._spClient.updateSandbox({
|
|
75
|
+
files: allFiles
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private _spClient?: SandpackClient;
|
|
81
|
+
private _contentsManager: Contents.IManager;
|
|
82
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IConnectionManager, IDict } from '../type';
|
|
2
|
+
import { wrap, transfer } from 'comlink';
|
|
3
|
+
export class CommManager {
|
|
4
|
+
constructor() {}
|
|
5
|
+
registerComm(instanceId: string, port: MessagePort): void {
|
|
6
|
+
const comm = wrap<Omit<IConnectionManager, 'registerConnection'>>(port);
|
|
7
|
+
|
|
8
|
+
this._commIds.set(instanceId, comm);
|
|
9
|
+
}
|
|
10
|
+
async generateResponse(request: Request): Promise<Response> {
|
|
11
|
+
const url = new URL(request.url);
|
|
12
|
+
const urlPath = url.pathname;
|
|
13
|
+
const method = request.method;
|
|
14
|
+
const requestHeaders: IDict = {};
|
|
15
|
+
|
|
16
|
+
for (const pair of request.headers.entries()) {
|
|
17
|
+
if (!pair[0].startsWith('sec-ch-ua')) {
|
|
18
|
+
requestHeaders[pair[0]] = pair[1];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const params = url.searchParams.toString();
|
|
22
|
+
const pathAfterExtensionName = urlPath.split('/jupyterpack/static')[1];
|
|
23
|
+
const pathList = pathAfterExtensionName.split('/').filter(Boolean);
|
|
24
|
+
const instanceId = pathList[0];
|
|
25
|
+
const kernelClientId = pathList[2];
|
|
26
|
+
|
|
27
|
+
const comm = this._commIds.get(instanceId);
|
|
28
|
+
if (!comm) {
|
|
29
|
+
throw new Error('Missing comm');
|
|
30
|
+
}
|
|
31
|
+
const requestBody = request.body ? await request.arrayBuffer() : undefined;
|
|
32
|
+
const data = await comm.generateResponse({
|
|
33
|
+
kernelClientId,
|
|
34
|
+
urlPath,
|
|
35
|
+
method,
|
|
36
|
+
headers: requestHeaders,
|
|
37
|
+
requestBody: requestBody
|
|
38
|
+
? transfer(requestBody, [requestBody])
|
|
39
|
+
: undefined,
|
|
40
|
+
params
|
|
41
|
+
});
|
|
42
|
+
if (data) {
|
|
43
|
+
const { headers, content, status_code } = data;
|
|
44
|
+
return new Response(content, { status: status_code, headers });
|
|
45
|
+
}
|
|
46
|
+
return await fetch(url, { method });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private _commIds: Map<
|
|
50
|
+
string,
|
|
51
|
+
Omit<IConnectionManager, 'registerConnection'>
|
|
52
|
+
> = new Map();
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { IConnectionManager, IDict, IKernelExecutor } from '../type';
|
|
2
|
+
import { UUID } from '@lumino/coreutils';
|
|
3
|
+
|
|
4
|
+
export class ConnectionManager implements IConnectionManager {
|
|
5
|
+
constructor(public instanceId: string) {}
|
|
6
|
+
|
|
7
|
+
async registerConnection(
|
|
8
|
+
kernelExecutor: IKernelExecutor
|
|
9
|
+
): Promise<{ instanceId: string; kernelClientId: string }> {
|
|
10
|
+
const uuid = UUID.uuid4();
|
|
11
|
+
|
|
12
|
+
this._kernelExecutors.set(uuid, kernelExecutor);
|
|
13
|
+
|
|
14
|
+
return { instanceId: this.instanceId, kernelClientId: uuid };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async generateResponse(options: {
|
|
18
|
+
kernelClientId: string;
|
|
19
|
+
urlPath: string;
|
|
20
|
+
method: string;
|
|
21
|
+
headers: IDict;
|
|
22
|
+
requestBody?: ArrayBuffer;
|
|
23
|
+
params?: string;
|
|
24
|
+
}): Promise<IDict | null> {
|
|
25
|
+
const { urlPath, kernelClientId, method, params, requestBody, headers } =
|
|
26
|
+
options;
|
|
27
|
+
const executor = this._kernelExecutors.get(kernelClientId);
|
|
28
|
+
if (!executor) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const response = await executor.getResponse({
|
|
33
|
+
urlPath,
|
|
34
|
+
method,
|
|
35
|
+
params,
|
|
36
|
+
headers,
|
|
37
|
+
requestBody
|
|
38
|
+
});
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _kernelExecutors = new Map<string, IKernelExecutor>();
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
6
|
+
import { UUID } from '@lumino/coreutils';
|
|
7
|
+
import { expose } from 'comlink';
|
|
8
|
+
|
|
9
|
+
import { IConnectionManagerToken } from '../token';
|
|
10
|
+
import { IConnectionManager, MessageAction } from '../type';
|
|
11
|
+
import { ConnectionManager } from './connection_manager';
|
|
12
|
+
|
|
13
|
+
const fullLabextensionsUrl = PageConfig.getOption('fullLabextensionsUrl');
|
|
14
|
+
const SCOPE = `${fullLabextensionsUrl}/jupyterpack/static`;
|
|
15
|
+
async function initServiceWorker(): Promise<ServiceWorker | undefined | null> {
|
|
16
|
+
if (!('serviceWorker' in navigator)) {
|
|
17
|
+
console.error('Cannot start extension without service worker');
|
|
18
|
+
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fullWorkerUrl = `${SCOPE}/service-worker.js`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const reg = await navigator.serviceWorker.register(fullWorkerUrl);
|
|
26
|
+
|
|
27
|
+
if (!reg) {
|
|
28
|
+
console.error('Missing service worker registration');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await reg.update();
|
|
32
|
+
if (reg.installing) {
|
|
33
|
+
const sw = reg.installing || reg.waiting;
|
|
34
|
+
sw.onstatechange = () => {
|
|
35
|
+
if (sw.state === 'installed') {
|
|
36
|
+
window.location.reload();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (reg.active) {
|
|
41
|
+
return reg.active;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(
|
|
45
|
+
'Service worker newly registered',
|
|
46
|
+
await navigator.serviceWorker.getRegistration(fullWorkerUrl)
|
|
47
|
+
);
|
|
48
|
+
return reg.active;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error('Failed to register service worker', e);
|
|
51
|
+
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createPingFrame() {
|
|
57
|
+
const iframe = document.createElement('iframe');
|
|
58
|
+
iframe.style.display = 'none';
|
|
59
|
+
iframe.src = URLExt.join(SCOPE, '__jupyterpack__', 'ping.html');
|
|
60
|
+
document.body.appendChild(iframe);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const swPlugin: JupyterFrontEndPlugin<IConnectionManager> = {
|
|
64
|
+
id: 'jupyterpack:service-worker-plugin',
|
|
65
|
+
description: 'jupyterpack service worker plugin',
|
|
66
|
+
autoStart: true,
|
|
67
|
+
provides: IConnectionManagerToken,
|
|
68
|
+
activate: async (app: JupyterFrontEnd): Promise<IConnectionManager> => {
|
|
69
|
+
console.log('Activating jupyterpack service worker');
|
|
70
|
+
const serviceWorker = await initServiceWorker();
|
|
71
|
+
if (!serviceWorker) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Failed to register the Service Worker, please make sure to use a browser that supports this feature.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const instanceId = UUID.uuid4();
|
|
78
|
+
const { port1: mainToServiceWorker, port2: serviceWorkerToMain } =
|
|
79
|
+
new MessageChannel();
|
|
80
|
+
|
|
81
|
+
const connectionManager = new ConnectionManager(instanceId);
|
|
82
|
+
expose(connectionManager, mainToServiceWorker);
|
|
83
|
+
serviceWorker.postMessage(
|
|
84
|
+
{ type: MessageAction.INIT, data: { instanceId } },
|
|
85
|
+
[serviceWorkerToMain]
|
|
86
|
+
);
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
createPingFrame();
|
|
89
|
+
}, 10000);
|
|
90
|
+
|
|
91
|
+
return connectionManager;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// import { expose } from 'comlink';
|
|
2
|
+
import { MessageAction } from '../type';
|
|
3
|
+
import { CommManager } from './comm_manager';
|
|
4
|
+
|
|
5
|
+
const COMM_MANAGER = new CommManager();
|
|
6
|
+
/**
|
|
7
|
+
* Install event listeners
|
|
8
|
+
*/
|
|
9
|
+
self.addEventListener('install', onInstall);
|
|
10
|
+
self.addEventListener('activate', onActivate);
|
|
11
|
+
self.addEventListener('fetch', onFetch);
|
|
12
|
+
self.addEventListener('message', onMessage);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle installation
|
|
16
|
+
*/
|
|
17
|
+
async function onInstall(event: ExtendableEvent): Promise<void> {
|
|
18
|
+
await self.skipWaiting();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handle activation.
|
|
23
|
+
*/
|
|
24
|
+
async function onActivate(event: ExtendableEvent): Promise<void> {
|
|
25
|
+
event.waitUntil(self.clients.claim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle fetching a single resource.
|
|
30
|
+
*/
|
|
31
|
+
async function onFetch(event: FetchEvent): Promise<void> {
|
|
32
|
+
const url = event.request.url;
|
|
33
|
+
if (url.endsWith('__jupyterpack__/ping')) {
|
|
34
|
+
return event.respondWith(new Response('pong'));
|
|
35
|
+
}
|
|
36
|
+
if (url.endsWith('__jupyterpack__/ping.html')) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
event.respondWith(COMM_MANAGER.generateResponse(event.request));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onMessage(
|
|
43
|
+
msg: MessageEvent<{ type: MessageAction; data: any }>
|
|
44
|
+
): void {
|
|
45
|
+
const { type, data } = msg.data;
|
|
46
|
+
|
|
47
|
+
switch (type) {
|
|
48
|
+
case MessageAction.INIT: {
|
|
49
|
+
const { instanceId } = data;
|
|
50
|
+
const serviceWorkerToMain = msg.ports[0];
|
|
51
|
+
COMM_MANAGER.registerComm(instanceId, serviceWorkerToMain);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/token.ts
ADDED
package/src/tools.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function removePrefix(path: string, prefix: string): string {
|
|
2
|
+
if (path.startsWith(prefix)) {
|
|
3
|
+
return path.slice(prefix.length);
|
|
4
|
+
}
|
|
5
|
+
// If the prefix doesn't match, return the original path
|
|
6
|
+
return path;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
10
|
+
let binary = '';
|
|
11
|
+
const bytes = new Uint8Array(buffer);
|
|
12
|
+
const chunkSize = 32768; // process in chunks for large buffers
|
|
13
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
14
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
15
|
+
binary += String.fromCharCode(...chunk);
|
|
16
|
+
}
|
|
17
|
+
return btoa(binary);
|
|
18
|
+
}
|
package/src/type.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { KernelMessage } from '@jupyterlab/services';
|
|
2
|
+
import { IDisposable } from '@lumino/disposable';
|
|
3
|
+
|
|
4
|
+
export interface IDict<T = any> {
|
|
5
|
+
[key: string]: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum JupyterPackFramework {
|
|
9
|
+
REACT = 'react',
|
|
10
|
+
DASH = 'dash'
|
|
11
|
+
}
|
|
12
|
+
export interface IJupyterPackFileFormat {
|
|
13
|
+
entry: string;
|
|
14
|
+
framework: JupyterPackFramework;
|
|
15
|
+
name?: string;
|
|
16
|
+
metadata?: IDict;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export enum MessageAction {
|
|
20
|
+
INIT = 'INIT'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IKernelExecutorParams {
|
|
24
|
+
method: string;
|
|
25
|
+
urlPath: string;
|
|
26
|
+
headers: IDict;
|
|
27
|
+
params?: string;
|
|
28
|
+
requestBody?: ArrayBuffer;
|
|
29
|
+
}
|
|
30
|
+
export interface IKernelExecutor extends IDisposable {
|
|
31
|
+
getResponse(options: IKernelExecutorParams): Promise<IDict>;
|
|
32
|
+
executeCode(
|
|
33
|
+
code: KernelMessage.IExecuteRequestMsg['content']
|
|
34
|
+
): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IConnectionManager {
|
|
38
|
+
registerConnection(
|
|
39
|
+
kernelExecutor: IKernelExecutor
|
|
40
|
+
): Promise<{ instanceId: string; kernelClientId: string }>;
|
|
41
|
+
generateResponse(
|
|
42
|
+
option: { kernelClientId: string } & IKernelExecutorParams
|
|
43
|
+
): Promise<IDict | null>;
|
|
44
|
+
}
|
package/style/base.css
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
See the JupyterLab Developer Guide for useful CSS Patterns:
|
|
3
|
+
|
|
4
|
+
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.jp-jupyterpack-document-panel {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.jupyterpack-iframe-panel {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-grow: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.jupyterpack-iframe-panel iframe {
|
|
18
|
+
flex-grow: 1;
|
|
19
|
+
border: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.jupyterpack-spinner {
|
|
23
|
+
top: calc(50% - 4px);
|
|
24
|
+
left: calc(50% - 4px);
|
|
25
|
+
width: 8px;
|
|
26
|
+
height: 8px;
|
|
27
|
+
position: relative;
|
|
28
|
+
border-radius: 50%;
|
|
29
|
+
background: rgb(212 72 13 / 100%);
|
|
30
|
+
animation: wave 1.5s ease-in infinite;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes wave {
|
|
34
|
+
0% {
|
|
35
|
+
box-shadow:
|
|
36
|
+
0 0 0 0 rgb(212 72 13 / 0%),
|
|
37
|
+
0 0 0 10px rgb(212 72 13 / 20%),
|
|
38
|
+
0 0 0 20px rgb(212 72 13 / 60%),
|
|
39
|
+
0 0 0 30px rgb(212 72 13 / 40%),
|
|
40
|
+
0 0 0 40px rgb(212 72 13 / 20%);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
100% {
|
|
44
|
+
box-shadow:
|
|
45
|
+
0 0 0 40px rgb(212 72 13 / 0%),
|
|
46
|
+
0 0 0 30px rgb(212 72 13 / 20%),
|
|
47
|
+
0 0 0 20px rgb(212 72 13 / 40%),
|
|
48
|
+
0 0 0 10px rgb(212 72 13 / 60%),
|
|
49
|
+
0 0 0 0 rgb(212 72 13 / 100%);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|