codexivo 0.0.0-stage → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +159 -2
- package/dist/index.js +154 -0
- package/dist/index.js.map +30 -0
- package/dist/worker.js +153 -0
- package/dist/worker.js.map +25 -0
- package/docs/README.md +27 -0
- package/docs/arquitectura.md +174 -0
- package/docs/especificacion-v1.md +701 -0
- package/docs/guia-aprender-desde-el-codigo.md +91 -0
- package/docs/integracion.md +104 -0
- package/docs/lenguaje.md +537 -0
- package/docs/modo-mundo.md +128 -0
- package/docs/recetario.md +159 -0
- package/docs/repl.md +64 -0
- package/package.json +66 -4
- package/src/catalog.ts +256 -0
- package/src/challenge.ts +138 -0
- package/src/engine.ts +104 -0
- package/src/error-codes.ts +427 -0
- package/src/errors.ts +56 -0
- package/src/flow.ts +385 -0
- package/src/index.ts +81 -0
- package/src/printer.ts +133 -0
- package/src/runtime/builtins.ts +1426 -0
- package/src/runtime/compiler.ts +499 -0
- package/src/runtime/costs.ts +96 -0
- package/src/runtime/machine.ts +1521 -0
- package/src/runtime/prelude.ts +230 -0
- package/src/runtime/suggest.ts +33 -0
- package/src/runtime/values.ts +241 -0
- package/src/sandbox-host.ts +310 -0
- package/src/sandbox.ts +95 -0
- package/src/syntax/ast.ts +344 -0
- package/src/syntax/lexer.ts +266 -0
- package/src/syntax/parser.ts +1090 -0
- package/src/syntax/token.ts +215 -0
- package/src/worker.ts +18 -0
- package/src/world.ts +518 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { evaluarReto } from './challenge';
|
|
2
|
+
import type { Challenge, ChallengeResult } from './challenge';
|
|
3
|
+
import type { ErrorInfo } from './errors';
|
|
4
|
+
import { Machine, serialize } from './runtime/machine';
|
|
5
|
+
import type { MachineEvent, MachineOptions, MachineStatus, SerializedValue, Snapshot } from './runtime/machine';
|
|
6
|
+
import { compile } from './runtime/compiler';
|
|
7
|
+
import { parse } from './syntax/parser';
|
|
8
|
+
import type * as ast from './syntax/ast';
|
|
9
|
+
import type { Token } from './syntax/token';
|
|
10
|
+
import { createWorld } from './world';
|
|
11
|
+
import type { WorldDefinition } from './world';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Protocolo del sandbox: los mensajes entre la página y el worker que ejecuta el código. La lógica vive aquí,
|
|
15
|
+
* sin depender de `postMessage`, para poder probarla; `worker.ts` la conecta con el worker y `sandbox.ts` es
|
|
16
|
+
* el cliente tipado para la página.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Qué eventos llegan en cada tanda. */
|
|
20
|
+
export type EventDetail = 'nada' | 'pasos' | 'decisiones' | 'todo';
|
|
21
|
+
|
|
22
|
+
export interface SandboxOptions {
|
|
23
|
+
/** Entrada inicial, además de la que se dé con `entrada`. */
|
|
24
|
+
input?: string[];
|
|
25
|
+
/** Si `leer()` sin entrada pausa la ejecución para pedirla. Por defecto `true`. */
|
|
26
|
+
interactiveInput?: boolean;
|
|
27
|
+
/** Semilla de `aleatorio`. Si no se da, se elige una al cargar y se conserva en los retrocesos. */
|
|
28
|
+
seed?: number;
|
|
29
|
+
maxOperations?: number;
|
|
30
|
+
maxCallDepth?: number;
|
|
31
|
+
maxOutput?: number;
|
|
32
|
+
breakpoints?: number[];
|
|
33
|
+
/** Mundo del modo mundo, en el formato de `WorldDefinition`. */
|
|
34
|
+
world?: WorldDefinition;
|
|
35
|
+
/**
|
|
36
|
+
* `pasos`: solo `paso` y `robot`; `decisiones`: además `decide`; `todo`: todos los eventos; `nada`: ninguno.
|
|
37
|
+
* Por defecto `decisiones`.
|
|
38
|
+
*/
|
|
39
|
+
detail?: EventDetail;
|
|
40
|
+
/** Operaciones por tanda antes de ceder el control. Por defecto 50 000. */
|
|
41
|
+
batchSize?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SandboxRequest =
|
|
45
|
+
| { type: 'cargar'; id: number; source: string; options?: SandboxOptions }
|
|
46
|
+
/** Ejecuta hasta terminar, sin detenerse en los puntos de interrupción. */
|
|
47
|
+
| { type: 'ejecutar'; id: number }
|
|
48
|
+
/** Ejecuta hasta el siguiente punto de interrupción, o hasta terminar. */
|
|
49
|
+
| { type: 'continuar'; id: number }
|
|
50
|
+
| { type: 'paso'; id: number }
|
|
51
|
+
| { type: 'pausar'; id: number }
|
|
52
|
+
/** Vuelve a ejecutar desde el inicio, con la misma entrada y semilla, hasta el paso `step`. */
|
|
53
|
+
| { type: 'ir_a_paso'; id: number; step: number }
|
|
54
|
+
| { type: 'entrada'; id: number; line: string }
|
|
55
|
+
| { type: 'cerrar_entrada'; id: number }
|
|
56
|
+
| { type: 'puntos_de_interrupción'; id: number; lines: number[] }
|
|
57
|
+
| { type: 'evaluar_reto'; id: number; source: string; challenge: Challenge };
|
|
58
|
+
|
|
59
|
+
export type StopReason = 'paso' | 'punto_de_interrupción' | 'pausa' | 'entrada' | 'fin' | 'error' | 'retroceso';
|
|
60
|
+
|
|
61
|
+
export interface Loaded {
|
|
62
|
+
type: 'cargado';
|
|
63
|
+
id: number;
|
|
64
|
+
tokens: Token[];
|
|
65
|
+
program: ast.Program;
|
|
66
|
+
/** Errores léxicos y sintácticos. Si hay alguno, no se puede ejecutar. */
|
|
67
|
+
errors: ErrorInfo[];
|
|
68
|
+
/** Semilla de `aleatorio` de esta carga. */
|
|
69
|
+
seed: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Progress {
|
|
73
|
+
type: 'progreso';
|
|
74
|
+
id: number;
|
|
75
|
+
operations: number;
|
|
76
|
+
steps: number;
|
|
77
|
+
/** Salida nueva desde el mensaje anterior. */
|
|
78
|
+
output: string;
|
|
79
|
+
events: MachineEvent[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Stopped {
|
|
83
|
+
type: 'detenido';
|
|
84
|
+
id: number;
|
|
85
|
+
status: MachineStatus;
|
|
86
|
+
reason: StopReason;
|
|
87
|
+
operations: number;
|
|
88
|
+
steps: number;
|
|
89
|
+
/** Salida nueva desde el mensaje anterior; con `replaceOutput`, toda la salida. */
|
|
90
|
+
output: string;
|
|
91
|
+
/** En un retroceso, la salida reemplaza a la anterior en lugar de agregarse. */
|
|
92
|
+
replaceOutput?: boolean;
|
|
93
|
+
events: MachineEvent[];
|
|
94
|
+
snapshot: Snapshot;
|
|
95
|
+
result?: SerializedValue;
|
|
96
|
+
error?: ErrorInfo;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type SandboxResponse =
|
|
100
|
+
| Loaded
|
|
101
|
+
| Progress
|
|
102
|
+
| Stopped
|
|
103
|
+
| { type: 'hecho'; id: number }
|
|
104
|
+
| { type: 'reto'; id: number; result: ChallengeResult }
|
|
105
|
+
/** El mensaje no se pudo atender (no hay programa cargado, la ejecución sigue en curso…). */
|
|
106
|
+
| { type: 'error'; id: number; message: string };
|
|
107
|
+
|
|
108
|
+
const DETAIL: Record<EventDetail, ReadonlySet<MachineEvent['type']> | 'todo'> = {
|
|
109
|
+
nada: new Set(),
|
|
110
|
+
pasos: new Set(['paso', 'robot']),
|
|
111
|
+
decisiones: new Set(['paso', 'robot', 'decide']),
|
|
112
|
+
todo: 'todo',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const tick = () => new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
116
|
+
|
|
117
|
+
export class SandboxHost {
|
|
118
|
+
private program?: ast.Program;
|
|
119
|
+
private options: SandboxOptions = {};
|
|
120
|
+
private seed = 0;
|
|
121
|
+
private machine?: Machine;
|
|
122
|
+
/** Líneas que se le dieron a `leer()` en esta carga, para repetirlas al retroceder. */
|
|
123
|
+
private inputLog: string[] = [];
|
|
124
|
+
private inputClosed = false;
|
|
125
|
+
private events: MachineEvent[] = [];
|
|
126
|
+
private muted = false;
|
|
127
|
+
private sentOutput = 0;
|
|
128
|
+
private running = false;
|
|
129
|
+
private pauseRequested = false;
|
|
130
|
+
|
|
131
|
+
constructor(private readonly post: (message: SandboxResponse) => void) {}
|
|
132
|
+
|
|
133
|
+
async handle(request: SandboxRequest): Promise<void> {
|
|
134
|
+
switch (request.type) {
|
|
135
|
+
case 'cargar':
|
|
136
|
+
if (this.running) return this.busy(request.id);
|
|
137
|
+
return this.load(request.id, request.source, request.options ?? {});
|
|
138
|
+
case 'evaluar_reto':
|
|
139
|
+
this.post({ type: 'reto', id: request.id, result: evaluarReto(request.source, request.challenge) });
|
|
140
|
+
return;
|
|
141
|
+
case 'pausar':
|
|
142
|
+
this.pauseRequested = this.running;
|
|
143
|
+
this.post({ type: 'hecho', id: request.id });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const machine = this.machine;
|
|
148
|
+
if (!machine) {
|
|
149
|
+
this.post({ type: 'error', id: request.id, message: 'No hay un programa cargado sin errores.' });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
switch (request.type) {
|
|
153
|
+
case 'entrada':
|
|
154
|
+
this.inputLog.push(request.line);
|
|
155
|
+
machine.provideInput(request.line);
|
|
156
|
+
this.post({ type: 'hecho', id: request.id });
|
|
157
|
+
return;
|
|
158
|
+
case 'cerrar_entrada':
|
|
159
|
+
this.inputClosed = true;
|
|
160
|
+
machine.closeInput();
|
|
161
|
+
this.post({ type: 'hecho', id: request.id });
|
|
162
|
+
return;
|
|
163
|
+
case 'puntos_de_interrupción':
|
|
164
|
+
this.options = { ...this.options, breakpoints: request.lines };
|
|
165
|
+
machine.setBreakpoints(request.lines);
|
|
166
|
+
this.post({ type: 'hecho', id: request.id });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.running) return this.busy(request.id);
|
|
171
|
+
switch (request.type) {
|
|
172
|
+
case 'paso':
|
|
173
|
+
machine.step();
|
|
174
|
+
this.stopped(request.id, machine);
|
|
175
|
+
return;
|
|
176
|
+
case 'ejecutar':
|
|
177
|
+
case 'continuar':
|
|
178
|
+
return this.runInBatches(request.id, machine, request.type === 'ejecutar');
|
|
179
|
+
case 'ir_a_paso':
|
|
180
|
+
this.goToStep(request.id, request.step);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private busy(id: number): void {
|
|
186
|
+
this.post({ type: 'error', id, message: 'La ejecución sigue en curso; pausa antes de mandar otra orden.' });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private load(id: number, source: string, options: SandboxOptions): void {
|
|
190
|
+
this.options = options;
|
|
191
|
+
this.seed = (options.seed ?? Math.floor(Math.random() * 2 ** 32)) >>> 0;
|
|
192
|
+
this.inputLog = [];
|
|
193
|
+
this.inputClosed = false;
|
|
194
|
+
this.events = [];
|
|
195
|
+
this.sentOutput = 0;
|
|
196
|
+
const { program, tokens, errors } = parse(source);
|
|
197
|
+
this.program = errors.length === 0 ? program : undefined;
|
|
198
|
+
this.machine = this.program ? this.createMachine() : undefined;
|
|
199
|
+
this.post({ type: 'cargado', id, tokens, program, errors: errors.map(error => error.toJSON()), seed: this.seed });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private createMachine(): Machine {
|
|
203
|
+
const { options } = this;
|
|
204
|
+
const levels = DETAIL[options.detail ?? 'decisiones'];
|
|
205
|
+
const machineOptions: MachineOptions = {
|
|
206
|
+
input: options.input ?? [],
|
|
207
|
+
interactiveInput: options.interactiveInput ?? true,
|
|
208
|
+
seed: this.seed,
|
|
209
|
+
breakpoints: options.breakpoints ?? [],
|
|
210
|
+
onEvent: event => {
|
|
211
|
+
if (this.muted) return;
|
|
212
|
+
if (levels === 'todo' || levels.has(event.type)) this.events.push(event);
|
|
213
|
+
},
|
|
214
|
+
...(options.maxOperations !== undefined ? { maxOperations: options.maxOperations } : {}),
|
|
215
|
+
...(options.maxCallDepth !== undefined ? { maxCallDepth: options.maxCallDepth } : {}),
|
|
216
|
+
...(options.maxOutput !== undefined ? { maxOutput: options.maxOutput } : {}),
|
|
217
|
+
...(options.world ? { world: createWorld(options.world) } : {}),
|
|
218
|
+
};
|
|
219
|
+
return new Machine(compile(this.program!), machineOptions);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async runInBatches(id: number, machine: Machine, ignoreBreakpoints: boolean): Promise<void> {
|
|
223
|
+
this.running = true;
|
|
224
|
+
this.pauseRequested = false;
|
|
225
|
+
const breakpoints = this.options.breakpoints ?? [];
|
|
226
|
+
if (ignoreBreakpoints) machine.setBreakpoints([]);
|
|
227
|
+
try {
|
|
228
|
+
while (true) {
|
|
229
|
+
const status = machine.run(this.options.batchSize ?? 50_000);
|
|
230
|
+
if (status !== 'pausada' || machine.pauseReason !== 'presupuesto') break;
|
|
231
|
+
if (this.pauseRequested) break;
|
|
232
|
+
this.post({
|
|
233
|
+
type: 'progreso',
|
|
234
|
+
id,
|
|
235
|
+
operations: machine.operations,
|
|
236
|
+
steps: machine.steps,
|
|
237
|
+
output: this.newOutput(machine),
|
|
238
|
+
events: this.takeEvents(),
|
|
239
|
+
});
|
|
240
|
+
// Ceder el control deja que llegue un `pausar`.
|
|
241
|
+
await tick();
|
|
242
|
+
if (this.pauseRequested) break;
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
if (ignoreBreakpoints) machine.setBreakpoints(breakpoints);
|
|
246
|
+
this.running = false;
|
|
247
|
+
}
|
|
248
|
+
this.stopped(id, machine, this.pauseRequested && machine.status === 'pausada' ? 'pausa' : undefined);
|
|
249
|
+
this.pauseRequested = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private goToStep(id: number, step: number): void {
|
|
253
|
+
// Como la ejecución es determinista, otra máquina con la misma entrada y semilla llega al mismo estado.
|
|
254
|
+
const machine = this.createMachine();
|
|
255
|
+
for (const line of this.inputLog) machine.provideInput(line);
|
|
256
|
+
if (this.inputClosed) machine.closeInput();
|
|
257
|
+
this.muted = true;
|
|
258
|
+
try {
|
|
259
|
+
while (machine.steps < step && !machine.done && machine.status !== 'esperando_entrada') machine.step();
|
|
260
|
+
} finally {
|
|
261
|
+
this.muted = false;
|
|
262
|
+
}
|
|
263
|
+
this.machine = machine;
|
|
264
|
+
this.events = [];
|
|
265
|
+
this.sentOutput = 0;
|
|
266
|
+
this.stopped(id, machine, 'retroceso', true);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private newOutput(machine: Machine): string {
|
|
270
|
+
const output = machine.output;
|
|
271
|
+
const fresh = output.slice(this.sentOutput);
|
|
272
|
+
this.sentOutput = output.length;
|
|
273
|
+
return fresh;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private takeEvents(): MachineEvent[] {
|
|
277
|
+
const events = this.events;
|
|
278
|
+
this.events = [];
|
|
279
|
+
return events;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private stopped(id: number, machine: Machine, reason?: StopReason, replaceOutput = false): void {
|
|
283
|
+
const status = machine.status;
|
|
284
|
+
const finalReason: StopReason =
|
|
285
|
+
reason ??
|
|
286
|
+
(status === 'terminada'
|
|
287
|
+
? 'fin'
|
|
288
|
+
: status === 'error'
|
|
289
|
+
? 'error'
|
|
290
|
+
: status === 'esperando_entrada'
|
|
291
|
+
? 'entrada'
|
|
292
|
+
: machine.pauseReason === 'punto_de_interrupción'
|
|
293
|
+
? 'punto_de_interrupción'
|
|
294
|
+
: 'paso');
|
|
295
|
+
this.post({
|
|
296
|
+
type: 'detenido',
|
|
297
|
+
id,
|
|
298
|
+
status,
|
|
299
|
+
reason: finalReason,
|
|
300
|
+
operations: machine.operations,
|
|
301
|
+
steps: machine.steps,
|
|
302
|
+
output: this.newOutput(machine),
|
|
303
|
+
...(replaceOutput ? { replaceOutput: true } : {}),
|
|
304
|
+
events: this.takeEvents(),
|
|
305
|
+
snapshot: machine.snapshot(),
|
|
306
|
+
...(status === 'terminada' ? { result: serialize(machine.result) } : {}),
|
|
307
|
+
...(machine.error ? { error: machine.error.toJSON() } : {}),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Challenge, ChallengeResult } from './challenge';
|
|
2
|
+
import type { Loaded, Progress, SandboxOptions, SandboxRequest, SandboxResponse, Stopped } from './sandbox-host';
|
|
3
|
+
|
|
4
|
+
/** Lo que `createSandbox` necesita de un worker (un `Worker` del navegador o de Bun). */
|
|
5
|
+
export interface WorkerLike {
|
|
6
|
+
postMessage(message: SandboxRequest): void;
|
|
7
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<SandboxResponse>) => void): void;
|
|
8
|
+
terminate?(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Sandbox {
|
|
12
|
+
/** Analiza el código; si no tiene errores, queda listo para ejecutarse. */
|
|
13
|
+
load(source: string, options?: SandboxOptions): Promise<Loaded>;
|
|
14
|
+
/** Ejecuta hasta terminar, sin detenerse en los puntos de interrupción. */
|
|
15
|
+
run(): Promise<Stopped>;
|
|
16
|
+
/** Ejecuta hasta el siguiente punto de interrupción, o hasta terminar. */
|
|
17
|
+
resume(): Promise<Stopped>;
|
|
18
|
+
step(): Promise<Stopped>;
|
|
19
|
+
/** Pide pausar; la promesa de `run()` o `resume()` se resuelve con el estado en el que quedó. */
|
|
20
|
+
pause(): Promise<void>;
|
|
21
|
+
/** Retrocede (o avanza) hasta el paso `n` volviendo a ejecutar desde el inicio. */
|
|
22
|
+
goToStep(step: number): Promise<Stopped>;
|
|
23
|
+
provideInput(line: string): Promise<void>;
|
|
24
|
+
closeInput(): Promise<void>;
|
|
25
|
+
setBreakpoints(lines: number[]): Promise<void>;
|
|
26
|
+
evaluarReto(source: string, challenge: Challenge): Promise<ChallengeResult>;
|
|
27
|
+
/** Progreso de cada tanda (operaciones, salida nueva, eventos). Regresa cómo dejar de escuchar. */
|
|
28
|
+
onProgress(listener: (progress: Progress) => void): () => void;
|
|
29
|
+
/** Cada vez que la ejecución se detiene. Regresa cómo dejar de escuchar. */
|
|
30
|
+
onStopped(listener: (stopped: Stopped) => void): () => void;
|
|
31
|
+
terminate(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Body<T extends SandboxRequest['type']> = Omit<Extract<SandboxRequest, { type: T }>, 'id'>;
|
|
35
|
+
|
|
36
|
+
/** Envuelve el worker de `codexivo/worker` con promesas y suscripciones. */
|
|
37
|
+
export const createSandbox = (worker: WorkerLike): Sandbox => {
|
|
38
|
+
let nextId = 1;
|
|
39
|
+
const pending = new Map<number, { resolve: (value: never) => void; reject: (error: Error) => void }>();
|
|
40
|
+
const progressListeners = new Set<(progress: Progress) => void>();
|
|
41
|
+
const stoppedListeners = new Set<(stopped: Stopped) => void>();
|
|
42
|
+
|
|
43
|
+
worker.addEventListener('message', ({ data }) => {
|
|
44
|
+
if (data.type === 'progreso') {
|
|
45
|
+
for (const listener of progressListeners) listener(data);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (data.type === 'detenido') for (const listener of stoppedListeners) listener(data);
|
|
49
|
+
const request = pending.get(data.id);
|
|
50
|
+
if (!request) return;
|
|
51
|
+
pending.delete(data.id);
|
|
52
|
+
if (data.type === 'error') request.reject(new Error(data.message));
|
|
53
|
+
else if (data.type === 'hecho') request.resolve(undefined as never);
|
|
54
|
+
else if (data.type === 'reto') request.resolve(data.result as never);
|
|
55
|
+
else request.resolve(data as never);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const send = <T extends SandboxRequest['type'], R>(type: T, body: Omit<Body<T>, 'type'> = {} as never) =>
|
|
59
|
+
new Promise<R>((resolve, reject) => {
|
|
60
|
+
const id = nextId++;
|
|
61
|
+
pending.set(id, { resolve: resolve as (value: never) => void, reject });
|
|
62
|
+
worker.postMessage({ type, id, ...body } as SandboxRequest);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const subscribe = <T>(listeners: Set<T>, listener: T) => {
|
|
66
|
+
listeners.add(listener);
|
|
67
|
+
return () => listeners.delete(listener);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
load: (source, options) => send<'cargar', Loaded>('cargar', { source, ...(options ? { options } : {}) }),
|
|
72
|
+
run: () => send<'ejecutar', Stopped>('ejecutar'),
|
|
73
|
+
resume: () => send<'continuar', Stopped>('continuar'),
|
|
74
|
+
step: () => send<'paso', Stopped>('paso'),
|
|
75
|
+
pause: () => send<'pausar', void>('pausar'),
|
|
76
|
+
goToStep: step => send<'ir_a_paso', Stopped>('ir_a_paso', { step }),
|
|
77
|
+
provideInput: line => send<'entrada', void>('entrada', { line }),
|
|
78
|
+
closeInput: () => send<'cerrar_entrada', void>('cerrar_entrada'),
|
|
79
|
+
setBreakpoints: lines => send<'puntos_de_interrupción', void>('puntos_de_interrupción', { lines }),
|
|
80
|
+
evaluarReto: (source, challenge) => send<'evaluar_reto', ChallengeResult>('evaluar_reto', { source, challenge }),
|
|
81
|
+
onProgress: listener => {
|
|
82
|
+
const unsubscribe = subscribe(progressListeners, listener);
|
|
83
|
+
return () => void unsubscribe();
|
|
84
|
+
},
|
|
85
|
+
onStopped: listener => {
|
|
86
|
+
const unsubscribe = subscribe(stoppedListeners, listener);
|
|
87
|
+
return () => void unsubscribe();
|
|
88
|
+
},
|
|
89
|
+
terminate: () => {
|
|
90
|
+
worker.terminate?.();
|
|
91
|
+
for (const request of pending.values()) request.reject(new Error('El sandbox se cerró.'));
|
|
92
|
+
pending.clear();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|