Skip to content

Commit 1da6732

Browse files
authored
Expose comm and custom messages on the widget model (#30)
1 parent fefc230 commit 1da6732

4 files changed

Lines changed: 123 additions & 5 deletions

File tree

src/model.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { MapChange } from '@jupyter/ydoc';
2+
import { Kernel } from '@jupyterlab/services';
23
import { JSONExt, JSONObject, PromiseDelegate } from '@lumino/coreutils';
34
import { ISignal, Signal } from '@lumino/signaling';
45
import * as Y from 'yjs';
56

67
import type { Awareness } from 'y-protocols/awareness';
7-
import { IJupyterYDoc, IJupyterYModel } from './types';
8+
import { IJupyterYDoc, IJupyterYModel, IYCommProvider } from './types';
89

910
export class JupyterYModel implements IJupyterYModel {
1011
constructor(commMetadata: { [key: string]: any }) {
@@ -57,6 +58,41 @@ export class JupyterYModel implements IJupyterYModel {
5758
return undefined;
5859
}
5960

61+
/**
62+
* The raw comm channel backing this model, available once the widget
63+
* manager has wired up the comm provider.
64+
*/
65+
get comm(): Kernel.IComm | undefined {
66+
return this._commProvider?.comm;
67+
}
68+
69+
/**
70+
* Fires when a custom (non Y-protocol) message is received over the comm.
71+
*/
72+
get messageReceived(): ISignal<IJupyterYModel, JSONObject> {
73+
return this._messageReceived;
74+
}
75+
76+
/**
77+
* Send a custom message to the kernel over the comm.
78+
*/
79+
sendMessage(data: JSONObject): void {
80+
this._commProvider?.sendMessage(data);
81+
}
82+
83+
/**
84+
* Wire the model to its comm provider. Called by the widget manager once
85+
* the comm is open; relays custom comm messages through `messageReceived`.
86+
*/
87+
setCommProvider(provider: IYCommProvider): void {
88+
this._commProvider = provider;
89+
provider.messageReceived.connect(this._onCommMessage, this);
90+
}
91+
92+
private _onCommMessage = (_: IYCommProvider, data: JSONObject): void => {
93+
this._messageReceived.emit(data);
94+
};
95+
6096
protected async initialize(commMetadata: { [key: string]: any }) {
6197
this.ydoc = this.ydocFactory(commMetadata);
6298
this.sharedModel = new JupyterYDoc(commMetadata, this._ydoc);
@@ -71,6 +107,8 @@ export class JupyterYModel implements IJupyterYModel {
71107
return;
72108
}
73109
this._isDisposed = true;
110+
this._commProvider?.messageReceived.disconnect(this._onCommMessage, this);
111+
this._commProvider?.dispose();
74112
this._sharedModel.dispose();
75113
this._disposed.emit();
76114
Signal.clearData(this);
@@ -89,6 +127,9 @@ export class JupyterYModel implements IJupyterYModel {
89127
private _yModelName: string;
90128
private _sharedModel: IJupyterYDoc;
91129

130+
private _commProvider?: IYCommProvider;
131+
private _messageReceived = new Signal<IJupyterYModel, JSONObject>(this);
132+
92133
private _isDisposed = false;
93134

94135
private _ready: PromiseDelegate<void> = new PromiseDelegate<void>();

src/notebookrenderer/widgetManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ export class WidgetModelRegistry implements IJupyterYWidgetModelRegistry {
7171

7272
await yModel.ready;
7373

74-
new YCommProvider({
74+
const commProvider = new YCommProvider({
7575
comm,
7676
ydoc: yModel.sharedModel.ydoc,
7777
awareness: yModel.awareness
7878
});
79+
yModel.setCommProvider(commProvider);
7980
this._yModels.set(comm.commId, yModel);
8081
};
8182

src/notebookrenderer/yCommProvider.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
} from 'y-protocols/awareness';
99
import * as syncProtocol from 'y-protocols/sync';
1010
import * as Y from 'yjs';
11-
import { IDisposable } from '@lumino/disposable';
11+
import { JSONObject } from '@lumino/coreutils';
12+
import { ISignal, Signal } from '@lumino/signaling';
13+
14+
import { IYCommProvider } from '../types';
1215

1316
export enum YMessageType {
1417
SYNC = 0,
@@ -26,7 +29,7 @@ export interface IYCommProviderOptions {
2629
awareness?: Awareness;
2730
}
2831

29-
export class YCommProvider implements IDisposable {
32+
export class YCommProvider implements IYCommProvider {
3033
constructor(options: IYCommProviderOptions) {
3134
this._comm = options.comm;
3235
this._ydoc = options.ydoc;
@@ -49,10 +52,21 @@ export class YCommProvider implements IDisposable {
4952
return this._ydoc;
5053
}
5154

55+
get comm(): Kernel.IComm {
56+
return this._comm;
57+
}
58+
5259
get awareness(): Awareness {
5360
return this._awareness;
5461
}
5562

63+
/**
64+
* Fires when a custom (non Y-protocol) message is received over the comm.
65+
*/
66+
get messageReceived(): ISignal<IYCommProvider, JSONObject> {
67+
return this._messageReceived;
68+
}
69+
5670
get synced(): boolean {
5771
return this._synced;
5872
}
@@ -78,10 +92,19 @@ export class YCommProvider implements IDisposable {
7892
}
7993
this._comm.close();
8094
this._isDisposed = true;
95+
Signal.clearData(this);
96+
}
97+
98+
/**
99+
* Send a custom message to the kernel over the comm.
100+
*/
101+
sendMessage(data: JSONObject): void {
102+
this._comm.send(data);
81103
}
82104

83105
private _onMsg = (msg: KernelMessage.ICommMsgMsg<'iopub' | 'shell'>) => {
84-
if (msg.buffers) {
106+
if (msg.buffers && msg.buffers.length > 0) {
107+
// Binary payload: a Y-protocol frame (document sync or awareness).
85108
const buffer = msg.buffers[0] as ArrayBuffer;
86109
const buffer_uint8 = new Uint8Array(
87110
ArrayBuffer.isView(buffer) ? buffer.buffer : buffer
@@ -90,6 +113,9 @@ export class YCommProvider implements IDisposable {
90113
if (encoding.length(encoder) > 1) {
91114
this._sendOverComm(encoding.toUint8Array(encoder));
92115
}
116+
} else {
117+
// No buffers: a custom application message
118+
this._messageReceived.emit(msg.content.data as JSONObject);
93119
}
94120
};
95121

@@ -140,6 +166,7 @@ export class YCommProvider implements IDisposable {
140166
private _ownsAwareness: boolean;
141167
private _synced: boolean;
142168
private _isDisposed = false;
169+
private _messageReceived = new Signal<IYCommProvider, JSONObject>(this);
143170
}
144171

145172
namespace Private {

src/types.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MapChange, StateChange } from '@jupyter/ydoc';
2+
import { Kernel } from '@jupyterlab/services';
23
import * as Y from 'yjs';
34
import { ISignal } from '@lumino/signaling';
45
import { JSONObject } from '@lumino/coreutils';
@@ -23,6 +24,31 @@ export interface IJupyterYDoc extends IDisposable {
2324
disposed: ISignal<any, void>;
2425
}
2526

27+
/**
28+
* A provider bridging a Y.Doc to a Jupyter Comm channel.
29+
*
30+
* Besides the Y-protocol (sync/awareness) traffic, it exposes the raw comm
31+
* and relays custom, non-document messages so consumers can implement
32+
* stateless "one shot" actions (e.g. a "zoom-to" request).
33+
*/
34+
export interface IYCommProvider extends IDisposable {
35+
/**
36+
* The underlying comm channel, exposed so consumers can do anything they
37+
* need with it beyond the built-in message handling.
38+
*/
39+
comm: Kernel.IComm;
40+
41+
/**
42+
* Fires when a custom (non Y-protocol) message is received over the comm.
43+
*/
44+
messageReceived: ISignal<IYCommProvider, JSONObject>;
45+
46+
/**
47+
* Send a custom message to the kernel over the comm.
48+
*/
49+
sendMessage(data: JSONObject): void;
50+
}
51+
2652
export interface IJupyterYModel extends IDisposable {
2753
yModelName: string;
2854
isDisposed: boolean;
@@ -39,4 +65,27 @@ export interface IJupyterYModel extends IDisposable {
3965
* instead of creating a second instance on the same Y.Doc
4066
*/
4167
awareness?: Awareness;
68+
69+
/**
70+
* The raw comm channel backing this model, exposed so consumers can send
71+
* and receive custom (non-document) messages. Available once the comm has
72+
* been opened and wired up by the widget manager.
73+
*/
74+
comm?: Kernel.IComm;
75+
76+
/**
77+
* Fires when a custom (non Y-protocol) message is received over the comm.
78+
*/
79+
messageReceived: ISignal<IJupyterYModel, JSONObject>;
80+
81+
/**
82+
* Send a custom message to the kernel over the comm.
83+
*/
84+
sendMessage(data: JSONObject): void;
85+
86+
/**
87+
* Wire the model to its comm provider. Called by the widget manager once
88+
* the comm is open; enables `comm`, `sendMessage` and `messageReceived`.
89+
*/
90+
setCommProvider(provider: IYCommProvider): void;
4291
}

0 commit comments

Comments
 (0)