Accessing a Notebook's Runtime
Based on an idea by Bryan Gin-ge Chen (which he explores in his notebook Dirty tricks), this notebook demonstrates a hack that exposes a notebook's underlying Runtime instance.
Related discussion: Check if a cell is defined
Suggested imports:
```
~~~js
import {runtime, main, observed} from '@mootari/access-runtime'
~~~
```
API
Instances
//const runtime = recomputeTrigger, captureRuntime
const runtime = captureRuntime;
display(runtime)
const main = Array.from(modules).find(d => d[1] === 'main')[0];
display(main)
const modules = () => {
// Builtins are stored in a separate module.
const builtin = runtime._builtin;
// Imported modules are keyed by their define() functions, which we don't need here.
const imports = new Set(runtime._modules.values());
// Find all modules by retrieving them directly from the variables.
// Derived modules are "anonymous" but keep a reference to their source module.
const source = m => !m._source ? m : source(m._source);
const modules = new Set(Array.from(runtime._variables, v => source(v._module)));
// When you edit a notebook on observablehq.com, Observable defines the
// variables dynamically on main instead of creating a separate module.
// When embedded however the entry notebook also becomes a Runtime module.
const main = [...modules].find(m => m !== builtin && !imports.has(m));
const _imports = [...imports];
const labels = [
[builtin, 'builtin'],
[main || _imports.shift(), 'main'],
..._imports.map((m, i) => [m, `child${i+1}`]),
];
return new Map(labels);
};
display(modules)
Utilities
function observed(variable = null) {
const _observed = v => v._observer !== no_observer;
if(variable !== null) return _observed(variable);
const vars = new Set();
for(const v of runtime._variables) _observed(v) && vars.add(v);
return vars;
};
display(observed)
const no_observer = () => {
const v = main.variable();
const o = v._observer;
v.delete();
return o;
};
display(no_observer)
Internals
const captureRuntime = new Promise(resolve => {
const forEach = Set.prototype.forEach;
Set.prototype.forEach = function(...args) {
const thisArg = args[1];
forEach.apply(this, args);
if(thisArg && thisArg._modules) {
Set.prototype.forEach = forEach;
resolve(thisArg);
}
};
//mutable recomputeTrigger = mutable recomputeTrigger + 1;
set_recomputeTrigger(recomputeTrigger + 1)
});
display(captureRuntime)
//mutable recomputeTrigger = 0
const recomputeTrigger = Mutable(0)
const set_recomputeTrigger = (t) => recomputeTrigger.value = t;
Examples
Hit Refresh to update the lists below.
const ex_refresh = view(Inputs.button('Refresh'))
display(ex_refresh)
Defined variables
const ex_vars = (() => {
ex_refresh(); // side effects
return Array.from(runtime._variables).map(v => ({
name: v._name,
module: modules.get(v._module),
type: [, 'normal', 'implicit', 'duplicate'][v._type],
observed: v._observer !== no_observer,
inputs: v._inputs.length,
outputs: v._outputs.size
}));
})();
display(ex_vars)
const ex_vars_filters = () => view(() => {
const unique = (arr, acc) => Array.from(new Set(arr.map(acc))).sort((a, b) => a?.localeCompare?.(b));
const modules = unique(ex_vars, v => v.module);
const types = unique(ex_vars, v => v.type);
const value = this?.value ?? {};
return Inputs.form({
modules: Inputs.checkbox(modules, {
label: 'Modules',
value: value.modules ?? modules,
}),
types: Inputs.checkbox(types, {
label: 'Types',
value: value.types ?? types,
}),
features: Inputs.checkbox(['named', 'observed', 'inputs', 'outputs'], {
label: 'Features',
value: value.features ?? [],
})
});
});
display(ex_vars_filters)
const ex_vars_table = () => {
const flags = (arr) => Object.fromEntries(arr.map(v => [v, true]));
const modules = flags(ex_vars_filters.modules);
const types = flags(ex_vars_filters.types);
const {named, observed, inputs, outputs} = flags(ex_vars_filters.features);
const data = ex_vars.filter(d => true
&& modules[d.module]
&& types[d.type]
&& (!named || d.name != null)
&& (!observed || d.observed)
&& (!inputs || d.inputs)
&& (!outputs || d.outputs)
);
return Inputs.table(data);
};
display(ex_vars_table)
Dependency matrix
const ex_deps = () => {
ex_refresh;
const vars = Array.from(observed(), d => ({
name: d._name,
inputs: Array.from(d._inputs, d => d._name)
}));
const inputs = new Set();
for(const {inputs: i} of vars) for(const n of i) inputs.add(n);
return Inputs.table(
vars.map(d => ({
'': d.name,
...Object.fromEntries(d.inputs.map(n => [n, '✔️'])),
})),
{
columns: ['', ...Array.from(inputs).sort((a, b) => a.localeCompare(b))],
header: {
'': htl.html`<em>_name`
}
}
);
};
display(ex_deps)
Explainer
Presumably, our best shot at fetching the runtime is to receive the instance as thisArg to Set.prototype.forEach() in runtime_computeNow():
- In
captureRuntimewe apply a temporary monkey patch toSet.prototype.forEachin order to gain access to any passed-in parameters. - In
runtimewe define a dependency onrecomputeTriggerto ensure that theMutable's generator value is observed. - To trigger a recomputation we reassign
mutable recomputeTrigger. - In our overridden
Set.forEachcallback we then use duck typing to match the Runtime instance. - Once we've encountered the instance we restore
Set.forEachand resolvecaptureRuntime, and in turnruntime.
Updates
- 2022-08-28: Rewrite and simplification, documentation updates.
- 2022-08-27: Added
main,no_observer,observed. Added example forobserved.
Thumbnail image: Austrian National Library