Skip to content

Commit f9492a6

Browse files
authored
Vendor @observablehq/runtime (#208)
* Localize and rewrite the package `@observablehq/runtime` * Fix the snapshot test * Clean up the test directory * Add tests for the runtime * Revert file `cn.js` * Increase/decrease indegree in a less confusing way * Remove invalid assertions * Add exceptions for unused variables * Update license and add copyright notice to vendored files
1 parent 6cd5634 commit f9492a6

50 files changed

Lines changed: 3228 additions & 145 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LICENCE

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
ISC License
22

3-
Copyright 2025 Recho
3+
Copyright 2025-2026 Recho
44

55
Permission to use, copy, modify, and/or distribute this software for any purpose
66
with or without fee is hereby granted, provided that the above copyright notice
@@ -32,3 +32,22 @@ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
3232
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
3333
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
3434
THIS SOFTWARE.
35+
36+
---
37+
38+
This software includes a vendored copy of @observablehq/runtime, which is
39+
released under the ISC license.
40+
41+
Copyright 2018-2024 Observable, Inc.
42+
43+
Permission to use, copy, modify, and/or distribute this software for any purpose
44+
with or without fee is hereby granted, provided that the above copyright notice
45+
and this permission notice appear in all copies.
46+
47+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
48+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
49+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
50+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
51+
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
52+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
53+
THIS SOFTWARE.

eslint.config.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ export default defineConfig([
4646
...config,
4747
files: ["editor/**/*.{ts,tsx}", "runtime/**/*.ts", "test/**/*.{ts,tsx}", "app/**/*.{ts,tsx}", "lib/**/*.ts"],
4848
})),
49+
{
50+
files: ["editor/**/*.{ts,tsx}", "runtime/**/*.ts", "test/**/*.{ts,tsx}", "app/**/*.{ts,tsx}", "lib/**/*.ts"],
51+
rules: {
52+
"@typescript-eslint/no-unused-vars": [
53+
"error",
54+
{
55+
argsIgnorePattern: "^_",
56+
varsIgnorePattern: "^_",
57+
caughtErrorsIgnorePattern: "^_",
58+
},
59+
],
60+
},
61+
},
4962
{
5063
ignores: ["**/*.recho.js", "test/output/**/*"],
5164
},

lib/runtime/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Runtime
2+
3+
This directory contains a TypeScript rewrite of [`@observablehq/runtime`](https://github.com/observablehq/runtime), distributed under the same ISC license as the original work.

lib/runtime/errors.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Originally developed by Observable, Inc.
2+
// Adapted and modified by Recho from @observablehq/runtime v6.0.0
3+
// Copyright 2018-2024 Observable, Inc.
4+
// Copyright 2025-2026 Recho
5+
// ISC License
6+
7+
export class RuntimeError extends Error {
8+
constructor(
9+
message: string,
10+
public readonly input?: string,
11+
) {
12+
super(message);
13+
// Keep the property non-enumerable.
14+
Object.defineProperties(this, {
15+
name: {
16+
value: "RuntimeError",
17+
enumerable: false,
18+
writable: true,
19+
configurable: true,
20+
},
21+
});
22+
}
23+
}

lib/runtime/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Originally developed by Observable, Inc.
2+
// Adapted and modified by Recho from @observablehq/runtime v6.0.0
3+
// Copyright 2018-2024 Observable, Inc.
4+
// Copyright 2025-2026 Recho
5+
// ISC License
6+
7+
export {RuntimeError} from "./errors.ts";
8+
export {Runtime} from "./runtime.ts";
9+
export type {Builtins, GlobalFunction, ModuleDefinition} from "./runtime.ts";
10+
export type {Module, InjectSpecifier, InjectInput} from "./module.ts";
11+
export type {Variable, Observer, VariableOptions, ObserverInput, VariableDefinition} from "./variable.ts";

lib/runtime/module.ts

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Originally developed by Observable, Inc.
2+
// Adapted and modified by Recho from @observablehq/runtime v6.0.0
3+
// Copyright 2018-2024 Observable, Inc.
4+
// Copyright 2025-2026 Recho
5+
// ISC License
6+
7+
import {constant, identity, rethrow} from "./utils.ts";
8+
import {RuntimeError} from "./errors.ts";
9+
import {
10+
Variable,
11+
no_observer,
12+
variable_stale,
13+
type ObserverInput,
14+
type VariableDefineParameters,
15+
type VariableOptions,
16+
} from "./variable.ts";
17+
import type {Runtime} from "./runtime.ts";
18+
19+
export interface InjectSpecifier {
20+
name: string;
21+
alias?: string;
22+
}
23+
24+
export type InjectInput = string | InjectSpecifier;
25+
26+
/**
27+
* A module represents a namespace for reactive variables. Variables within a
28+
* module can reference each other and form a dependency graph.
29+
*/
30+
export class Module {
31+
// Read-only cross-class access (getters only)
32+
private _runtime: Runtime;
33+
private _scope: Map<string, Variable>;
34+
private _builtins: Map<string, unknown>;
35+
private _source: Module | null;
36+
37+
// Getters for read-only cross-class members
38+
get runtime(): Runtime {
39+
return this._runtime;
40+
}
41+
42+
get scope(): Map<string, Variable> {
43+
return this._scope;
44+
}
45+
46+
get builtins(): Map<string, unknown> {
47+
return this._builtins;
48+
}
49+
50+
get source(): Module | null {
51+
return this._source;
52+
}
53+
54+
/**
55+
* Creates a new module.
56+
* @param runtime The runtime that manages this module
57+
* @param builtins Optional builtin values to add to the module's scope
58+
*/
59+
constructor(runtime: Runtime, builtins: Iterable<[string, unknown]> = []) {
60+
this._runtime = runtime;
61+
this._scope = new Map();
62+
this._builtins = new Map([
63+
["@variable", Variable.VARIABLE],
64+
["invalidation", Variable.INVALIDATION],
65+
["visibility", Variable.VISIBILITY],
66+
...builtins,
67+
]);
68+
this._source = null;
69+
}
70+
71+
/**
72+
* Resolves a variable by name, creating an implicit variable if needed.
73+
* This method looks up variables in the module's scope, builtins, runtime builtins, or global scope.
74+
* @param name The variable name to resolve
75+
* @returns The resolved variable
76+
* @internal
77+
*/
78+
_resolve(name: string): Variable {
79+
let variable = this._scope.get(name);
80+
let value: unknown;
81+
82+
if (!variable) {
83+
variable = new Variable(Variable.Type.IMPLICIT, this);
84+
if (this._builtins.has(name)) {
85+
variable.define(name, constant(this._builtins.get(name)));
86+
} else if (this._runtime.builtin.scope.has(name)) {
87+
variable.import(name, this._runtime.builtin);
88+
} else {
89+
try {
90+
value = this._runtime.global(name);
91+
} catch (error) {
92+
return variable.define(name, rethrow(error));
93+
}
94+
if (value === undefined) {
95+
this._scope.set(name, variable);
96+
variable.name = name;
97+
} else {
98+
variable.define(name, constant(value));
99+
}
100+
}
101+
}
102+
return variable;
103+
}
104+
105+
/**
106+
* Redefines an existing variable in the module.
107+
* @param name The name of the variable to redefine
108+
* @param args The definition arguments (same as Variable.define)
109+
* @returns The redefined variable
110+
* @throws {RuntimeError} If the variable is not defined or is defined multiple times
111+
*/
112+
redefine(name: string | null, definition: unknown): Variable;
113+
redefine(name: string | null, inputs: ArrayLike<string>, definition: unknown): Variable;
114+
redefine(name: string, ...args: [unknown] | [ArrayLike<string>, unknown]): Variable {
115+
const v = this._scope.get(name);
116+
if (!v) throw new RuntimeError(`${name} is not defined`);
117+
if (v.type === Variable.Type.DUPLICATE) throw new RuntimeError(`${name} is defined more than once`);
118+
const targs = [name, ...args] as Parameters<Variable["define"]>;
119+
return v.define(...targs);
120+
}
121+
122+
/**
123+
* Defines a new variable in the module.
124+
* @param args The definition arguments (name, inputs, definition) - see Variable.define for details
125+
* @returns The newly created variable
126+
*/
127+
define(definition: unknown): Variable;
128+
define(name: string | null, definition: unknown): Variable;
129+
define(inputs: ArrayLike<string>, definition: unknown): Variable;
130+
define(name: string | null, inputs: ArrayLike<string>, definition: unknown): Variable;
131+
define(...args: VariableDefineParameters): Variable {
132+
const v = new Variable(Variable.Type.NORMAL, this);
133+
return v.define(...(args as Parameters<Variable["define"]>));
134+
}
135+
136+
/**
137+
* Imports a variable from another module.
138+
* @param args The import arguments (remote name, local name, module) - see Variable.import for details
139+
* @returns The newly created import variable
140+
*/
141+
import(remote: string, module: Module): Variable;
142+
import(remote: string, name: string, module: Module): Variable;
143+
import(remote: string, nameOrModule: string | Module, module?: Module): Variable;
144+
import(...args: [string, Module] | [string, string, Module] | [string, string | Module, Module?]): Variable {
145+
const v = new Variable(Variable.Type.NORMAL, this);
146+
return v.import(...(args as Parameters<Variable["import"]>));
147+
}
148+
149+
/**
150+
* Creates a new variable in the module with an optional observer.
151+
* @param observer Optional observer to monitor the variable's state changes
152+
* @param options Optional variable options (e.g., shadow variables)
153+
* @returns The newly created variable
154+
*/
155+
variable(observer?: ObserverInput, options?: VariableOptions): Variable {
156+
return new Variable(Variable.Type.NORMAL, this, observer, options);
157+
}
158+
159+
/**
160+
* Gets the current value of a variable by name.
161+
* This method waits for the runtime to compute all pending updates before returning the value.
162+
* If the variable becomes stale during computation, it retries until a stable value is obtained.
163+
* @param name The name of the variable
164+
* @returns A promise that resolves to the variable's current value
165+
* @throws {RuntimeError} If the variable is not defined
166+
*/
167+
async value(name: string): Promise<unknown> {
168+
let v = this._scope.get(name);
169+
if (!v) throw new RuntimeError(`${name} is not defined`);
170+
if (v.observer === no_observer) {
171+
v = this.variable(true).define([name], identity);
172+
try {
173+
return await this._revalue(v);
174+
} finally {
175+
v.delete();
176+
}
177+
} else {
178+
return this._revalue(v);
179+
}
180+
}
181+
182+
/**
183+
* Creates a derived module that imports specified variables from another module.
184+
* The derived module is a copy of this module with additional injected variables.
185+
* All transitive dependencies are also copied to maintain the dependency graph.
186+
* @param injects The variables to inject (can be strings or {name, alias} objects)
187+
* @param injectModule The module to import the variables from
188+
* @returns A new derived module with the injected variables
189+
*/
190+
derive(injects: Iterable<InjectInput>, injectModule: Module): Module {
191+
const map = new Map<Module, Module>();
192+
const modules = new Set<Module>();
193+
const copies: Array<[Module, Module]> = [];
194+
195+
const alias = (source: Module): Module => {
196+
let target = map.get(source);
197+
if (target) return target;
198+
target = new Module(source._runtime, source._builtins);
199+
target._source = source;
200+
map.set(source, target);
201+
copies.push([target, source]);
202+
modules.add(source);
203+
return target;
204+
};
205+
206+
const derive = alias(this);
207+
for (const inject of injects) {
208+
const {alias: injectAlias, name} = typeof inject === "object" ? inject : {name: inject, alias: undefined};
209+
derive.import(name, injectAlias == null ? name : injectAlias, injectModule);
210+
}
211+
212+
for (const module of modules) {
213+
for (const [name, variable] of module._scope) {
214+
if (variable.definition === identity) {
215+
if (module === this && derive._scope.has(name)) continue;
216+
const importedModule = variable.inputs[0].module;
217+
if (importedModule._source) alias(importedModule);
218+
}
219+
}
220+
}
221+
222+
for (const [target, source] of copies) {
223+
for (const [name, sourceVariable] of source._scope) {
224+
const targetVariable = target._scope.get(name);
225+
if (targetVariable && targetVariable.type !== Variable.Type.IMPLICIT) continue;
226+
if (sourceVariable.definition === identity) {
227+
const sourceInput = sourceVariable.inputs[0];
228+
const sourceModule = sourceInput.module;
229+
target.import(sourceInput.name!, name, map.get(sourceModule) || sourceModule);
230+
} else {
231+
target.define(
232+
name,
233+
sourceVariable.inputs.map((v) => v.name!),
234+
sourceVariable.definition,
235+
);
236+
}
237+
}
238+
}
239+
240+
return derive;
241+
}
242+
243+
/**
244+
* Adds or updates a builtin value in the module.
245+
* Builtins are predefined values that can be referenced by variables in the module.
246+
* @param name The name of the builtin
247+
* @param value The value of the builtin
248+
*/
249+
builtin(name: string, value: unknown): void {
250+
this._builtins.set(name, value);
251+
}
252+
253+
/**
254+
* Retrieves the current value of a variable, retrying if it becomes stale during computation.
255+
* This is an internal helper method used by the value() method.
256+
* @param variable The variable to get the value from
257+
* @returns A promise that resolves to the variable's current value
258+
* @internal
259+
*/
260+
private async _revalue(variable: Variable): Promise<unknown> {
261+
await this._runtime.compute();
262+
try {
263+
return await variable.promise;
264+
} catch (error) {
265+
if (error === variable_stale) return this._revalue(variable);
266+
throw error;
267+
}
268+
}
269+
}

0 commit comments

Comments
 (0)