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