@ttsc/playground 0.21.0 → 0.23.0
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/README.md +7 -0
- package/lib/src/compiler/internal/createWorkerCompilerService.d.ts +1 -1
- package/lib/src/compiler/internal/createWorkerCompilerService.js +44 -12
- package/lib/src/compiler/internal/createWorkerCompilerService.js.map +1 -1
- package/lib/src/compiler/loadTypiaSourcePack.d.ts +6 -4
- package/lib/src/compiler/loadTypiaSourcePack.js +101 -13
- package/lib/src/compiler/loadTypiaSourcePack.js.map +1 -1
- package/lib/src/compiler/normalizeError.js +11 -2
- package/lib/src/compiler/normalizeError.js.map +1 -1
- package/lib/src/index.d.ts +2 -2
- package/lib/src/index.js +3 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/react/PlaygroundShell.js +188 -106
- package/lib/src/react/PlaygroundShell.js.map +1 -1
- package/lib/src/react/internal/PlaygroundCompilerLifecycle.d.ts +39 -0
- package/lib/src/react/internal/PlaygroundCompilerLifecycle.js +79 -0
- package/lib/src/react/internal/PlaygroundCompilerLifecycle.js.map +1 -0
- package/lib/src/react/internal/PlaygroundExecutionLifecycle.d.ts +26 -0
- package/lib/src/react/internal/PlaygroundExecutionLifecycle.js +49 -0
- package/lib/src/react/internal/PlaygroundExecutionLifecycle.js.map +1 -0
- package/lib/src/react/internal/recoverTerminalCompilerWorker.d.ts +15 -0
- package/lib/src/react/internal/recoverTerminalCompilerWorker.js +35 -0
- package/lib/src/react/internal/recoverTerminalCompilerWorker.js.map +1 -0
- package/lib/src/sandbox/loadTypiaRuntimePack.d.ts +8 -5
- package/lib/src/sandbox/loadTypiaRuntimePack.js +98 -23
- package/lib/src/sandbox/loadTypiaRuntimePack.js.map +1 -1
- package/lib/src/structures/IInstallTypiaSourcePackOptions.d.ts +5 -1
- package/lib/src/structures/ILoadTypiaRuntimePackOptions.d.ts +7 -0
- package/lib/src/structures/ILoadTypiaRuntimePackOptions.js +3 -0
- package/lib/src/structures/ILoadTypiaRuntimePackOptions.js.map +1 -0
- package/lib/src/structures/IPlaygroundShellProps.d.ts +7 -0
- package/lib/src/structures/index.d.ts +1 -0
- package/lib/src/structures/index.js +1 -0
- package/lib/src/structures/index.js.map +1 -1
- package/package.json +3 -3
- package/src/compiler/internal/createWorkerCompilerService.ts +43 -17
- package/src/compiler/loadTypiaSourcePack.ts +155 -13
- package/src/compiler/normalizeError.ts +15 -2
- package/src/index.ts +8 -2
- package/src/react/PlaygroundShell.tsx +307 -193
- package/src/react/internal/PlaygroundCompilerLifecycle.ts +95 -0
- package/src/react/internal/PlaygroundExecutionLifecycle.ts +55 -0
- package/src/react/internal/recoverTerminalCompilerWorker.ts +41 -0
- package/src/sandbox/loadTypiaRuntimePack.ts +155 -19
- package/src/structures/IInstallTypiaSourcePackOptions.ts +5 -1
- package/src/structures/ILoadTypiaRuntimePackOptions.ts +7 -0
- package/src/structures/IPlaygroundShellProps.ts +7 -0
- package/src/structures/index.ts +1 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "lz-string";
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
|
|
9
|
+
import { normalizeError } from "../compiler/normalizeError";
|
|
9
10
|
import { BUILT_IN_PLAYGROUND_PACKAGES } from "../npm/BUILT_IN_PLAYGROUND_PACKAGES";
|
|
10
11
|
import { collectExternalPackageNames } from "../npm/collectExternalPackageNames";
|
|
11
12
|
import { installPlaygroundDependencies } from "../npm/installPlaygroundDependencies";
|
|
@@ -25,6 +26,12 @@ import { OptionsPanel } from "./OptionsPanel";
|
|
|
25
26
|
import { ResultViewer } from "./ResultViewer";
|
|
26
27
|
import { SourceEditor } from "./SourceEditor";
|
|
27
28
|
import { createCompilerClient } from "./createCompilerClient";
|
|
29
|
+
import {
|
|
30
|
+
type IPlaygroundCompilerGeneration,
|
|
31
|
+
PlaygroundCompilerLifecycle,
|
|
32
|
+
} from "./internal/PlaygroundCompilerLifecycle";
|
|
33
|
+
import { PlaygroundExecutionLifecycle } from "./internal/PlaygroundExecutionLifecycle";
|
|
34
|
+
import { recoverTerminalCompilerWorker } from "./internal/recoverTerminalCompilerWorker";
|
|
28
35
|
|
|
29
36
|
const DEFAULT_OPTIONS: ITransformOptions = {
|
|
30
37
|
typia: true,
|
|
@@ -85,8 +92,9 @@ export function PlaygroundShell({
|
|
|
85
92
|
const debounce = useRef<number | null>(null);
|
|
86
93
|
const shareToastTimer = useRef<number | null>(null);
|
|
87
94
|
const dependencyProgressTimer = useRef<number | null>(null);
|
|
88
|
-
const dependencyInstallChain = useRef<Promise<void>>(Promise.resolve());
|
|
89
95
|
const dependencyAbort = useRef<AbortController | null>(null);
|
|
96
|
+
const compilerLifecycle = useRef(new PlaygroundCompilerLifecycle());
|
|
97
|
+
const executionLifecycle = useRef(new PlaygroundExecutionLifecycle());
|
|
90
98
|
// Exact mounted identities and active requests. A name-only set cannot
|
|
91
99
|
// validate a later transitive range or distinguish an npm alias target.
|
|
92
100
|
const installedDependencies = useRef<
|
|
@@ -103,43 +111,105 @@ export function PlaygroundShell({
|
|
|
103
111
|
const runtimeDependencyFiles = useRef<Record<string, string>>({});
|
|
104
112
|
const sourceVersion = useRef(0);
|
|
105
113
|
const latestSource = useRef(source);
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// flight compile
|
|
109
|
-
//
|
|
110
|
-
// `updateSource` bumps the COMPILE epoch only — typing aborts a stale
|
|
111
|
-
// compile but does not interrupt a click-driven Execute.
|
|
114
|
+
// Compile/run uses an epoch, while Execute uses an abortable lifecycle.
|
|
115
|
+
// The boundaries stay separate so starting Execute does not invalidate an
|
|
116
|
+
// in-flight compile. Source and option changes explicitly invalidate both
|
|
117
|
+
// pipelines because either makes an older result obsolete.
|
|
112
118
|
const compileEpoch = useRef(0);
|
|
113
|
-
const executeEpoch = useRef(0);
|
|
114
|
-
// Retry epoch: each Retry click bumps it; only the latest retry's
|
|
115
|
-
// async block is allowed to write boot state. Prior retries' bodies
|
|
116
|
-
// bail on epoch mismatch so a fast double-Retry can't leave a stale
|
|
117
|
-
// `setBootPhase("ready")` chasing a torn-down connection.
|
|
118
|
-
const retryEpoch = useRef(0);
|
|
119
119
|
|
|
120
120
|
const mergedExtraLibs = useMemo(
|
|
121
121
|
() => ({ ...staticEditorLibs, ...editorExtraLibs }),
|
|
122
122
|
[staticEditorLibs, editorExtraLibs],
|
|
123
123
|
);
|
|
124
124
|
|
|
125
|
+
const clearDependencyGraph = useCallback((publishEditorLibs = true) => {
|
|
126
|
+
installedDependencies.current = new Map();
|
|
127
|
+
dependencyRoots.current = new Set();
|
|
128
|
+
runtimeDependencyFiles.current = {};
|
|
129
|
+
if (publishEditorLibs) setEditorExtraLibs({});
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const invalidateCompilerGeneration = useCallback(
|
|
133
|
+
(
|
|
134
|
+
reason: string,
|
|
135
|
+
expected?: IPlaygroundCompilerGeneration,
|
|
136
|
+
publishState: boolean = true,
|
|
137
|
+
): IPlaygroundCompilerGeneration | undefined => {
|
|
138
|
+
const replacement =
|
|
139
|
+
expected === undefined
|
|
140
|
+
? compilerLifecycle.current.invalidate()
|
|
141
|
+
: compilerLifecycle.current.invalidateIfCurrent(expected);
|
|
142
|
+
if (replacement === undefined) return undefined;
|
|
143
|
+
|
|
144
|
+
compileEpoch.current++;
|
|
145
|
+
executionLifecycle.current.invalidate(reason);
|
|
146
|
+
const abort = dependencyAbort.current;
|
|
147
|
+
dependencyAbort.current = null;
|
|
148
|
+
abort?.abort(createAbortError(reason));
|
|
149
|
+
if (dependencyProgressTimer.current !== null) {
|
|
150
|
+
window.clearTimeout(dependencyProgressTimer.current);
|
|
151
|
+
dependencyProgressTimer.current = null;
|
|
152
|
+
}
|
|
153
|
+
clearDependencyGraph(publishState);
|
|
154
|
+
if (publishState) {
|
|
155
|
+
setDependencyProgress(null);
|
|
156
|
+
setDependencyPackageNames([]);
|
|
157
|
+
setRunning(false);
|
|
158
|
+
setExecuting(false);
|
|
159
|
+
setBundleError(null);
|
|
160
|
+
setBootError(null);
|
|
161
|
+
setBootPhase("booting");
|
|
162
|
+
}
|
|
163
|
+
return replacement;
|
|
164
|
+
},
|
|
165
|
+
[clearDependencyGraph],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const recoverTerminalWorker = useCallback(
|
|
169
|
+
(
|
|
170
|
+
error: unknown,
|
|
171
|
+
generation: IPlaygroundCompilerGeneration,
|
|
172
|
+
): Promise<boolean> => {
|
|
173
|
+
let replacement: IPlaygroundCompilerGeneration | undefined;
|
|
174
|
+
return recoverTerminalCompilerWorker(error, {
|
|
175
|
+
claim: () => {
|
|
176
|
+
replacement = invalidateCompilerGeneration(
|
|
177
|
+
"compiler Worker replacement required",
|
|
178
|
+
generation,
|
|
179
|
+
);
|
|
180
|
+
return replacement !== undefined;
|
|
181
|
+
},
|
|
182
|
+
reset: client.reset,
|
|
183
|
+
fail: (terminalError) => {
|
|
184
|
+
if (replacement?.isCurrent() !== true) return;
|
|
185
|
+
setBootError(terminalError);
|
|
186
|
+
setBootPhase("failed");
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
[client, invalidateCompilerGeneration],
|
|
191
|
+
);
|
|
192
|
+
|
|
125
193
|
const updateSource = useCallback((next: string) => {
|
|
126
194
|
sourceVersion.current++;
|
|
127
195
|
latestSource.current = next;
|
|
128
|
-
//
|
|
129
|
-
// result would be stale) AND an in-flight Execute (the bundled code
|
|
130
|
-
// would not match what's on screen). Without bumping executeEpoch,
|
|
131
|
-
// an Execute whose dependency install was aborted by this typing
|
|
132
|
-
// would silently treat the abort as success and bundle against an
|
|
133
|
-
// incomplete MemFS — emitting JS for packages the user just edited
|
|
134
|
-
// away from, or failing in confusing ways.
|
|
196
|
+
// Typing invalidates both the compile result and any click-driven Execute.
|
|
135
197
|
compileEpoch.current++;
|
|
136
|
-
|
|
198
|
+
executionLifecycle.current.invalidate("source changed");
|
|
137
199
|
dependencyAbort.current?.abort(createAbortError("source changed"));
|
|
200
|
+
setExecuting(false);
|
|
138
201
|
setDependencyProgress(null);
|
|
139
202
|
setDependencyPackageNames([]);
|
|
140
203
|
setSource(next);
|
|
141
204
|
}, []);
|
|
142
205
|
|
|
206
|
+
const updateOptions = useCallback((next: ITransformOptions) => {
|
|
207
|
+
compileEpoch.current++;
|
|
208
|
+
executionLifecycle.current.invalidate("compiler options changed");
|
|
209
|
+
setExecuting(false);
|
|
210
|
+
setOptions(next);
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
143
213
|
// ── Decode source from URL on mount ──
|
|
144
214
|
useEffect(() => {
|
|
145
215
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -153,16 +223,33 @@ export function PlaygroundShell({
|
|
|
153
223
|
}
|
|
154
224
|
}, [updateSource]);
|
|
155
225
|
|
|
226
|
+
// Establish a generation boundary before any boot work starts. Cleanup
|
|
227
|
+
// fences every callback that captured the old client before resetting it.
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
invalidateCompilerGeneration("compiler client changed");
|
|
230
|
+
return () => {
|
|
231
|
+
// Internal recovery may have replaced the generation captured at setup,
|
|
232
|
+
// but every such replacement still belongs to this client.
|
|
233
|
+
invalidateCompilerGeneration(
|
|
234
|
+
"compiler client disposed",
|
|
235
|
+
undefined,
|
|
236
|
+
false,
|
|
237
|
+
);
|
|
238
|
+
void client.reset();
|
|
239
|
+
};
|
|
240
|
+
}, [client, invalidateCompilerGeneration]);
|
|
241
|
+
|
|
156
242
|
// ── Eagerly boot the worker so first compile is instant ──
|
|
157
243
|
useEffect(() => {
|
|
158
244
|
let cancelled = false;
|
|
245
|
+
const generation = compilerLifecycle.current.capture();
|
|
159
246
|
setBootPhase("booting");
|
|
160
247
|
createCompilerService().then(
|
|
161
248
|
() => {
|
|
162
|
-
if (!cancelled) setBootPhase("ready");
|
|
249
|
+
if (!cancelled && generation.isCurrent()) setBootPhase("ready");
|
|
163
250
|
},
|
|
164
251
|
(err: unknown) => {
|
|
165
|
-
if (!cancelled) {
|
|
252
|
+
if (!cancelled && generation.isCurrent()) {
|
|
166
253
|
setBootError(err);
|
|
167
254
|
setBootPhase("failed");
|
|
168
255
|
}
|
|
@@ -178,127 +265,165 @@ export function PlaygroundShell({
|
|
|
178
265
|
input: string,
|
|
179
266
|
version: number = sourceVersion.current,
|
|
180
267
|
): Promise<unknown | null> => {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
268
|
+
const result = await compilerLifecycle.current.enqueue(
|
|
269
|
+
async (generation): Promise<unknown | null> => {
|
|
270
|
+
const isCurrent = (): boolean =>
|
|
271
|
+
generation.isCurrent() && sourceVersion.current === version;
|
|
272
|
+
const resetWorker = (): Promise<boolean> =>
|
|
273
|
+
compilerLifecycle.current.resetWorkerIfCurrent(
|
|
274
|
+
generation,
|
|
275
|
+
client.reset,
|
|
276
|
+
clearDependencyGraph,
|
|
277
|
+
);
|
|
278
|
+
if (!isCurrent()) return null;
|
|
279
|
+
|
|
280
|
+
const firstPassPackageNames = collectExternalPackageNames(
|
|
281
|
+
input,
|
|
282
|
+
preinstalledPackages,
|
|
283
|
+
);
|
|
284
|
+
if (
|
|
285
|
+
dependencyRootDelta(firstPassPackageNames, dependencyRoots.current)
|
|
286
|
+
.changed === false
|
|
287
|
+
)
|
|
288
|
+
return null;
|
|
194
289
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
preinstalledPackages,
|
|
198
|
-
);
|
|
199
|
-
const delta = dependencyRootDelta(
|
|
200
|
-
packageNames,
|
|
201
|
-
dependencyRoots.current,
|
|
202
|
-
);
|
|
203
|
-
if (!delta.changed) return null;
|
|
204
|
-
const replacing = delta.removed.length !== 0;
|
|
205
|
-
const requested = replacing ? packageNames : delta.added;
|
|
290
|
+
await wait(DEPENDENCY_INSTALL_QUIET_MS);
|
|
291
|
+
if (!isCurrent()) return null;
|
|
206
292
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
setDependencyPackageNames(requested);
|
|
212
|
-
const abort = new AbortController();
|
|
213
|
-
dependencyAbort.current = abort;
|
|
214
|
-
let workerMutated = false;
|
|
215
|
-
try {
|
|
216
|
-
const installed = await installPlaygroundDependencies(requested, {
|
|
217
|
-
installedDependencies: replacing
|
|
218
|
-
? []
|
|
219
|
-
: installedDependencies.current.values(),
|
|
220
|
-
ignoredPackages: preinstalledPackages,
|
|
221
|
-
signal: abort.signal,
|
|
222
|
-
onProgress: setDependencyProgress,
|
|
223
|
-
});
|
|
224
|
-
if (sourceVersion.current !== version) return null;
|
|
225
|
-
|
|
226
|
-
if (replacing) {
|
|
227
|
-
workerMutated = true;
|
|
228
|
-
await client.reset();
|
|
229
|
-
installedDependencies.current = new Map();
|
|
230
|
-
dependencyRoots.current = new Set();
|
|
231
|
-
runtimeDependencyFiles.current = {};
|
|
232
|
-
setEditorExtraLibs({});
|
|
233
|
-
}
|
|
234
|
-
if (Object.keys(installed.compilerFiles).length > 0) {
|
|
235
|
-
const service = await createCompilerService();
|
|
236
|
-
workerMutated = true;
|
|
237
|
-
await service.installDependencies({
|
|
238
|
-
files: installed.compilerFiles,
|
|
239
|
-
packages: installed.packages.map(({ name, version }) => ({
|
|
240
|
-
name,
|
|
241
|
-
version,
|
|
242
|
-
})),
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
installedDependencies.current = new Map(
|
|
246
|
-
installed.resolvedDependencies.map(
|
|
247
|
-
(dependency) => [dependency.name, dependency] as const,
|
|
248
|
-
),
|
|
293
|
+
const packageNames = collectExternalPackageNames(
|
|
294
|
+
latestSource.current,
|
|
295
|
+
preinstalledPackages,
|
|
249
296
|
);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
? installed.editorLibs
|
|
254
|
-
: { ...previous, ...installed.editorLibs },
|
|
297
|
+
const delta = dependencyRootDelta(
|
|
298
|
+
packageNames,
|
|
299
|
+
dependencyRoots.current,
|
|
255
300
|
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
dependencyProgressTimer.current = window.setTimeout(() => {
|
|
263
|
-
setDependencyProgress(null);
|
|
264
|
-
setDependencyPackageNames([]);
|
|
301
|
+
if (!delta.changed) return null;
|
|
302
|
+
const replacing = delta.removed.length !== 0;
|
|
303
|
+
const requested = replacing ? packageNames : delta.added;
|
|
304
|
+
|
|
305
|
+
if (dependencyProgressTimer.current !== null) {
|
|
306
|
+
window.clearTimeout(dependencyProgressTimer.current);
|
|
265
307
|
dependencyProgressTimer.current = null;
|
|
266
|
-
}, 350);
|
|
267
|
-
return null;
|
|
268
|
-
} catch (error) {
|
|
269
|
-
if (workerMutated) {
|
|
270
|
-
await client.reset();
|
|
271
|
-
installedDependencies.current = new Map();
|
|
272
|
-
dependencyRoots.current = new Set();
|
|
273
|
-
runtimeDependencyFiles.current = {};
|
|
274
|
-
setEditorExtraLibs({});
|
|
275
308
|
}
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
309
|
+
if (!isCurrent()) return null;
|
|
310
|
+
setDependencyPackageNames(requested);
|
|
311
|
+
const abort = new AbortController();
|
|
312
|
+
dependencyAbort.current = abort;
|
|
313
|
+
let workerMutated = false;
|
|
314
|
+
try {
|
|
315
|
+
const installed = await installPlaygroundDependencies(requested, {
|
|
316
|
+
installedDependencies: replacing
|
|
317
|
+
? []
|
|
318
|
+
: installedDependencies.current.values(),
|
|
319
|
+
ignoredPackages: preinstalledPackages,
|
|
320
|
+
signal: abort.signal,
|
|
321
|
+
onProgress: (progress) => {
|
|
322
|
+
if (isCurrent()) setDependencyProgress(progress);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (!isCurrent()) return null;
|
|
326
|
+
|
|
327
|
+
if (replacing) {
|
|
328
|
+
workerMutated = true;
|
|
329
|
+
if (!(await resetWorker())) return null;
|
|
330
|
+
if (!isCurrent()) return null;
|
|
331
|
+
}
|
|
332
|
+
if (Object.keys(installed.compilerFiles).length > 0) {
|
|
333
|
+
if (!isCurrent()) return null;
|
|
334
|
+
const service = await createCompilerService();
|
|
335
|
+
if (!isCurrent()) return null;
|
|
336
|
+
workerMutated = true;
|
|
337
|
+
const installedForSource =
|
|
338
|
+
await compilerLifecycle.current.mutateWorkerIfCurrent(
|
|
339
|
+
generation,
|
|
340
|
+
() => sourceVersion.current === version,
|
|
341
|
+
() =>
|
|
342
|
+
service.installDependencies({
|
|
343
|
+
files: installed.compilerFiles,
|
|
344
|
+
packages: installed.packages.map(({ name, version }) => ({
|
|
345
|
+
name,
|
|
346
|
+
version,
|
|
347
|
+
})),
|
|
348
|
+
}),
|
|
349
|
+
client.reset,
|
|
350
|
+
clearDependencyGraph,
|
|
351
|
+
);
|
|
352
|
+
if (!installedForSource) return null;
|
|
353
|
+
}
|
|
354
|
+
installedDependencies.current = new Map(
|
|
355
|
+
installed.resolvedDependencies.map(
|
|
356
|
+
(dependency) => [dependency.name, dependency] as const,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
dependencyRoots.current = new Set(packageNames);
|
|
360
|
+
setEditorExtraLibs((previous) =>
|
|
361
|
+
replacing
|
|
362
|
+
? installed.editorLibs
|
|
363
|
+
: { ...previous, ...installed.editorLibs },
|
|
364
|
+
);
|
|
365
|
+
runtimeDependencyFiles.current = replacing
|
|
366
|
+
? installed.runtimeFiles
|
|
367
|
+
: {
|
|
368
|
+
...runtimeDependencyFiles.current,
|
|
369
|
+
...installed.runtimeFiles,
|
|
370
|
+
};
|
|
371
|
+
const progressTimer = window.setTimeout(() => {
|
|
372
|
+
if (
|
|
373
|
+
dependencyProgressTimer.current === progressTimer &&
|
|
374
|
+
generation.isCurrent()
|
|
375
|
+
) {
|
|
376
|
+
setDependencyProgress(null);
|
|
377
|
+
setDependencyPackageNames([]);
|
|
378
|
+
dependencyProgressTimer.current = null;
|
|
379
|
+
}
|
|
380
|
+
}, 350);
|
|
381
|
+
dependencyProgressTimer.current = progressTimer;
|
|
279
382
|
return null;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (!generation.isCurrent()) return null;
|
|
385
|
+
if (workerMutated) {
|
|
386
|
+
if (!(await resetWorker())) return null;
|
|
387
|
+
}
|
|
388
|
+
if (!isCurrent()) return null;
|
|
389
|
+
if (isAbortError(error)) {
|
|
390
|
+
setDependencyProgress(null);
|
|
391
|
+
setDependencyPackageNames([]);
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
setDependencyProgress({
|
|
395
|
+
phase: "error",
|
|
396
|
+
packageName: requested[0],
|
|
397
|
+
completed: 0,
|
|
398
|
+
total: requested.length,
|
|
399
|
+
message: describeUnknownError(error),
|
|
400
|
+
});
|
|
401
|
+
const progressTimer = window.setTimeout(() => {
|
|
402
|
+
if (
|
|
403
|
+
dependencyProgressTimer.current === progressTimer &&
|
|
404
|
+
generation.isCurrent()
|
|
405
|
+
) {
|
|
406
|
+
setDependencyProgress(null);
|
|
407
|
+
setDependencyPackageNames([]);
|
|
408
|
+
dependencyProgressTimer.current = null;
|
|
409
|
+
}
|
|
410
|
+
}, 2400);
|
|
411
|
+
dependencyProgressTimer.current = progressTimer;
|
|
412
|
+
return error;
|
|
413
|
+
} finally {
|
|
414
|
+
if (dependencyAbort.current === abort)
|
|
415
|
+
dependencyAbort.current = null;
|
|
280
416
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
completed: 0,
|
|
285
|
-
total: requested.length,
|
|
286
|
-
message: describeUnknownError(error),
|
|
287
|
-
});
|
|
288
|
-
dependencyProgressTimer.current = window.setTimeout(() => {
|
|
289
|
-
setDependencyProgress(null);
|
|
290
|
-
setDependencyPackageNames([]);
|
|
291
|
-
dependencyProgressTimer.current = null;
|
|
292
|
-
}, 2400);
|
|
293
|
-
return error;
|
|
294
|
-
} finally {
|
|
295
|
-
if (dependencyAbort.current === abort) dependencyAbort.current = null;
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
dependencyInstallChain.current = task.then(() => {});
|
|
299
|
-
return task;
|
|
417
|
+
},
|
|
418
|
+
);
|
|
419
|
+
return result ?? null;
|
|
300
420
|
},
|
|
301
|
-
[
|
|
421
|
+
[
|
|
422
|
+
clearDependencyGraph,
|
|
423
|
+
client,
|
|
424
|
+
createCompilerService,
|
|
425
|
+
preinstalledPackages,
|
|
426
|
+
],
|
|
302
427
|
);
|
|
303
428
|
|
|
304
429
|
// ── Run compile when source / options change ──
|
|
@@ -311,39 +436,48 @@ export function PlaygroundShell({
|
|
|
311
436
|
const run = useCallback(
|
|
312
437
|
async (input: string, opts: ITransformOptions, version: number) => {
|
|
313
438
|
const epoch = ++compileEpoch.current;
|
|
439
|
+
const generation = compilerLifecycle.current.capture();
|
|
314
440
|
setRunning(true);
|
|
315
441
|
try {
|
|
316
442
|
const dependencyError = await installDependenciesForSource(
|
|
317
443
|
input,
|
|
318
444
|
version,
|
|
319
445
|
);
|
|
320
|
-
if (compileEpoch.current !== epoch) return;
|
|
446
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent()) return;
|
|
321
447
|
if (dependencyError) {
|
|
448
|
+
if (await recoverTerminalWorker(dependencyError, generation)) return;
|
|
322
449
|
setResult({
|
|
323
450
|
type: "error",
|
|
324
451
|
target: "javascript",
|
|
325
|
-
value:
|
|
452
|
+
value: normalizeError(dependencyError),
|
|
326
453
|
});
|
|
327
454
|
// Keep prior lintDiagnostics intact — a dependency-install blip
|
|
328
455
|
// shouldn't wipe the user's most recent successful lint output.
|
|
329
456
|
return;
|
|
330
457
|
}
|
|
331
458
|
const service = await createCompilerService();
|
|
459
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent()) return;
|
|
332
460
|
const next = await service.compile({
|
|
333
461
|
source: input,
|
|
334
462
|
options: opts,
|
|
335
463
|
});
|
|
336
|
-
if (compileEpoch.current !== epoch) return;
|
|
464
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent()) return;
|
|
465
|
+
if (
|
|
466
|
+
next.type === "error" &&
|
|
467
|
+
(await recoverTerminalWorker(next.value, generation))
|
|
468
|
+
)
|
|
469
|
+
return;
|
|
337
470
|
setResult(next);
|
|
338
471
|
if (opts.lint !== false) {
|
|
339
472
|
const lint = await service.lint({ source: input, options: opts });
|
|
340
|
-
if (compileEpoch.current !== epoch) return;
|
|
473
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent()) return;
|
|
341
474
|
setLintDiagnostics(lint.diagnostics);
|
|
342
475
|
} else {
|
|
343
476
|
setLintDiagnostics([]);
|
|
344
477
|
}
|
|
345
478
|
} catch (err) {
|
|
346
|
-
if (compileEpoch.current !== epoch) return;
|
|
479
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent()) return;
|
|
480
|
+
if (await recoverTerminalWorker(err, generation)) return;
|
|
347
481
|
// Surface the error in the diagnostics pane via an error result —
|
|
348
482
|
// a transient compile/lint/install rejection (tgrid timeout,
|
|
349
483
|
// message-channel disconnect) must NOT tear the playground into
|
|
@@ -352,7 +486,7 @@ export function PlaygroundShell({
|
|
|
352
486
|
setResult({
|
|
353
487
|
type: "error",
|
|
354
488
|
target: "javascript",
|
|
355
|
-
value:
|
|
489
|
+
value: normalizeError(err),
|
|
356
490
|
});
|
|
357
491
|
// Leave lintDiagnostics alone — clearing them on a transient
|
|
358
492
|
// compile blip would wipe the user's last good lint output.
|
|
@@ -360,13 +494,16 @@ export function PlaygroundShell({
|
|
|
360
494
|
// Only the winning epoch clears the flag. Older pipelines that
|
|
361
495
|
// returned early on an epoch mismatch must NOT clear running, or
|
|
362
496
|
// a fresh in-flight compile would show "ready" while it's still
|
|
363
|
-
// working.
|
|
364
|
-
//
|
|
365
|
-
// does not stick the spinner across pipeline boundaries.
|
|
497
|
+
// working. Execute has its own lifecycle, so it cannot stick this
|
|
498
|
+
// spinner across pipeline boundaries.
|
|
366
499
|
if (compileEpoch.current === epoch) setRunning(false);
|
|
367
500
|
}
|
|
368
501
|
},
|
|
369
|
-
[
|
|
502
|
+
[
|
|
503
|
+
createCompilerService,
|
|
504
|
+
installDependenciesForSource,
|
|
505
|
+
recoverTerminalWorker,
|
|
506
|
+
],
|
|
370
507
|
);
|
|
371
508
|
|
|
372
509
|
useEffect(() => {
|
|
@@ -413,28 +550,11 @@ export function PlaygroundShell({
|
|
|
413
550
|
if (dependencyProgressTimer.current !== null)
|
|
414
551
|
window.clearTimeout(dependencyProgressTimer.current);
|
|
415
552
|
dependencyAbort.current?.abort(createAbortError("playground unmounted"));
|
|
553
|
+
executionLifecycle.current.invalidate("playground unmounted");
|
|
416
554
|
},
|
|
417
555
|
[],
|
|
418
556
|
);
|
|
419
557
|
|
|
420
|
-
// Tear down the Worker (+ its wasm instance) when the component
|
|
421
|
-
// unmounts or workerUrl changes. Without this an SPA navigation away
|
|
422
|
-
// from the playground leaks one Worker per mount; a workerUrl swap
|
|
423
|
-
// leaks the previous Worker forever.
|
|
424
|
-
//
|
|
425
|
-
// Reset the dependency graph too. Its exact versions, requests, roots, and
|
|
426
|
-
// runtime files describe only the worker generation that is being closed.
|
|
427
|
-
//
|
|
428
|
-
useEffect(() => {
|
|
429
|
-
setEditorExtraLibs({});
|
|
430
|
-
return () => {
|
|
431
|
-
void client.reset();
|
|
432
|
-
installedDependencies.current = new Map();
|
|
433
|
-
dependencyRoots.current = new Set();
|
|
434
|
-
runtimeDependencyFiles.current = {};
|
|
435
|
-
};
|
|
436
|
-
}, [client]);
|
|
437
|
-
|
|
438
558
|
const onPickExample = useCallback(
|
|
439
559
|
(id: string) => {
|
|
440
560
|
const example = examples.find((e) => e.id === id);
|
|
@@ -453,10 +573,8 @@ export function PlaygroundShell({
|
|
|
453
573
|
|
|
454
574
|
const onExecute = useCallback(async () => {
|
|
455
575
|
if (!executeBundle) return;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// the source mid-Execute without the running compile being torn down.
|
|
459
|
-
const epoch = ++executeEpoch.current;
|
|
576
|
+
const attempt = executionLifecycle.current.begin();
|
|
577
|
+
const generation = compilerLifecycle.current.capture();
|
|
460
578
|
setExecuting(true);
|
|
461
579
|
setBundleError(null);
|
|
462
580
|
// Clear the previous run's console output up front. Without this the
|
|
@@ -467,7 +585,7 @@ export function PlaygroundShell({
|
|
|
467
585
|
setConsoleMessages([]);
|
|
468
586
|
const messages: IConsoleMessage[] = [];
|
|
469
587
|
const push = (type: IConsoleMessage["type"], args: unknown[]) => {
|
|
470
|
-
if (
|
|
588
|
+
if (!attempt.isCurrent() || !generation.isCurrent()) return;
|
|
471
589
|
messages.push({ type, value: args });
|
|
472
590
|
setConsoleMessages([...messages]);
|
|
473
591
|
};
|
|
@@ -484,18 +602,21 @@ export function PlaygroundShell({
|
|
|
484
602
|
currentSource,
|
|
485
603
|
currentVersion,
|
|
486
604
|
);
|
|
487
|
-
if (
|
|
605
|
+
if (!attempt.isCurrent() || !generation.isCurrent()) return;
|
|
488
606
|
if (dependencyError) {
|
|
607
|
+
if (await recoverTerminalWorker(dependencyError, generation)) return;
|
|
489
608
|
push("error", [dependencyError]);
|
|
490
609
|
return;
|
|
491
610
|
}
|
|
492
611
|
const service = await createCompilerService();
|
|
612
|
+
if (!attempt.isCurrent() || !generation.isCurrent()) return;
|
|
493
613
|
const compiled = await service.bundle({
|
|
494
614
|
source: currentSource,
|
|
495
615
|
options,
|
|
496
616
|
});
|
|
497
|
-
if (
|
|
617
|
+
if (!attempt.isCurrent() || !generation.isCurrent()) return;
|
|
498
618
|
if (compiled.type === "error") {
|
|
619
|
+
if (await recoverTerminalWorker(compiled.value, generation)) return;
|
|
499
620
|
const message =
|
|
500
621
|
typeof compiled.value === "string"
|
|
501
622
|
? compiled.value
|
|
@@ -519,18 +640,18 @@ export function PlaygroundShell({
|
|
|
519
640
|
await executeBundle(code, {
|
|
520
641
|
console: sandboxConsole,
|
|
521
642
|
runtimeFiles: runtimeDependencyFiles.current,
|
|
643
|
+
signal: attempt.signal,
|
|
522
644
|
});
|
|
645
|
+
if (!generation.isCurrent()) return;
|
|
523
646
|
} catch (error) {
|
|
524
647
|
push("error", [error]);
|
|
525
648
|
}
|
|
526
649
|
} catch (error) {
|
|
527
|
-
if (
|
|
650
|
+
if (!attempt.isCurrent() || !generation.isCurrent()) return;
|
|
651
|
+
if (await recoverTerminalWorker(error, generation)) return;
|
|
528
652
|
push("error", [error]);
|
|
529
653
|
} finally {
|
|
530
|
-
|
|
531
|
-
// compileEpoch above. With a separate executeEpoch this is robust
|
|
532
|
-
// against concurrent click + edit interleavings.
|
|
533
|
-
if (executeEpoch.current === epoch) setExecuting(false);
|
|
654
|
+
if (attempt.finish()) setExecuting(false);
|
|
534
655
|
}
|
|
535
656
|
// `source` is intentionally NOT a dep: the body snapshots
|
|
536
657
|
// `latestSource.current` (always fresh ref) rather than reading the
|
|
@@ -542,6 +663,7 @@ export function PlaygroundShell({
|
|
|
542
663
|
executeBundle,
|
|
543
664
|
installDependenciesForSource,
|
|
544
665
|
options,
|
|
666
|
+
recoverTerminalWorker,
|
|
545
667
|
]);
|
|
546
668
|
|
|
547
669
|
const allDiagnostics = useMemo(() => {
|
|
@@ -616,22 +738,21 @@ export function PlaygroundShell({
|
|
|
616
738
|
</pre>
|
|
617
739
|
<button
|
|
618
740
|
onClick={() => {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
741
|
+
const failedGeneration = compilerLifecycle.current.capture();
|
|
742
|
+
const retryGeneration = invalidateCompilerGeneration(
|
|
743
|
+
"compiler retry requested",
|
|
744
|
+
failedGeneration,
|
|
745
|
+
);
|
|
746
|
+
if (retryGeneration === undefined) return;
|
|
624
747
|
void (async () => {
|
|
625
748
|
await client.reset();
|
|
626
|
-
if (
|
|
627
|
-
setBootError(null);
|
|
628
|
-
setBootPhase("booting");
|
|
749
|
+
if (!retryGeneration.isCurrent()) return;
|
|
629
750
|
try {
|
|
630
751
|
await createCompilerService();
|
|
631
|
-
if (
|
|
752
|
+
if (!retryGeneration.isCurrent()) return;
|
|
632
753
|
setBootPhase("ready");
|
|
633
754
|
} catch (err) {
|
|
634
|
-
if (
|
|
755
|
+
if (!retryGeneration.isCurrent()) return;
|
|
635
756
|
setBootError(err);
|
|
636
757
|
setBootPhase("failed");
|
|
637
758
|
}
|
|
@@ -805,7 +926,7 @@ export function PlaygroundShell({
|
|
|
805
926
|
{optionsOpen && (
|
|
806
927
|
<OptionsPanel
|
|
807
928
|
options={options}
|
|
808
|
-
onChange={
|
|
929
|
+
onChange={updateOptions}
|
|
809
930
|
onClose={() => setOptionsOpen(false)}
|
|
810
931
|
toggles={optionToggles}
|
|
811
932
|
/>
|
|
@@ -832,13 +953,6 @@ function describeUnknownError(error: unknown): string {
|
|
|
832
953
|
}
|
|
833
954
|
}
|
|
834
955
|
|
|
835
|
-
function normalizeClientError(error: unknown): unknown {
|
|
836
|
-
if (error instanceof Error) {
|
|
837
|
-
return { name: error.name, message: error.message, stack: error.stack };
|
|
838
|
-
}
|
|
839
|
-
return { name: "Error", message: describeUnknownError(error) };
|
|
840
|
-
}
|
|
841
|
-
|
|
842
956
|
function wait(ms: number): Promise<void> {
|
|
843
957
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
844
958
|
}
|