Skip to content

Commit 62ca36f

Browse files
committed
widgets: get sending binary buffers to work, so now the official file upload widget works!
1 parent fb85b1d commit 62ca36f

File tree

4 files changed

+71
-12
lines changed

4 files changed

+71
-12
lines changed

src/packages/jupyter/kernel/kernel.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -999,20 +999,32 @@ class JupyterKernel extends EventEmitter implements JupyterKernelInterface {
999999
target_name,
10001000
data,
10011001
buffers64,
1002+
buffers,
10021003
}: {
10031004
msg_id: string;
10041005
comm_id: string;
10051006
target_name: string;
10061007
data: any;
10071008
buffers64?: string[];
1009+
buffers?: Buffer[];
10081010
}): void {
10091011
const dbg = this.dbg("send_comm_message_to_kernel");
10101012
dbg({ msg_id, comm_id, target_name, data, buffers64 });
1011-
const buffers = buffers64?.map((x) => new Buffer(base64ToBuffer(x))) ?? [];
1012-
dbg(
1013-
"buffers lengths = ",
1014-
buffers.map((x) => x.byteLength),
1015-
);
1013+
if (buffers64 != null && buffers64.length > 0) {
1014+
buffers = buffers64?.map((x) => new Buffer(base64ToBuffer(x))) ?? [];
1015+
dbg(
1016+
"buffers lengths = ",
1017+
buffers.map((x) => x.byteLength),
1018+
);
1019+
if (this._actions?.syncdb.ipywidgets_state != null) {
1020+
this._actions.syncdb.ipywidgets_state.setModelBuffers(
1021+
comm_id,
1022+
data.buffer_paths,
1023+
buffers,
1024+
false,
1025+
);
1026+
}
1027+
}
10161028

10171029
const message = {
10181030
parent_header: {},

src/packages/jupyter/redux/project-actions.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,16 +1335,28 @@ export class JupyterActions extends JupyterActions0 {
13351335
let data: any;
13361336
if (type === "value") {
13371337
const state = this.syncdb.ipywidgets_state.get_model_value(model_id);
1338-
data = { method: "update", state };
1338+
// Saving the buffers on change is critical since otherwise this breaks:
1339+
// https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#file-upload
1340+
// Note that stupidly the buffer (e.g., image upload) gets sent to the kernel twice.
1341+
// But it does work robustly, and the kernel and nodejs server processes next to each
1342+
// other so this isn't so bad.
1343+
const { buffer_paths, buffers } =
1344+
this.syncdb.ipywidgets_state.getKnownBuffers(model_id);
1345+
data = { method: "update", state, buffer_paths };
13391346
this.jupyter_kernel.send_comm_message_to_kernel({
13401347
msg_id: misc.uuid(),
13411348
target_name: "jupyter.widget",
13421349
comm_id: model_id,
13431350
data,
1351+
buffers,
13441352
});
13451353
} else if (type === "buffers") {
1346-
// TODO: we will implement this soon. A good example where this is required
1347-
// is by the file upload widget:
1354+
// TODO: we MIGHT need implement this... but MAYBE NOT. An example where this seems like it might be
1355+
// required is by the file upload widget, but actually that just uses the value type above, since
1356+
// we explicitly fill in the widgets there; also there is an explicit comm upload message that
1357+
// the widget sends out that updates the buffer, and in send_comm_message_to_kernel in jupyter/kernel/kernel.ts
1358+
// when processing that message, we saves those buffers and make sure they are set in the
1359+
// value case above (otherwise they would get removed).
13481360
// https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#file-upload
13491361
// which creates a buffer from the content of the file, then sends it to the backend,
13501362
// which sees a change and has to write that buffer to the kernel (here) so that

src/packages/jupyter/types/project-interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ export interface JupyterKernelInterface extends EventEmitterInterface {
146146
comm_id: string;
147147
target_name: string;
148148
data: any;
149+
buffers?: any[];
150+
buffers64?: any[];
149151
}): void;
150152
get_connection_file(): string | undefined;
151153

src/packages/sync/editor/generic/ipywidgets-state.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,39 @@ export class IpywidgetsState extends EventEmitter {
281281
return { buffers, buffer_paths };
282282
};
283283

284+
// This is used on the backend when syncing changes from project nodejs *to*
285+
// the jupyter kernel.
286+
getKnownBuffers = (model_id: string) => {
287+
let value: iMap<string, string> | undefined = this.get(model_id, "buffers");
288+
if (value == null) {
289+
return { buffer_paths: [], buffers: [] };
290+
}
291+
// value is an array from JSON of paths array to array buffers:
292+
const buffer_paths: string[][] = [];
293+
const buffers: ArrayBuffer[] = [];
294+
if (this.buffers[model_id] == null) {
295+
this.buffers[model_id] = {};
296+
}
297+
const f = (path: string) => {
298+
const hash = value?.get(path);
299+
if (!hash) {
300+
return;
301+
}
302+
const cur = this.buffers[model_id][path];
303+
if (cur?.hash == hash) {
304+
buffer_paths.push(JSON.parse(path));
305+
buffers.push(cur.buffer);
306+
return;
307+
}
308+
};
309+
value
310+
.keySeq()
311+
.toJS()
312+
.filter((path) => path.startsWith("["))
313+
.map(f);
314+
return { buffers, buffer_paths };
315+
};
316+
284317
private clientGetBuffer = async (model_id: string, path: string) => {
285318
// async get of the buffer efficiently via HTTP:
286319
if (this.client.ipywidgetsGetBuffer == null) {
@@ -307,7 +340,7 @@ export class IpywidgetsState extends EventEmitter {
307340
};
308341

309342
// returns the sha1 hashes of the buffers
310-
private set_model_buffers = (
343+
setModelBuffers = (
311344
// model that buffers are associated to:
312345
model_id: string,
313346
// if given, these are buffers with given paths; if not given, we
@@ -317,7 +350,7 @@ export class IpywidgetsState extends EventEmitter {
317350
buffers: Buffer[],
318351
fire_change_event: boolean = true,
319352
): string[] => {
320-
const dbg = this.dbg("set_model_buffers");
353+
const dbg = this.dbg("setModelBuffers");
321354
dbg("buffer_paths = ", buffer_paths);
322355

323356
const data: { [path: string]: boolean } = {};
@@ -728,7 +761,7 @@ scat.x, scat.y = np.random.rand(2, 50)
728761
if (content.data.buffer_paths?.length > 0) {
729762
// Deal with binary buffers:
730763
dbg("setting binary buffers");
731-
this.set_model_buffers(
764+
this.setModelBuffers(
732765
model_id,
733766
content.data.buffer_paths,
734767
msg.buffers,
@@ -752,7 +785,7 @@ scat.x, scat.y = np.random.rand(2, 50)
752785
) {
753786
// TODO
754787
dbg("custom message -- there are BUFFERS -- saving them");
755-
buffer_hashes = this.set_model_buffers(
788+
buffer_hashes = this.setModelBuffers(
756789
model_id,
757790
undefined,
758791
buffers,

0 commit comments

Comments
 (0)