Skip to content

Commit 4671b8e

Browse files
sspickleclaude
andcommitted
media: colab bootstrap served via jsDelivr (external script)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
1 parent d1be792 commit 4671b8e

1 file changed

Lines changed: 162 additions & 0 deletions

File tree

‎media/glowcomm_colab.js‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// glowcomm_colab.js — browser side of the Colab (comm-only) VPython frontend.
2+
//
3+
// Injected inline by with_colab.py via display(HTML). Loads GlowScript from
4+
// a CDN (Colab output frames may load external scripts; jsDelivr serves the
5+
// vpython/vscode-vpython repo's media/ directory), then registers a Jupyter
6+
// comm target through Colab's shim (google.colab.kernel.comms) and drives
7+
// glowcomm_host.js:
8+
//
9+
// downlink: comm message (msg.data = package | 'trigger') -> fe.handle
10+
// uplink: pacer -> fe.tick() -> send(events) -> comm.send(events)
11+
//
12+
// Colab comm API (probed 2026-08): registerTarget(name, cb); the comm object
13+
// is {send, close, messages} with messages an async iterator. The kernel may
14+
// retry the comm open (it can't know when this script has run), so every
15+
// open is acked and the LATEST comm becomes the active channel.
16+
//
17+
// Adaptive pacing: 33 ms while the kernel is answering, easing off to 500 ms
18+
// when nothing has come back for 2 s (kernel blocked in a long computation
19+
// or between cells) — bounds the message backlog a blocked kernel must
20+
// swallow when it wakes.
21+
22+
window.__VPYTHON_COLAB_BOOT = function (opts) {
23+
'use strict';
24+
var CDN = opts.cdn; // ends with '/'
25+
var NONCE = opts.nonce; // this session's token; stale saved frames have an old one
26+
var TICK_MS = 33, SLOW_MS = 500, EASE_AFTER_MS = 2000;
27+
28+
var root = document.getElementById('vpython-colab-root');
29+
var status = document.createElement('div');
30+
status.style.cssText = 'font-family:monospace;font-size:12px;opacity:.8';
31+
var container = document.createElement('div');
32+
root.appendChild(status);
33+
root.appendChild(container);
34+
35+
function setStatus(m) { status.textContent = m; }
36+
function fail(m) {
37+
var line = document.createElement('pre');
38+
line.textContent = 'VPython (Colab): ' + m;
39+
line.style.cssText = 'color:#c00;white-space:pre-wrap';
40+
root.appendChild(line);
41+
}
42+
43+
function loadScript(name) {
44+
return new Promise(function (resolve, reject) {
45+
var s = document.createElement('script');
46+
s.src = CDN + name;
47+
s.onload = resolve;
48+
s.onerror = function () { reject(new Error('failed to load ' + CDN + name)); };
49+
document.head.appendChild(s);
50+
});
51+
}
52+
53+
// Hide any AMD loader while classic UMD scripts execute (else jQuery UI
54+
// registers as a module and never patches $.fn — learned in VS Code).
55+
var hadDefine = Object.prototype.hasOwnProperty.call(window, 'define');
56+
var savedDefine = window.define;
57+
try { window.define = undefined; } catch (e) { }
58+
function restoreAmd() {
59+
if (hadDefine) { window.define = savedDefine; }
60+
else { try { delete window.define; } catch (e) { } }
61+
}
62+
63+
setStatus('VPython: loading GlowScript from CDN…');
64+
loadScript('jquery.min.js')
65+
.then(function () { return loadScript('jquery-ui.custom.min.js'); })
66+
.then(function () { return loadScript('glow.min.js'); })
67+
.then(function () {
68+
window.Jupyter_VPython = CDN + 'data/'; // texture prefix for glow
69+
function loadFont(file, slot) {
70+
return new Promise(function (resolve, reject) {
71+
window.opentype_load(window.Jupyter_VPython + file, function (err, font) {
72+
if (err) { reject(new Error('font ' + file + ': ' + err)); return; }
73+
window[slot] = font;
74+
resolve();
75+
});
76+
});
77+
}
78+
return Promise.all([
79+
loadFont('Roboto-Medium.ttf', '__font_sans'),
80+
loadFont('NimbusRomNo9L-Med.otf', '__font_serif')
81+
]);
82+
})
83+
.then(function () { return loadScript('glowcomm_host.js'); })
84+
.then(function () {
85+
restoreAmd();
86+
var jq = window.$ || window.jQuery;
87+
if (!jq || !jq.fn || typeof jq.fn.resizable !== 'function') {
88+
throw new Error('jQuery UI did not attach ($.fn.resizable missing)');
89+
}
90+
if (typeof window.createGlowFrontend !== 'function') {
91+
throw new Error('glowcomm_host loaded but createGlowFrontend missing');
92+
}
93+
94+
var active = null, fe = null, lastReply = Date.now();
95+
96+
function startPacer() {
97+
(function tickLoop() {
98+
var idle = Date.now() - lastReply > EASE_AFTER_MS;
99+
try { fe.tick(); } catch (e) { fail('tick: ' + e); }
100+
setTimeout(tickLoop, idle ? SLOW_MS : TICK_MS);
101+
})();
102+
}
103+
104+
// Shared by both handshake directions: bind a live comm as the active
105+
// channel, create the frontend on first use, and pump its messages.
106+
function useComm(comm, ackFirst) {
107+
active = comm; // latest wins on both sides
108+
if (ackFirst) { comm.send({ ack: 1 }); }
109+
if (!fe) {
110+
fe = window.createGlowFrontend({
111+
container: container,
112+
glow: window,
113+
send: function (events) { if (active) { active.send(events); } }
114+
});
115+
startPacer();
116+
setStatus('');
117+
}
118+
(async function () {
119+
try {
120+
for await (var m of comm.messages) {
121+
lastReply = Date.now();
122+
var d = (m && m.data !== undefined) ? m.data : m;
123+
fe.handle(d === 'trigger' ? 'trigger' : d);
124+
}
125+
} catch (e) { /* iterator ends when comm closes; a newer comm takes over */ }
126+
})();
127+
}
128+
129+
var comms = google.colab.kernel.comms;
130+
131+
// Fallback: kernel-initiated opens (retried from the kernel idle loop).
132+
comms.registerTarget('vpython-glow', function (comm, openMsg) {
133+
// Ignore kernel-initiated opens meant for a different session's frame.
134+
var d = openMsg && (openMsg.data || (openMsg.content && openMsg.content.data));
135+
if (d && d.nonce && d.nonce !== NONCE) { return; }
136+
useComm(comm, true); // ack tells the kernel which comm to attach
137+
});
138+
139+
// Preferred: WE open the comm, because only we know when this script
140+
// has actually finished loading. The kernel registered the target
141+
// before displaying this bootstrap, so it always exists by now. The
142+
// kernel processes the open at its next idle moment and flushes the
143+
// buffered scene. No races, no retries.
144+
if (typeof comms.open === 'function') {
145+
Promise.resolve(comms.open('vpython-glow-kernel', { nonce: NONCE }))
146+
.then(function (comm) {
147+
if (comm && comm.send && comm.messages) { useComm(comm, true); }
148+
else { setStatus('VPython: comms.open returned unusable comm; ' +
149+
'waiting for kernel-initiated connect…'); }
150+
})
151+
.catch(function (e) {
152+
setStatus('VPython: comms.open failed (' + e + '); ' +
153+
'waiting for kernel-initiated connect…');
154+
});
155+
setStatus('VPython: ready — connecting to the kernel…');
156+
} else {
157+
setStatus('VPython: ready — waiting for the kernel to connect ' +
158+
'(run the next cell if this lingers)…');
159+
}
160+
})
161+
.catch(function (e) { restoreAmd(); fail((e && e.stack) || String(e)); });
162+
};

0 commit comments

Comments
 (0)