Runtime SDK
Tools for observation and manipulation of the Observable Runtime.
```js
import {runtime, thisModule, observe, variables, descendants, lookupVariable, toObject} from '@tomlarkworthy/runtime-sdk'
```
viewof variables
the live view of variables in a runtime
// live view of all the variables
const variables = function (runtime) {
const view = Inputs.input(runtime._variables);
observeSet(runtime._variables, () => {
// There is a delay before the variable names are updated
setTimeout(() => {
view.value = runtime._variables;
view.dispatchEvent(new Event("input", { bubbles: true }));
}, 0);
});
return view;
}
const runtime_variables = view(variables(runtime))
runtime_variables
descendants
live view of a variable (s) and all its dataflow successors
const descendants = function (...variables) {
const results = new Set(variables);
const queue = variables;
do {
[...queue.pop()._outputs].forEach((v) => {
if (!results.has(v)) {
results.add(v);
queue.push(v);
}
});
} while (queue.length);
return results;
}
const decendants_example = [...descendants(lookupVariable("runtime", main))].map(
toObject
)
lookupVariable
lookup a variable by module
const lookupVariable = (name, module) => module._scope.get(name)
observe(variable)
This was monstrously difficult to develop. Taps a variable, intercepting all observer calls ["fulfilled", "rejected", "pending"] whilst preserving the behaviour of the existing observer attached to the variable. If detachNodes is true and the the existing observer hosts a DOM node, the additional variable "steals" it for it's DOM tree. When the observer attaches, if the variable is already fulfilled, the observer is signalled.
Currently unobserved variables are marked as reachable and become active.
const trace_variable = "---"
const no_observer = () => {
const variable = main.variable();
const symbol = variable._observer;
variable.delete();
return symbol;
}
function observe(v, observer, { invalidation, detachNodes = false } = {}) {
const cancels = new Set();
const onCancel = () => cancels.forEach((f) => f());
if (invalidation) invalidation.then(onCancel);
if (v?._name === trace_variable) {
console.log("observe", trace_variable, v);
debugger;
}
if (_.isEqual(v._observer, {}) || v._observer === no_observer) {
// No existing observer, so we install one
if (!v._reachable) {
// the the variable is not reachable, we mark it as reachable
// and trigger a recompute
v._reachable = true;
v._module._runtime._dirty.add(v);
v._module._runtime._updates.add(v);
}
let previous = v._observer;
v._observer = observer;
cancels.add(() => (v._observer = previous));
} else {
// intercepts an existing observer handler
["fulfilled", "rejected", "pending"].forEach((type) => {
const old = v._observer[type];
v._observer[type] = (...args) => {
if (v?._name === trace_variable) {
console.log(trace_variable, type, ...args);
}
// The old is often a prototype, so we use Reflect to call it
if (old) {
if (v?._name === trace_variable) {
console.log(`previous: ${type} ${trace_variable}`);
}
Reflect.apply(old, v._observer, args);
if (type === "fulfilled") {
if (
detachNodes &&
isnode(args[0]) &&
observer._node !== args[0].parentNode
) {
if (v?._name === trace_variable) {
console.log(`dettaching existing DOM: ${trace_variable}`);
}
args[0].remove();
}
}
}
if (v?._name === trace_variable) {
console.log(`tapped ${trace_variable} ${type}`);
}
if (observer[type]) observer[type](...args);
};
cancels.add(() => (v._observer[type] = old));
});
if (v?._name === trace_variable) {
debugger;
console.log(`checking`, trace_variable, v, toObject(v), v._value);
}
}
// Resolve initial state
if (v._value !== undefined) {
setTimeout(() => {
if (
detachNodes &&
isnode(v._value) &&
observer._node !== v._value.parentNode
) {
if (v?._name === trace_variable) {
console.log(`dettaching existing DOM: ${trace_variable}`);
}
v._value.remove();
}
if (v?._name === trace_variable) {
console.log(`tapped fulfilled: ${trace_variable}`);
}
observer.fulfilled(v._value, v._name);
}, 0);
} else {
// either in pending or error state, we can check by racing a promise
getPromiseState(v._promise).then(({ state, error, value }) => {
if (state == "rejected") {
if (observer.rejected) observer.rejected(error, v._name);
} else if (state == "pending") {
if (observer.pending) observer.pending();
} else if (state == "fulfilled") {
if (observer.fulfilled) observer.fulfilled(value, v._name);
}
});
}
return onCancel;
}
keepalive
Keep a named cell evaluated. Useful to keep background tasks alive even after importing.
const keepalive = (module, variable_name) => {
if (variable_name === undefined) debugger;
const name = `dynamic observe ${variable_name}`;
console.log(`keepalive: ${name}`);
if (module._scope.has(name)) return;
const variable = module.variable({}).define(name, [variable_name], (m) => m);
return () => variable.delete();
}
isOnObservableCom
const isOnObservableCom = () =>
location.href.includes("observableusercontent.com") &&
!location.href.includes("blob:")
viewof thisModule
Use like this
```
viewof notebookModule = thisModule()
```
const thisModule = async () => {
const view = new EventTarget();
view.tag = Symbol();
let module = undefined;
return Object.defineProperty(view, "value", {
get: () => {
if (module) return module;
find_with_tag(view.tag).then((v) => {
module = v._module;
view.dispatchEvent(new Event("input"));
});
}
});
}
const find_with_tag = (tag) => {
return new Promise((resolve) => {
[...runtime._variables].map((v) => {
if (v?._value?.tag == tag) {
resolve(v);
}
});
});
}
Utils
unorderedSync
Helper for syncing two arrays
const unorderedSync = (goal, current, identityFn = _.isEqual) => ({
add: _.differenceWith(goal, current, identityFn),
remove: _.differenceWith(current, goal, (a, b) => identityFn(b, a))
})
unorderedSync(
[
{ name: "red", age: 12 },
{ name: "joe", age: 1 }
],
[{ name: "joe" }, { name: "jean" }],
(a, b) => a.name == b.name
)
getPromiseState
async function getPromiseState(p) {
const sentinel = Symbol();
try {
const val = await Promise.race([p, Promise.resolve(sentinel)]);
return val === sentinel
? { state: "pending" }
: { state: "fulfilled", fulfilled: val };
} catch (err) {
return { state: "rejected", error: err };
}
}
Reposition set
function repositionSetElement(set, element, newPosition) {
if (!set.has(element)) {
throw new Error("Element not found in the set.");
}
// Convert Set to an array
const elementsArray = Array.from(set);
// Remove the element
const currentIndex = elementsArray.indexOf(element);
elementsArray.splice(currentIndex, 1);
// Insert element at the new position
elementsArray.splice(newPosition, 0, element);
// Reconstruct the Set
set.clear();
elementsArray.forEach(set.add, set);
}
// https://github.com/observablehq/inspector/blob/dba0354491fae7873d72f7cba485c356bac7c8fe/src/index.js#L66C10-L69C2
const isnode = (value) => {
return (
(value instanceof Element || value instanceof Text) &&
value instanceof value.constructor
);
}
//import { runtime, main } from "@mootari/access-runtime"
import { runtime, main } from "/components/access-runtime.js"
function observeSet(set, callback) {
const originalAdd = set.add;
set.add = function (value) {
const result = originalAdd.call(this, value); // Call the original `add`
callback("add", [value], this); // Invoke the callback
return result; // Maintain chainability
};
const originalDelete = set.delete;
set.delete = function (value) {
const result = originalDelete.call(this, value); // Call the original `delete`
callback("delete", [value], this); // Invoke the callback
return result;
};
const originalClear = set.clear;
set.clear = function () {
const result = originalClear.call(this); // Call the original `clear`
callback("clear", [], this); // Invoke the callback
return result;
};
return set; // Return the modified `Set`
}
const toObject = (v) =>
Object.fromEntries(Object.getOwnPropertyNames(v).map((p) => [p, v[p]]))