Skip to content

Commit 0e51b2c

Browse files
committed
Allow instantiation of multiple notebooks inside a single scope
Signed-off-by: Gordon Smith <[email protected]>
1 parent 09c603e commit 0e51b2c

File tree

3 files changed

+96
-70
lines changed

3 files changed

+96
-70
lines changed

src/runtime/define.ts

Lines changed: 93 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type {Variable, VariableDefinition} from "@observablehq/runtime";
1+
import {Runtime, type Module, type Variable, type VariableDefinition} from "@observablehq/runtime";
22
import type {DisplayState} from "./display.js";
33
import {clear, display, observe} from "./display.js";
4-
import {main} from "./index.js";
4+
import {library} from "./stdlib/index.js";
55
import {input} from "./stdlib/generators/index.js";
66
import {Mutator} from "./stdlib/mutable.js";
7+
import {fileAttachments} from "./stdlib/fileAttachment.js";
78

89
export type DefineState = DisplayState & {
910
/** the runtime variables associated with this cell */
@@ -31,61 +32,101 @@ export type Definition = {
3132
assets?: Map<string, string>;
3233
};
3334

34-
export function define(state: DefineState, definition: Definition, observer = observe): void {
35-
const {id, body, inputs = [], outputs = [], output, autodisplay, autoview, automutable} = definition;
36-
const variables = state.variables;
37-
const v = main.variable(observer(state, definition), {shadow: {}});
38-
const vid = output ?? (outputs.length ? `cell ${id}` : null);
39-
if (inputs.includes("display") || inputs.includes("view")) {
40-
let displayVersion = -1; // the variable._version of currently-displayed values
41-
const vd = new (v.constructor as typeof Variable)(2, v._module);
42-
vd.define(
43-
inputs.filter((i) => i !== "display" && i !== "view"),
44-
() => {
45-
const version = v._version; // capture version on input change
46-
return (value: unknown) => {
47-
if (version < displayVersion) throw new Error("stale display");
48-
else if (state.variables[0] !== v) throw new Error("stale display");
49-
else if (version > displayVersion) clear(state);
50-
displayVersion = version;
51-
display(state, value);
52-
return value;
53-
};
54-
}
55-
);
56-
v._shadow.set("display", vd);
57-
if (inputs.includes("view")) {
58-
const vv = new (v.constructor as typeof Variable)(2, v._module, null, {shadow: {}});
59-
vv._shadow.set("display", vd);
60-
vv.define(["display"], (display) => (value: unknown) => input(display(value)));
61-
v._shadow.set("view", vv);
35+
export class NotebookInstance {
36+
37+
protected runtime = Object.assign(new Runtime({ ...library, __ojs_runtime: () => this.runtime }), { fileAttachments });
38+
protected main = (this.runtime as typeof this.runtime & { main: Module }).main = this.runtime.module();
39+
protected stateById = new Map<number, DefineState>();
40+
41+
constructor() {
42+
this.main.constructor.prototype.defines = function (this: Module, name: string): boolean {
43+
return (
44+
this._scope.has(name) ||
45+
this._builtins.has(name) ||
46+
this._runtime._builtin._scope.has(name)
47+
);
48+
};
49+
}
50+
51+
define(state: DefineState, definition: Definition, observer = observe): void {
52+
this.undefine(definition.id);
53+
this._define(state, definition, observer);
54+
this.stateById.set(definition.id, state);
55+
}
56+
57+
undefine(id: number): void {
58+
const state = this.stateById.get(id);
59+
if (state) {
60+
state.variables.forEach((v) => v.delete());
61+
this.stateById.delete(id);
62+
state.root.textContent = "";
63+
}
64+
}
65+
66+
clear(): void {
67+
const keys = Array.from(this.stateById.keys());
68+
for (const key of keys) {
69+
this.undefine(key);
6270
}
63-
} else if (!autodisplay) {
64-
clear(state);
6571
}
66-
variables.push(v.define(vid, inputs, body));
67-
if (output != null) {
68-
if (autoview) {
69-
const o = unprefix(output, "viewof$");
70-
variables.push(main.define(o, [output], input));
71-
} else if (automutable) {
72-
const o = unprefix(output, "mutable ");
73-
const x = `cell ${id}`;
74-
v.define(o, [x], ([mutable]) => mutable); // observe live value
75-
variables.push(
76-
main.define(output, inputs, body), // initial value
77-
main.define(x, [output], Mutator),
78-
main.define(`mutable$${o}`, [x], ([, mutator]) => mutator)
72+
73+
protected _define(state: DefineState, definition: Definition, observer = observe): void {
74+
const { id, body, inputs = [], outputs = [], output, autodisplay, autoview, automutable } = definition;
75+
const variables = state.variables;
76+
const v = this.main.variable(observer(state, definition), { shadow: {} });
77+
const vid = output ?? (outputs.length ? `cell ${id}` : null);
78+
if (inputs.includes("display") || inputs.includes("view")) {
79+
let displayVersion = -1; // the variable._version of currently-displayed values
80+
const vd = new (v.constructor as typeof Variable)(2, v._module);
81+
vd.define(
82+
inputs.filter((i) => i !== "display" && i !== "view"),
83+
() => {
84+
const version = v._version; // capture version on input change
85+
return (value: unknown) => {
86+
if (version < displayVersion) throw new Error("stale display");
87+
else if (state.variables[0] !== v) throw new Error("stale display");
88+
else if (version > displayVersion) clear(state);
89+
displayVersion = version;
90+
display(state, value);
91+
return value;
92+
};
93+
}
7994
);
95+
v._shadow.set("display", vd);
96+
if (inputs.includes("view")) {
97+
const vv = new (v.constructor as typeof Variable)(2, v._module, null, { shadow: {} });
98+
vv._shadow.set("display", vd);
99+
vv.define(["display"], (display) => (value: unknown) => input(display(value)));
100+
v._shadow.set("view", vv);
101+
}
102+
} else if (!autodisplay) {
103+
clear(state);
80104
}
81-
} else {
82-
for (const o of outputs) {
83-
variables.push(main.variable(true).define(o, [vid!], (exports) => exports[o]));
105+
variables.push(v.define(vid, inputs, body));
106+
if (output != null) {
107+
if (autoview) {
108+
const o = this.unprefix(output, "viewof$");
109+
variables.push(this.main.define(o, [output], input));
110+
} else if (automutable) {
111+
const o = this.unprefix(output, "mutable ");
112+
const x = `cell ${id}`;
113+
v.define(o, [x], ([mutable]) => mutable); // observe live value
114+
variables.push(
115+
this.main.define(output, inputs, body), // initial value
116+
this.main.define(x, [output], Mutator),
117+
this.main.define(`mutable$${o}`, [x], ([, mutator]) => mutator)
118+
);
119+
}
120+
} else {
121+
for (const o of outputs) {
122+
variables.push(this.main.variable(true).define(o, [vid!], (exports) => exports[o]));
123+
}
84124
}
85125
}
86-
}
87126

88-
function unprefix(name: string, prefix: string): string {
89-
if (!name.startsWith(prefix)) throw new Error(`expected ${prefix}: ${name}`);
90-
return name.slice(prefix.length);
127+
protected unprefix(name: string, prefix: string): string {
128+
if (!name.startsWith(prefix)) throw new Error(`expected ${prefix}: ${name}`);
129+
return name.slice(prefix.length);
130+
}
91131
}
132+

src/runtime/index.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import type {Module} from "@observablehq/runtime";
2-
import {Runtime} from "@observablehq/runtime";
3-
import {fileAttachments} from "./stdlib/fileAttachment.js";
4-
import {library} from "./stdlib/index.js";
5-
61
export * from "./define.js";
72
export * from "./display.js";
83
export * from "./inspect.js";
@@ -12,14 +7,3 @@ export type * from "./stdlib/databaseClient.js";
127
export type * from "./stdlib/fileAttachment.js";
138
export {DatabaseClient} from "./stdlib/databaseClient.js";
149
export {FileAttachment, registerFile} from "./stdlib/fileAttachment.js";
15-
16-
export const runtime = Object.assign(new Runtime({...library, __ojs_runtime: () => runtime}), {fileAttachments});
17-
export const main = (runtime as typeof runtime & {main: Module}).main = runtime.module();
18-
19-
main.constructor.prototype.defines = function (this: Module, name: string): boolean {
20-
return (
21-
this._scope.has(name) ||
22-
this._builtins.has(name) ||
23-
this._runtime._builtin._scope.has(name)
24-
);
25-
};

src/vite/observable.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export function observable({
133133
`<style type="text/css">
134134
@import url("observable:styles/theme-${notebook.theme}.css");
135135
</style><script type="module">
136-
import {define} from "observable:runtime/define";${Array.from(assets)
136+
import {NotebookInstance} from "observable:runtime/define";${Array.from(assets)
137137
.map(
138138
(asset, i) => `
139139
import asset${i + 1} from ${JSON.stringify(`${asset}?url`)};`
@@ -149,12 +149,13 @@ ${Array.from(assets)
149149
]);`
150150
: ""
151151
}
152+
const instance = new NotebookInstance();
152153
${notebook.cells
153154
.filter((cell) => !statics.has(cell))
154155
.map((cell) => {
155156
const transpiled = transpile(cell.value, cell.mode, {resolveFiles: true});
156157
return `
157-
define(
158+
instance.define(
158159
{
159160
root: document.getElementById(\`cell-${cell.id}\`),
160161
expanded: [],

0 commit comments

Comments
 (0)