Skip to content

Commit 6ca921b

Browse files
committed
widgets: fixing things -- mainly by imposing clarity regarding serialized v deserialized
1 parent af8d734 commit 6ca921b

File tree

5 files changed

+96
-71
lines changed

5 files changed

+96
-71
lines changed

src/packages/frontend/jupyter/output-messages/ipywidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function IpyWidget({ id: cell_id, value, actions }: WidgetProps) {
5353
// console.log("IpyWidget: not rendering due to widget_manager=null");
5454
return;
5555
}
56-
if (widget_manager.ipywidgets_state.get_model_state(id) == null) {
56+
if (widget_manager.ipywidgets_state.getSerializedModelState(id) == null) {
5757
// console.log("IpyWidget: not rendering due to uknown model state");
5858
setUnknown(true);
5959
return;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as base from "@jupyter-widgets/base";
77
import * as phosphor_controls from "@jupyter-widgets/controls";
88
import {
99
IpywidgetsState,
10-
ModelState,
10+
SerializedModelState as ModelState,
1111
} from "@cocalc/sync/editor/generic/ipywidgets-state";
1212
import { once } from "@cocalc/util/async-utils";
1313
import { Comm as ClassicComm } from "./comm";
@@ -146,12 +146,12 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> {
146146
): Promise<void> {
147147
if (!this.has_model(model_id)) {
148148
// given table a chance to push out the corresponding value update
149-
// before we create the model. Otherwise get_model_state below
149+
// before we create the model. Otherwise getSerializedModelState below
150150
// can't take into account the value.
151151
await this.waitUntilTableStabilizes();
152152
}
153153
const state: ModelState | undefined =
154-
this.ipywidgets_state.get_model_state(model_id);
154+
this.ipywidgets_state.getSerializedModelState(model_id);
155155
if (state == null) {
156156
// nothing to do...
157157
return;
@@ -591,7 +591,7 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> {
591591
} else if (moduleName === "k3d") {
592592
// NOTE: I completely rewrote the entire k3d widget interface, since
593593
// it made tons of assumptions that break RTC.
594-
module = await import('k3d');
594+
module = await import("k3d");
595595
} else if (moduleName === "jupyter-matplotlib") {
596596
//module = await import("jupyter-matplotlib");
597597
throw Error(`custom widgets: ${moduleName} not installed`);

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

Lines changed: 88 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
} from "@cocalc/widgets";
1515
import {
1616
IpywidgetsState,
17-
ModelState,
17+
SerializedModelState,
1818
} from "@cocalc/sync/editor/generic/ipywidgets-state";
1919
import { once } from "@cocalc/util/async-utils";
2020
import { is_array, is_object, len, uuid } from "@cocalc/util/misc";
@@ -26,6 +26,8 @@ import { size } from "lodash";
2626
import type { JupyterActions } from "@cocalc/frontend/jupyter/browser-actions";
2727
import { FileContext } from "@cocalc/frontend/lib/file-context";
2828

29+
type DeserializedModelState = { [key: string]: any };
30+
2931
export type SendCommFunction = (string, data) => string;
3032

3133
const log = console.log;
@@ -57,7 +59,7 @@ export class WidgetManager {
5759
this.ipywidgets_state.on("change", async (keys) => {
5860
for (const key of keys) {
5961
const [, model_id, type] = JSON.parse(key);
60-
this.handleIpWidgetsChange({ model_id, type });
62+
this.handleIpwidgetsTableChange({ model_id, type });
6163
}
6264
});
6365
}
@@ -86,6 +88,7 @@ VBox([s1, s2])
8688
if (type == "state") {
8789
(async () => {
8890
try {
91+
// causes initialization:
8992
await this.manager.get_model(model_id);
9093
// also ensure any buffers are set, e.g., this is needed when loading
9194
// https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#image
@@ -99,11 +102,17 @@ VBox([s1, s2])
99102
}
100103
};
101104

102-
private handleIpWidgetsChange = async ({ model_id, type }) => {
103-
log("handleIpWidgetsChange", { model_id, type });
105+
private handleIpwidgetsTableChange = async ({ model_id, type }) => {
106+
log("handleIpwidgetsTableChange", { model_id, type });
104107
switch (type) {
105108
case "state":
106-
await this.ipywidgets_state_StateChange(model_id);
109+
// Overall state is only used for creating widget when it is
110+
// rendered for the first time as part of Environment.getSerializedModelState
111+
// So do NOT call it again here on any change. Updates for RTC use
112+
// type='value'. Doing this causes a lot of noise, which e.g., completely
113+
// breaks rendering using the threejs custom widgets, e.g., this breaks;
114+
// from pythreejs import DodecahedronGeometry; DodecahedronGeometry()
115+
// await this.ipywidgets_state_StateChange(model_id);
107116
break;
108117
case "value":
109118
await this.ipywidgets_state_ValueChange(model_id);
@@ -120,19 +129,19 @@ VBox([s1, s2])
120129
}
121130
};
122131

123-
private ipywidgets_state_StateChange = async (model_id: string) => {
124-
log("handleStateChange: ", model_id);
125-
const state = this.ipywidgets_state.get_model_state(model_id);
126-
log("handleStateChange: state=", state);
127-
if (state == null) {
128-
return;
129-
}
130-
await this.updateModel(model_id, state!, false);
131-
};
132+
// private ipywidgets_state_StateChange = async (model_id: string) => {
133+
// log("ipywidgets_state_StateChange: ", model_id);
134+
// const state = this.ipywidgets_state.getSerializedModelState(model_id);
135+
// log("ipywidgets_state_StateChange: state=", JSON.stringify(state));
136+
// if (state == null) {
137+
// return;
138+
// }
139+
// await this.updateModel(model_id, state!, false);
140+
// };
132141

133142
private updateModel = async (
134143
model_id: string,
135-
changed: ModelState,
144+
changed: SerializedModelState,
136145
merge: boolean,
137146
): Promise<void> => {
138147
const model: base.DOMWidgetModel | undefined =
@@ -145,14 +154,9 @@ VBox([s1, s2])
145154
if (changed.last_changed != null) {
146155
this.last_changed[model_id] = changed;
147156
}
148-
const success = await this.dereferenceModelLinks(changed);
149-
if (!success) {
150-
console.warn(
151-
"update model suddenly references not known models -- can't handle this yet (TODO!); ignoring update.",
152-
);
153-
return;
154-
}
157+
155158
changed = await this.deserializeState(model, changed);
159+
156160
if (changed.hasOwnProperty("outputs") && changed["outputs"] == null) {
157161
// It can definitely be 'undefined' but set, e.g., the 'out.clear_output()' example at
158162
// https://ipywidgets.readthedocs.io/en/latest/examples/Output%20Widget.html
@@ -161,23 +165,40 @@ VBox([s1, s2])
161165
changed["outputs"] = [];
162166
}
163167
if (merge) {
164-
const state = model.get_state(false);
165-
const x: ModelState = {};
168+
const deserializedState = model.get_state(false);
169+
const x: DeserializedModelState = {};
166170
for (const k in changed) {
167-
if (state[k] != null && is_object(state[k]) && is_object(changed[k])) {
168-
x[k] = { ...state[k], ...changed[k] };
171+
if (
172+
deserializedState[k] != null &&
173+
is_object(deserializedState[k]) &&
174+
is_object(changed[k])
175+
) {
176+
x[k] = { ...deserializedState[k], ...changed[k] };
169177
} else {
170178
x[k] = changed[k];
171179
}
172180
}
173181
changed = x;
174182
}
175-
log("updateModel -- doing set_state", { model_id, merge, changed });
183+
184+
const success = await this.dereferenceModelLinks(changed);
185+
if (!success) {
186+
console.warn(
187+
"update model suddenly references not known models -- can't handle this yet (TODO!); ignoring update.",
188+
);
189+
return;
190+
}
191+
192+
log("updateModel -- doing set", {
193+
model_id,
194+
merge,
195+
changed: { ...changed },
196+
});
176197
try {
177198
model.set(changed);
178199
} catch (err) {
179-
//window.z = { merge, model, model_id, changed };
180-
console.error("saved to z", err);
200+
// window.z = { merge, model, model_id, changed: { ...changed } };
201+
console.error("updateModel set failed -- ", err);
181202
}
182203
};
183204

@@ -235,10 +256,11 @@ VBox([s1, s2])
235256
// via ipywidgets_state_BuffersChange!
236257
private setBuffers = async (
237258
model_id: string,
238-
state: ModelState,
259+
state: SerializedModelState,
239260
): Promise<void> => {
240261
const { buffer_paths, buffers } =
241262
await this.ipywidgets_state.get_model_buffers(model_id);
263+
log("setBuffers", model_id, buffer_paths);
242264
if (buffer_paths.length == 0) {
243265
return; // nothing to do
244266
}
@@ -273,7 +295,7 @@ VBox([s1, s2])
273295
return;
274296
}
275297
log("handleBuffersChange: ", { model_id, buffer_paths, buffers });
276-
const state = this.ipywidgets_state.get_model_state(model_id);
298+
const state = this.ipywidgets_state.getSerializedModelState(model_id);
277299
if (state == null) {
278300
log("handleBuffersChange: no state data", { model_id });
279301
return;
@@ -295,7 +317,7 @@ VBox([s1, s2])
295317
model.set(deserializedChange);
296318
} catch (err) {
297319
// window.y = { model_id, model, change, buffer_paths, buffers };
298-
console.error("saved to y", err);
320+
console.error("ipywidgets_state_BuffersChange failed -- ", err);
299321
}
300322
}
301323
};
@@ -372,8 +394,8 @@ VBox([s1, s2])
372394

373395
private deserializeState = async (
374396
model: base.DOMWidgetModel,
375-
serialized_state: ModelState,
376-
): Promise<ModelState> => {
397+
serialized_state: SerializedModelState,
398+
): Promise<DeserializedModelState> => {
377399
// log("deserializeState", { model, serialized_state });
378400
// NOTE: this is a reimplementation of soemething in
379401
// ipywidgets/packages/base/src/widget.ts
@@ -393,8 +415,8 @@ VBox([s1, s2])
393415
private _deserializeState = async (
394416
model_id: string,
395417
constructor: any,
396-
serialized_state: ModelState,
397-
): Promise<ModelState> => {
418+
serialized_state: SerializedModelState,
419+
): Promise<DeserializedModelState> => {
398420
// console.log("_deserialize_state", {
399421
// model_id,
400422
// constructor,
@@ -409,11 +431,15 @@ VBox([s1, s2])
409431
// We skip deserialize if the deserialize function is unpack_model,
410432
// since we do our own model unpacking, due to issues with ordering
411433
// and RTC.
412-
const deserialized: ModelState = {};
434+
const deserialized: DeserializedModelState = {};
413435
await this.setBuffers(model_id, serialized_state);
414436
for (const k in serialized_state) {
415437
const deserialize = serializers[k]?.deserialize;
416-
if (deserialize != null && !is_unpack_models(deserialize)) {
438+
if (
439+
deserialize != null &&
440+
!is_unpack_models(deserialize) &&
441+
!isModelReference(serialized_state[k])
442+
) {
417443
deserialized[k] = deserialize(serialized_state[k]);
418444
} else {
419445
deserialized[k] = serialized_state[k];
@@ -445,7 +471,7 @@ VBox([s1, s2])
445471
private dereferenceModelLink = async (
446472
val: string,
447473
): Promise<base.DOMWidgetModel | undefined> => {
448-
if (val.slice(0, 10) !== "IPY_MODEL_") {
474+
if (!isModelReference(val)) {
449475
throw Error(`val (="${val}") is not a model reference.`);
450476
}
451477

@@ -454,27 +480,21 @@ VBox([s1, s2])
454480
};
455481

456482
dereferenceModelLinks = async (state): Promise<boolean> => {
457-
// log("dereferenceModelLinks", "BEFORE", state);
483+
// log("dereferenceModelLinks", "BEFORE", { ...state });
458484
for (const key in state) {
459485
const val = state[key];
460-
if (typeof val === "string") {
461-
// single string
462-
if (val.slice(0, 10) === "IPY_MODEL_") {
463-
// that is a reference
464-
const model = await this.dereferenceModelLink(val);
465-
if (model != null) {
466-
state[key] = model;
467-
} else {
468-
return false; // something can't be resolved yet.
469-
}
486+
if (isModelReference(val)) {
487+
// that is a reference
488+
const model = await this.dereferenceModelLink(val);
489+
if (model != null) {
490+
state[key] = model;
491+
} else {
492+
return false; // something can't be resolved yet.
470493
}
471494
} else if (is_array(val)) {
472495
// array of stuff
473496
for (const i in val) {
474-
if (
475-
typeof val[i] === "string" &&
476-
val[i].slice(0, 10) === "IPY_MODEL_"
477-
) {
497+
if (isModelReference(val[i])) {
478498
// this one is a string reference
479499
const model = await this.dereferenceModelLink(val[i]);
480500
if (model != null) {
@@ -487,7 +507,7 @@ VBox([s1, s2])
487507
} else if (is_object(val)) {
488508
for (const key in val) {
489509
const z = val[key];
490-
if (typeof z == "string" && z.slice(0, 10) == "IPY_MODEL_") {
510+
if (isModelReference(z)) {
491511
const model = await this.dereferenceModelLink(z);
492512
if (model != null) {
493513
val[key] = model;
@@ -498,7 +518,7 @@ VBox([s1, s2])
498518
}
499519
}
500520
}
501-
// log("dereferenceModelLinks", "AFTER (success)", state);
521+
// log("dereferenceModelLinks", "AFTER (success)", { ...state });
502522
return true;
503523
};
504524

@@ -602,16 +622,16 @@ class Environment implements WidgetEnvironment {
602622
}
603623

604624
async getSerializedModelState(model_id) {
605-
// log("getModelState", model_id);
625+
// log("getSerializedModelState", model_id);
606626
if (this.manager.ipywidgets_state.get_state() != "ready") {
607627
await once(this.manager.ipywidgets_state, "ready");
608628
}
609-
let state = this.manager.ipywidgets_state.get_model_state(model_id);
629+
let state = this.manager.ipywidgets_state.getSerializedModelState(model_id);
610630
if (!state) {
611-
log("getModelState", model_id, "not yet known -- waiting");
631+
log("getSerializedModelState", model_id, "not yet known -- waiting");
612632
while (state == null) {
613633
await once(this.manager.ipywidgets_state, "change");
614-
state = this.manager.ipywidgets_state.get_model_state(model_id);
634+
state = this.manager.ipywidgets_state.getSerializedModelState(model_id);
615635
}
616636
}
617637
if (state == null) {
@@ -620,13 +640,13 @@ class Environment implements WidgetEnvironment {
620640
if (state._model_module == "k3d" && state.type != null) {
621641
while (!state?.type || !state?.id) {
622642
log(
623-
"getModelState",
643+
"getSerializedModelState",
624644
model_id,
625645
"k3d: waiting for state.type to be defined",
626646
);
627647

628648
await once(this.manager.ipywidgets_state, "change");
629-
state = this.manager.ipywidgets_state.get_model_state(model_id);
649+
state = this.manager.ipywidgets_state.getSerializedModelState(model_id);
630650
}
631651
}
632652
if (state == null) {
@@ -649,7 +669,7 @@ class Environment implements WidgetEnvironment {
649669
}
650670
}
651671

652-
log("getModelState", { model_id, state });
672+
log("getSerializedModelState", { model_id, state });
653673
setTimeout(() => this.manager.watchModel(model_id), 1);
654674
return {
655675
modelName: state._model_name,
@@ -737,3 +757,8 @@ function setInObject(obj: any, path: string[], value: any) {
737757
// and then set: obj[z] = value
738758
obj[path[path.length - 1]] = value;
739759
}
760+
761+
const IPY_MODEL = "IPY_MODEL_";
762+
function isModelReference(value): boolean {
763+
return typeof value == "string" && value.startsWith(IPY_MODEL);
764+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1362,7 +1362,7 @@ export class JupyterActions extends JupyterActions0 {
13621362
// and could lead to race conditions probably with multiple users, etc.
13631363
// It happens right when the widget is created.
13641364
/*
1365-
const state = this.syncdb.ipywidgets_state.get_model_state(model_id);
1365+
const state = this.syncdb.ipywidgets_state.getModelSerializedState(model_id);
13661366
data = { method: "update", state };
13671367
this.jupyter_kernel.send_comm_message_to_kernel(
13681368
misc.uuid(),

0 commit comments

Comments
 (0)