Skip to content

Commit c75b561

Browse files
committed
include buffers in custom comm message from backend to frontend
1 parent ed60492 commit c75b561

File tree

4 files changed

+114
-63
lines changed

4 files changed

+114
-63
lines changed

src/packages/frontend/jupyter/widgets/manager2.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { fromJS } from "immutable";
2222
import { CellOutputMessage } from "@cocalc/frontend/jupyter/output-messages/message";
2323
import React from "react";
2424
import ReactDOM from "react-dom/client";
25-
import { size } from "lodash";
2625
import type { JupyterActions } from "@cocalc/frontend/jupyter/browser-actions";
2726
import { FileContext } from "@cocalc/frontend/lib/file-context";
2827

@@ -262,7 +261,7 @@ VBox([s1, s2])
262261
state: SerializedModelState,
263262
): Promise<void> => {
264263
const { buffer_paths, buffers } =
265-
await this.ipywidgets_state.get_model_buffers(model_id);
264+
await this.ipywidgets_state.getModelBuffers(model_id);
266265
log("setBuffers", model_id, buffer_paths);
267266
if (buffer_paths.length == 0) {
268267
return; // nothing to do
@@ -293,7 +292,7 @@ VBox([s1, s2])
293292
https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#image
294293
*/
295294
const { buffer_paths, buffers } =
296-
await this.ipywidgets_state.get_model_buffers(model_id);
295+
await this.ipywidgets_state.getModelBuffers(model_id);
297296
if (buffer_paths.length == 0) {
298297
return;
299298
}
@@ -327,23 +326,14 @@ VBox([s1, s2])
327326

328327
private ipywidgets_state_MessageChange = async (model_id: string) => {
329328
// does this needs to tie into openCommChannel in the Environment...?
330-
const message = this.ipywidgets_state.get_message(model_id);
331-
log("handleMessageChange: ", model_id, message);
332-
if (size(message) == 0) {
333-
// TODO: temporary until we have delete functionality for tables
334-
log("handleMessageChange: message is empty -- nothing to do");
329+
const x = await this.ipywidgets_state.getMessage(model_id);
330+
if (x == null) {
335331
return;
336332
}
337-
log("handleMessageChange: getting model", model_id);
333+
const { message, buffers } = x;
334+
log("handleMessageChange: ", model_id, message, buffers);
338335
const model = await this.manager.get_model(model_id);
339-
log(
340-
"handleMessageChange:",
341-
"got model",
342-
model_id,
343-
"and now sending msg:custom",
344-
message,
345-
);
346-
model.trigger("msg:custom", message);
336+
model.trigger("msg:custom", message, buffers);
347337
};
348338

349339
// [ ] TODO: maybe have to keep trying for a while until model exists!
@@ -664,7 +654,7 @@ class Environment implements WidgetEnvironment {
664654
state["outputs"] = [];
665655
}
666656
const { buffer_paths, buffers } =
667-
await this.manager.ipywidgets_state.get_model_buffers(model_id);
657+
await this.manager.ipywidgets_state.getModelBuffers(model_id);
668658

669659
if (buffers.length > 0) {
670660
for (let i = 0; i < buffer_paths.length; i++) {

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

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
import { SyncDoc } from "./sync-doc";
2222
import { SyncTable } from "@cocalc/sync/table/synctable";
2323
import { Client } from "./types";
24-
import { delay } from "awaiting";
2524
import { debounce } from "lodash";
2625
import sha1 from "sha1";
2726

@@ -37,7 +36,6 @@ type Value = { [key: string]: any };
3736
// Also, we don't implement complete object delete yet so instead we
3837
// set the data field to null, which clears all state about and
3938
// object and makes it easy to know to ignore it.
40-
// We also use this time for deleting ephemeral messages.
4139
const GC_DEBOUNCE_MS = 10000;
4240

4341
// If for some reason GC needs to be deleted, e.g., maybe you
@@ -48,6 +46,9 @@ const GC_DEBOUNCE_MS = 10000;
4846
// collection.
4947
const DISABLE_GC = false;
5048

49+
// ignore messages past this age.
50+
const MAX_MESSAGE_TIME_MS = 10000;
51+
5152
interface CommMessage {
5253
header: { msg_id: string };
5354
parent_header: { msg_id: string };
@@ -74,7 +75,9 @@ export class IpywidgetsState extends EventEmitter {
7475
// This should be done in conjunction with the main table (with gc
7576
// on backend, and with change to null event on the frontend).
7677
private buffers: {
77-
[model_id: string]: { [path: string]: { buffer: Buffer; hash: string } };
78+
[model_id: string]: {
79+
[path: string]: { buffer: Buffer; hash: string };
80+
};
7881
} = {};
7982
// Similar but used on frontend
8083
private arrayBuffers: {
@@ -227,7 +230,7 @@ export class IpywidgetsState extends EventEmitter {
227230
buffers, which is a problem I don't think upstream ipywidgets
228231
has to solve.
229232
*/
230-
get_model_buffers = async (
233+
getModelBuffers = async (
231234
model_id: string,
232235
): Promise<{
233236
buffer_paths: string[][];
@@ -258,19 +261,8 @@ export class IpywidgetsState extends EventEmitter {
258261
buffers.push(cur.buffer);
259262
return;
260263
}
261-
// async get of the buffer efficiently via HTTP:
262-
if (this.client.ipywidgetsGetBuffer == null) {
263-
throw Error(
264-
"NotImplementedError: frontend client must implement ipywidgetsGetBuffer in order to support binary buffers",
265-
);
266-
}
267264
try {
268-
const buffer = await this.client.ipywidgetsGetBuffer(
269-
this.syncdoc.project_id,
270-
auxFileToOriginal(this.syncdoc.path),
271-
model_id,
272-
path,
273-
);
265+
const buffer = await this.clientGetBuffer(model_id, path);
274266
this.arrayBuffers[model_id][path] = { buffer, hash };
275267
buffer_paths.push(JSON.parse(path));
276268
buffers.push(buffer);
@@ -279,40 +271,81 @@ export class IpywidgetsState extends EventEmitter {
279271
}
280272
};
281273
// Run f in parallel on all of the keys of value:
282-
await Promise.all(value.keySeq().toJS().map(f));
274+
await Promise.all(
275+
value
276+
.keySeq()
277+
.toJS()
278+
.filter((path) => path.startsWith("["))
279+
.map(f),
280+
);
283281
return { buffers, buffer_paths };
284282
};
285283

284+
private clientGetBuffer = async (model_id: string, path: string) => {
285+
// async get of the buffer efficiently via HTTP:
286+
if (this.client.ipywidgetsGetBuffer == null) {
287+
throw Error(
288+
"NotImplementedError: frontend client must implement ipywidgetsGetBuffer in order to support binary buffers",
289+
);
290+
}
291+
return await this.client.ipywidgetsGetBuffer(
292+
this.syncdoc.project_id,
293+
auxFileToOriginal(this.syncdoc.path),
294+
model_id,
295+
path,
296+
);
297+
};
298+
286299
// Used on the backend by the project http server
287-
getBuffer = (model_id: string, buffer_path: string): Buffer | undefined => {
300+
getBuffer = (
301+
model_id: string,
302+
buffer_path_or_sha1: string,
303+
): Buffer | undefined => {
288304
const dbg = this.dbg("getBuffer");
289-
dbg("getBuffer", model_id, buffer_path);
290-
return this.buffers[model_id]?.[buffer_path]?.buffer;
305+
dbg("getBuffer", model_id, buffer_path_or_sha1);
306+
return this.buffers[model_id]?.[buffer_path_or_sha1]?.buffer;
291307
};
292308

309+
// returns the sha1 hashes of the buffers
293310
private set_model_buffers = (
311+
// model that buffers are associated to:
294312
model_id: string,
295-
buffer_paths: string[][],
313+
// if given, these are buffers with given paths; if not given, we
314+
// store buffer associated to sha1 (which is used for custom messages)
315+
buffer_paths: string[][] | undefined,
316+
// the actual buffers.
296317
buffers: Buffer[],
297318
fire_change_event: boolean = true,
298-
): void => {
319+
): string[] => {
299320
const dbg = this.dbg("set_model_buffers");
300321
dbg("buffer_paths = ", buffer_paths);
301322

302323
const data: { [path: string]: boolean } = {};
303324
if (this.buffers[model_id] == null) {
304325
this.buffers[model_id] = {};
305326
}
306-
for (let i = 0; i < buffer_paths.length; i++) {
307-
const key = JSON.stringify(buffer_paths[i]);
308-
// we set to the sha1 of the buffer not just to make getting
309-
// the buffer easy, but to make it easy to KNOW if we
310-
// even need to get the buffer.
311-
const hash = sha1(buffers[i]);
312-
data[key] = hash;
313-
this.buffers[model_id][key] = { buffer: buffers[i], hash };
327+
const hashes: string[] = [];
328+
if (buffer_paths != null) {
329+
for (let i = 0; i < buffer_paths.length; i++) {
330+
const key = JSON.stringify(buffer_paths[i]);
331+
// we set to the sha1 of the buffer not just to make getting
332+
// the buffer easy, but to make it easy to KNOW if we
333+
// even need to get the buffer.
334+
const hash = sha1(buffers[i]);
335+
hashes.push(hash);
336+
data[key] = hash;
337+
this.buffers[model_id][key] = { buffer: buffers[i], hash };
338+
}
339+
} else {
340+
for (const buffer of buffers) {
341+
const hash = sha1(buffer);
342+
hashes.push(hash);
343+
this.buffers[model_id][hash] = { buffer, hash };
344+
data[hash] = hash;
345+
}
314346
}
315347
this.set(model_id, "buffers", data, fire_change_event);
348+
return hashes;
316349
};
317350

318351
/*
@@ -477,6 +510,8 @@ scat.x, scat.y = np.random.rand(2, 50)
477510
const [string_id, model_id, type] = JSON.parse(key);
478511
if (!activeIds.has(model_id)) {
479512
// Delete this model from the table (or as close to delete as we have).
513+
// This removes the last message, state, buffer info, and value,
514+
// depending on type.
480515
this.table.set(
481516
{ string_id, type, model_id, data: null },
482517
"none",
@@ -709,16 +744,25 @@ scat.x, scat.y = np.random.rand(2, 50)
709744
message,
710745
buffers: `${buffers?.length ?? "no"} buffers`,
711746
});
747+
let buffer_hashes: string[];
712748
if (
713749
buffers != null &&
714750
buffers.length > 0 &&
715751
content.data.buffer_paths == null
716752
) {
717753
// TODO
718-
dbg("custom message -- there are BUFFERS -- NOT implemented!!");
754+
dbg("custom message -- there are BUFFERS -- saving them");
755+
buffer_hashes = this.set_model_buffers(
756+
model_id,
757+
undefined,
758+
buffers,
759+
false,
760+
);
761+
} else {
762+
buffer_hashes = [];
719763
}
720764
// We now send the message.
721-
this.sendCustomMessage(model_id, message, false);
765+
this.sendCustomMessage(model_id, message, buffer_hashes, false);
722766
break;
723767

724768
case "echo_update":
@@ -900,32 +944,49 @@ with out:
900944
private sendCustomMessage = async (
901945
model_id: string,
902946
message: object,
947+
buffer_hashes: string[],
903948
fire_change_event: boolean = true,
904949
): Promise<void> => {
905950
/*
906951
Send a custom message.
907952
908953
It's not at all clear what this should even mean in the context of
909954
realtime collaboration, and there will likely be clients where
910-
this is bad. But for now, we just make the message available
911-
via the table for a few seconds, then remove it. Any clients
912-
that are connected while we do this can react, and any that aren't
913-
just don't get the message (which is presumably fine).
955+
this is bad. But for now, we just make the last message sent
956+
available via the table, and each successive message overwrites the previous
957+
one. Any clients that are connected while we do this can react,
958+
and any that aren't just don't get the message (which is presumably fine).
914959
915960
Some widgets like ipympl use this to initialize state, so when a new
916961
client connects, it requests a message describing the plot, and everybody
917962
receives it.
918963
*/
919964

920-
this.set(model_id, "message", message, fire_change_event);
921-
await delay(GC_DEBOUNCE_MS);
922-
// Actually, delete is not implemented for synctable, so for
923-
// now we just set it to an empty message.
924-
this.set(model_id, "message", {}, fire_change_event);
965+
this.set(
966+
model_id,
967+
"message",
968+
{ message, buffer_hashes, time: Date.now() },
969+
fire_change_event,
970+
);
925971
};
926972

927-
get_message = (model_id: string) => {
928-
return this.get(model_id, "message")?.toJS();
973+
// Return the most recent message for the given model.
974+
getMessage = async (
975+
model_id: string,
976+
): Promise<{ message: object; buffers: ArrayBuffer[] } | undefined> => {
977+
const x = this.get(model_id, "message")?.toJS();
978+
if (x == null) {
979+
return undefined;
980+
}
981+
if (Date.now() - (x.time ?? 0) >= MAX_MESSAGE_TIME_MS) {
982+
return undefined;
983+
}
984+
const { message, buffer_hashes } = x;
985+
let buffers: ArrayBuffer[] = [];
986+
for (const hash of buffer_hashes) {
987+
buffers.push(await this.clientGetBuffer(model_id, hash));
988+
}
989+
return { message, buffers };
929990
};
930991
}
931992

src/packages/sync/table/synctable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export class SyncTable extends EventEmitter {
351351
Raises an exception if something goes wrong doing the set.
352352
Returns updated value otherwise.
353353
354-
DOES NOT causes a save.
354+
DOES NOT cause a save.
355355
356356
NOTE: we always use db schema to ensure types are correct,
357357
converting if necessary. This has a performance impact,

src/packages/util/smc-version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
/* autogenerated by the update_version script */
2-
exports.version=1719725282;
2+
exports.version=1721605026;

0 commit comments

Comments
 (0)