@ttsc/playground 0.22.0 → 0.24.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
|
@@ -5,6 +5,7 @@ exports.PlaygroundShell = PlaygroundShell;
|
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const lz_string_1 = require("lz-string");
|
|
7
7
|
const react_1 = require("react");
|
|
8
|
+
const normalizeError_1 = require("../compiler/normalizeError");
|
|
8
9
|
const BUILT_IN_PLAYGROUND_PACKAGES_1 = require("../npm/BUILT_IN_PLAYGROUND_PACKAGES");
|
|
9
10
|
const collectExternalPackageNames_1 = require("../npm/collectExternalPackageNames");
|
|
10
11
|
const installPlaygroundDependencies_1 = require("../npm/installPlaygroundDependencies");
|
|
@@ -18,6 +19,9 @@ const OptionsPanel_1 = require("./OptionsPanel");
|
|
|
18
19
|
const ResultViewer_1 = require("./ResultViewer");
|
|
19
20
|
const SourceEditor_1 = require("./SourceEditor");
|
|
20
21
|
const createCompilerClient_1 = require("./createCompilerClient");
|
|
22
|
+
const PlaygroundCompilerLifecycle_1 = require("./internal/PlaygroundCompilerLifecycle");
|
|
23
|
+
const PlaygroundExecutionLifecycle_1 = require("./internal/PlaygroundExecutionLifecycle");
|
|
24
|
+
const recoverTerminalCompilerWorker_1 = require("./internal/recoverTerminalCompilerWorker");
|
|
21
25
|
const DEFAULT_OPTIONS = {
|
|
22
26
|
typia: true,
|
|
23
27
|
lint: true,
|
|
@@ -48,8 +52,9 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
48
52
|
const debounce = (0, react_1.useRef)(null);
|
|
49
53
|
const shareToastTimer = (0, react_1.useRef)(null);
|
|
50
54
|
const dependencyProgressTimer = (0, react_1.useRef)(null);
|
|
51
|
-
const dependencyInstallChain = (0, react_1.useRef)(Promise.resolve());
|
|
52
55
|
const dependencyAbort = (0, react_1.useRef)(null);
|
|
56
|
+
const compilerLifecycle = (0, react_1.useRef)(new PlaygroundCompilerLifecycle_1.PlaygroundCompilerLifecycle());
|
|
57
|
+
const executionLifecycle = (0, react_1.useRef)(new PlaygroundExecutionLifecycle_1.PlaygroundExecutionLifecycle());
|
|
53
58
|
// Exact mounted identities and active requests. A name-only set cannot
|
|
54
59
|
// validate a later transitive range or distinguish an npm alias target.
|
|
55
60
|
const installedDependencies = (0, react_1.useRef)(new Map());
|
|
@@ -64,37 +69,80 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
64
69
|
const runtimeDependencyFiles = (0, react_1.useRef)({});
|
|
65
70
|
const sourceVersion = (0, react_1.useRef)(0);
|
|
66
71
|
const latestSource = (0, react_1.useRef)(source);
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// flight compile
|
|
70
|
-
//
|
|
71
|
-
// `updateSource` bumps the COMPILE epoch only — typing aborts a stale
|
|
72
|
-
// compile but does not interrupt a click-driven Execute.
|
|
72
|
+
// Compile/run uses an epoch, while Execute uses an abortable lifecycle.
|
|
73
|
+
// The boundaries stay separate so starting Execute does not invalidate an
|
|
74
|
+
// in-flight compile. Source and option changes explicitly invalidate both
|
|
75
|
+
// pipelines because either makes an older result obsolete.
|
|
73
76
|
const compileEpoch = (0, react_1.useRef)(0);
|
|
74
|
-
const executeEpoch = (0, react_1.useRef)(0);
|
|
75
|
-
// Retry epoch: each Retry click bumps it; only the latest retry's
|
|
76
|
-
// async block is allowed to write boot state. Prior retries' bodies
|
|
77
|
-
// bail on epoch mismatch so a fast double-Retry can't leave a stale
|
|
78
|
-
// `setBootPhase("ready")` chasing a torn-down connection.
|
|
79
|
-
const retryEpoch = (0, react_1.useRef)(0);
|
|
80
77
|
const mergedExtraLibs = (0, react_1.useMemo)(() => ({ ...staticEditorLibs, ...editorExtraLibs }), [staticEditorLibs, editorExtraLibs]);
|
|
78
|
+
const clearDependencyGraph = (0, react_1.useCallback)((publishEditorLibs = true) => {
|
|
79
|
+
installedDependencies.current = new Map();
|
|
80
|
+
dependencyRoots.current = new Set();
|
|
81
|
+
runtimeDependencyFiles.current = {};
|
|
82
|
+
if (publishEditorLibs)
|
|
83
|
+
setEditorExtraLibs({});
|
|
84
|
+
}, []);
|
|
85
|
+
const invalidateCompilerGeneration = (0, react_1.useCallback)((reason, expected, publishState = true) => {
|
|
86
|
+
const replacement = expected === undefined
|
|
87
|
+
? compilerLifecycle.current.invalidate()
|
|
88
|
+
: compilerLifecycle.current.invalidateIfCurrent(expected);
|
|
89
|
+
if (replacement === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
compileEpoch.current++;
|
|
92
|
+
executionLifecycle.current.invalidate(reason);
|
|
93
|
+
const abort = dependencyAbort.current;
|
|
94
|
+
dependencyAbort.current = null;
|
|
95
|
+
abort?.abort(createAbortError(reason));
|
|
96
|
+
if (dependencyProgressTimer.current !== null) {
|
|
97
|
+
window.clearTimeout(dependencyProgressTimer.current);
|
|
98
|
+
dependencyProgressTimer.current = null;
|
|
99
|
+
}
|
|
100
|
+
clearDependencyGraph(publishState);
|
|
101
|
+
if (publishState) {
|
|
102
|
+
setDependencyProgress(null);
|
|
103
|
+
setDependencyPackageNames([]);
|
|
104
|
+
setRunning(false);
|
|
105
|
+
setExecuting(false);
|
|
106
|
+
setBundleError(null);
|
|
107
|
+
setBootError(null);
|
|
108
|
+
setBootPhase("booting");
|
|
109
|
+
}
|
|
110
|
+
return replacement;
|
|
111
|
+
}, [clearDependencyGraph]);
|
|
112
|
+
const recoverTerminalWorker = (0, react_1.useCallback)((error, generation) => {
|
|
113
|
+
let replacement;
|
|
114
|
+
return (0, recoverTerminalCompilerWorker_1.recoverTerminalCompilerWorker)(error, {
|
|
115
|
+
claim: () => {
|
|
116
|
+
replacement = invalidateCompilerGeneration("compiler Worker replacement required", generation);
|
|
117
|
+
return replacement !== undefined;
|
|
118
|
+
},
|
|
119
|
+
reset: client.reset,
|
|
120
|
+
fail: (terminalError) => {
|
|
121
|
+
if (replacement?.isCurrent() !== true)
|
|
122
|
+
return;
|
|
123
|
+
setBootError(terminalError);
|
|
124
|
+
setBootPhase("failed");
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}, [client, invalidateCompilerGeneration]);
|
|
81
128
|
const updateSource = (0, react_1.useCallback)((next) => {
|
|
82
129
|
sourceVersion.current++;
|
|
83
130
|
latestSource.current = next;
|
|
84
|
-
//
|
|
85
|
-
// result would be stale) AND an in-flight Execute (the bundled code
|
|
86
|
-
// would not match what's on screen). Without bumping executeEpoch,
|
|
87
|
-
// an Execute whose dependency install was aborted by this typing
|
|
88
|
-
// would silently treat the abort as success and bundle against an
|
|
89
|
-
// incomplete MemFS — emitting JS for packages the user just edited
|
|
90
|
-
// away from, or failing in confusing ways.
|
|
131
|
+
// Typing invalidates both the compile result and any click-driven Execute.
|
|
91
132
|
compileEpoch.current++;
|
|
92
|
-
|
|
133
|
+
executionLifecycle.current.invalidate("source changed");
|
|
93
134
|
dependencyAbort.current?.abort(createAbortError("source changed"));
|
|
135
|
+
setExecuting(false);
|
|
94
136
|
setDependencyProgress(null);
|
|
95
137
|
setDependencyPackageNames([]);
|
|
96
138
|
setSource(next);
|
|
97
139
|
}, []);
|
|
140
|
+
const updateOptions = (0, react_1.useCallback)((next) => {
|
|
141
|
+
compileEpoch.current++;
|
|
142
|
+
executionLifecycle.current.invalidate("compiler options changed");
|
|
143
|
+
setExecuting(false);
|
|
144
|
+
setOptions(next);
|
|
145
|
+
}, []);
|
|
98
146
|
// ── Decode source from URL on mount ──
|
|
99
147
|
(0, react_1.useEffect)(() => {
|
|
100
148
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -107,15 +155,27 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
107
155
|
}
|
|
108
156
|
}
|
|
109
157
|
}, [updateSource]);
|
|
158
|
+
// Establish a generation boundary before any boot work starts. Cleanup
|
|
159
|
+
// fences every callback that captured the old client before resetting it.
|
|
160
|
+
(0, react_1.useEffect)(() => {
|
|
161
|
+
invalidateCompilerGeneration("compiler client changed");
|
|
162
|
+
return () => {
|
|
163
|
+
// Internal recovery may have replaced the generation captured at setup,
|
|
164
|
+
// but every such replacement still belongs to this client.
|
|
165
|
+
invalidateCompilerGeneration("compiler client disposed", undefined, false);
|
|
166
|
+
void client.reset();
|
|
167
|
+
};
|
|
168
|
+
}, [client, invalidateCompilerGeneration]);
|
|
110
169
|
// ── Eagerly boot the worker so first compile is instant ──
|
|
111
170
|
(0, react_1.useEffect)(() => {
|
|
112
171
|
let cancelled = false;
|
|
172
|
+
const generation = compilerLifecycle.current.capture();
|
|
113
173
|
setBootPhase("booting");
|
|
114
174
|
createCompilerService().then(() => {
|
|
115
|
-
if (!cancelled)
|
|
175
|
+
if (!cancelled && generation.isCurrent())
|
|
116
176
|
setBootPhase("ready");
|
|
117
177
|
}, (err) => {
|
|
118
|
-
if (!cancelled) {
|
|
178
|
+
if (!cancelled && generation.isCurrent()) {
|
|
119
179
|
setBootError(err);
|
|
120
180
|
setBootPhase("failed");
|
|
121
181
|
}
|
|
@@ -125,13 +185,17 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
125
185
|
};
|
|
126
186
|
}, [createCompilerService]);
|
|
127
187
|
const installDependenciesForSource = (0, react_1.useCallback)(async (input, version = sourceVersion.current) => {
|
|
128
|
-
const
|
|
188
|
+
const result = await compilerLifecycle.current.enqueue(async (generation) => {
|
|
189
|
+
const isCurrent = () => generation.isCurrent() && sourceVersion.current === version;
|
|
190
|
+
const resetWorker = () => compilerLifecycle.current.resetWorkerIfCurrent(generation, client.reset, clearDependencyGraph);
|
|
191
|
+
if (!isCurrent())
|
|
192
|
+
return null;
|
|
129
193
|
const firstPassPackageNames = (0, collectExternalPackageNames_1.collectExternalPackageNames)(input, preinstalledPackages);
|
|
130
194
|
if (dependencyRootDelta(firstPassPackageNames, dependencyRoots.current)
|
|
131
195
|
.changed === false)
|
|
132
196
|
return null;
|
|
133
197
|
await wait(DEPENDENCY_INSTALL_QUIET_MS);
|
|
134
|
-
if (
|
|
198
|
+
if (!isCurrent())
|
|
135
199
|
return null;
|
|
136
200
|
const packageNames = (0, collectExternalPackageNames_1.collectExternalPackageNames)(latestSource.current, preinstalledPackages);
|
|
137
201
|
const delta = dependencyRootDelta(packageNames, dependencyRoots.current);
|
|
@@ -143,6 +207,8 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
143
207
|
window.clearTimeout(dependencyProgressTimer.current);
|
|
144
208
|
dependencyProgressTimer.current = null;
|
|
145
209
|
}
|
|
210
|
+
if (!isCurrent())
|
|
211
|
+
return null;
|
|
146
212
|
setDependencyPackageNames(requested);
|
|
147
213
|
const abort = new AbortController();
|
|
148
214
|
dependencyAbort.current = abort;
|
|
@@ -154,28 +220,36 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
154
220
|
: installedDependencies.current.values(),
|
|
155
221
|
ignoredPackages: preinstalledPackages,
|
|
156
222
|
signal: abort.signal,
|
|
157
|
-
onProgress:
|
|
223
|
+
onProgress: (progress) => {
|
|
224
|
+
if (isCurrent())
|
|
225
|
+
setDependencyProgress(progress);
|
|
226
|
+
},
|
|
158
227
|
});
|
|
159
|
-
if (
|
|
228
|
+
if (!isCurrent())
|
|
160
229
|
return null;
|
|
161
230
|
if (replacing) {
|
|
162
231
|
workerMutated = true;
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
setEditorExtraLibs({});
|
|
232
|
+
if (!(await resetWorker()))
|
|
233
|
+
return null;
|
|
234
|
+
if (!isCurrent())
|
|
235
|
+
return null;
|
|
168
236
|
}
|
|
169
237
|
if (Object.keys(installed.compilerFiles).length > 0) {
|
|
238
|
+
if (!isCurrent())
|
|
239
|
+
return null;
|
|
170
240
|
const service = await createCompilerService();
|
|
241
|
+
if (!isCurrent())
|
|
242
|
+
return null;
|
|
171
243
|
workerMutated = true;
|
|
172
|
-
await service.installDependencies({
|
|
244
|
+
const installedForSource = await compilerLifecycle.current.mutateWorkerIfCurrent(generation, () => sourceVersion.current === version, () => service.installDependencies({
|
|
173
245
|
files: installed.compilerFiles,
|
|
174
246
|
packages: installed.packages.map(({ name, version }) => ({
|
|
175
247
|
name,
|
|
176
248
|
version,
|
|
177
249
|
})),
|
|
178
|
-
});
|
|
250
|
+
}), client.reset, clearDependencyGraph);
|
|
251
|
+
if (!installedForSource)
|
|
252
|
+
return null;
|
|
179
253
|
}
|
|
180
254
|
installedDependencies.current = new Map(installed.resolvedDependencies.map((dependency) => [dependency.name, dependency]));
|
|
181
255
|
dependencyRoots.current = new Set(packageNames);
|
|
@@ -188,21 +262,26 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
188
262
|
...runtimeDependencyFiles.current,
|
|
189
263
|
...installed.runtimeFiles,
|
|
190
264
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
265
|
+
const progressTimer = window.setTimeout(() => {
|
|
266
|
+
if (dependencyProgressTimer.current === progressTimer &&
|
|
267
|
+
generation.isCurrent()) {
|
|
268
|
+
setDependencyProgress(null);
|
|
269
|
+
setDependencyPackageNames([]);
|
|
270
|
+
dependencyProgressTimer.current = null;
|
|
271
|
+
}
|
|
195
272
|
}, 350);
|
|
273
|
+
dependencyProgressTimer.current = progressTimer;
|
|
196
274
|
return null;
|
|
197
275
|
}
|
|
198
276
|
catch (error) {
|
|
277
|
+
if (!generation.isCurrent())
|
|
278
|
+
return null;
|
|
199
279
|
if (workerMutated) {
|
|
200
|
-
await
|
|
201
|
-
|
|
202
|
-
dependencyRoots.current = new Set();
|
|
203
|
-
runtimeDependencyFiles.current = {};
|
|
204
|
-
setEditorExtraLibs({});
|
|
280
|
+
if (!(await resetWorker()))
|
|
281
|
+
return null;
|
|
205
282
|
}
|
|
283
|
+
if (!isCurrent())
|
|
284
|
+
return null;
|
|
206
285
|
if (isAbortError(error)) {
|
|
207
286
|
setDependencyProgress(null);
|
|
208
287
|
setDependencyPackageNames([]);
|
|
@@ -215,11 +294,15 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
215
294
|
total: requested.length,
|
|
216
295
|
message: describeUnknownError(error),
|
|
217
296
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
const progressTimer = window.setTimeout(() => {
|
|
298
|
+
if (dependencyProgressTimer.current === progressTimer &&
|
|
299
|
+
generation.isCurrent()) {
|
|
300
|
+
setDependencyProgress(null);
|
|
301
|
+
setDependencyPackageNames([]);
|
|
302
|
+
dependencyProgressTimer.current = null;
|
|
303
|
+
}
|
|
222
304
|
}, 2400);
|
|
305
|
+
dependencyProgressTimer.current = progressTimer;
|
|
223
306
|
return error;
|
|
224
307
|
}
|
|
225
308
|
finally {
|
|
@@ -227,9 +310,13 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
227
310
|
dependencyAbort.current = null;
|
|
228
311
|
}
|
|
229
312
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
313
|
+
return result ?? null;
|
|
314
|
+
}, [
|
|
315
|
+
clearDependencyGraph,
|
|
316
|
+
client,
|
|
317
|
+
createCompilerService,
|
|
318
|
+
preinstalledPackages,
|
|
319
|
+
]);
|
|
233
320
|
// ── Run compile when source / options change ──
|
|
234
321
|
//
|
|
235
322
|
// `target` (the active tab) is intentionally NOT a trigger here: the
|
|
@@ -239,32 +326,40 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
239
326
|
// click would burn multiple seconds of work per click.
|
|
240
327
|
const run = (0, react_1.useCallback)(async (input, opts, version) => {
|
|
241
328
|
const epoch = ++compileEpoch.current;
|
|
329
|
+
const generation = compilerLifecycle.current.capture();
|
|
242
330
|
setRunning(true);
|
|
243
331
|
try {
|
|
244
332
|
const dependencyError = await installDependenciesForSource(input, version);
|
|
245
|
-
if (compileEpoch.current !== epoch)
|
|
333
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent())
|
|
246
334
|
return;
|
|
247
335
|
if (dependencyError) {
|
|
336
|
+
if (await recoverTerminalWorker(dependencyError, generation))
|
|
337
|
+
return;
|
|
248
338
|
setResult({
|
|
249
339
|
type: "error",
|
|
250
340
|
target: "javascript",
|
|
251
|
-
value:
|
|
341
|
+
value: (0, normalizeError_1.normalizeError)(dependencyError),
|
|
252
342
|
});
|
|
253
343
|
// Keep prior lintDiagnostics intact — a dependency-install blip
|
|
254
344
|
// shouldn't wipe the user's most recent successful lint output.
|
|
255
345
|
return;
|
|
256
346
|
}
|
|
257
347
|
const service = await createCompilerService();
|
|
348
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent())
|
|
349
|
+
return;
|
|
258
350
|
const next = await service.compile({
|
|
259
351
|
source: input,
|
|
260
352
|
options: opts,
|
|
261
353
|
});
|
|
262
|
-
if (compileEpoch.current !== epoch)
|
|
354
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent())
|
|
355
|
+
return;
|
|
356
|
+
if (next.type === "error" &&
|
|
357
|
+
(await recoverTerminalWorker(next.value, generation)))
|
|
263
358
|
return;
|
|
264
359
|
setResult(next);
|
|
265
360
|
if (opts.lint !== false) {
|
|
266
361
|
const lint = await service.lint({ source: input, options: opts });
|
|
267
|
-
if (compileEpoch.current !== epoch)
|
|
362
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent())
|
|
268
363
|
return;
|
|
269
364
|
setLintDiagnostics(lint.diagnostics);
|
|
270
365
|
}
|
|
@@ -273,7 +368,9 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
273
368
|
}
|
|
274
369
|
}
|
|
275
370
|
catch (err) {
|
|
276
|
-
if (compileEpoch.current !== epoch)
|
|
371
|
+
if (compileEpoch.current !== epoch || !generation.isCurrent())
|
|
372
|
+
return;
|
|
373
|
+
if (await recoverTerminalWorker(err, generation))
|
|
277
374
|
return;
|
|
278
375
|
// Surface the error in the diagnostics pane via an error result —
|
|
279
376
|
// a transient compile/lint/install rejection (tgrid timeout,
|
|
@@ -283,7 +380,7 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
283
380
|
setResult({
|
|
284
381
|
type: "error",
|
|
285
382
|
target: "javascript",
|
|
286
|
-
value:
|
|
383
|
+
value: (0, normalizeError_1.normalizeError)(err),
|
|
287
384
|
});
|
|
288
385
|
// Leave lintDiagnostics alone — clearing them on a transient
|
|
289
386
|
// compile blip would wipe the user's last good lint output.
|
|
@@ -292,13 +389,16 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
292
389
|
// Only the winning epoch clears the flag. Older pipelines that
|
|
293
390
|
// returned early on an epoch mismatch must NOT clear running, or
|
|
294
391
|
// a fresh in-flight compile would show "ready" while it's still
|
|
295
|
-
// working.
|
|
296
|
-
//
|
|
297
|
-
// does not stick the spinner across pipeline boundaries.
|
|
392
|
+
// working. Execute has its own lifecycle, so it cannot stick this
|
|
393
|
+
// spinner across pipeline boundaries.
|
|
298
394
|
if (compileEpoch.current === epoch)
|
|
299
395
|
setRunning(false);
|
|
300
396
|
}
|
|
301
|
-
}, [
|
|
397
|
+
}, [
|
|
398
|
+
createCompilerService,
|
|
399
|
+
installDependenciesForSource,
|
|
400
|
+
recoverTerminalWorker,
|
|
401
|
+
]);
|
|
302
402
|
(0, react_1.useEffect)(() => {
|
|
303
403
|
if (bootPhase !== "ready")
|
|
304
404
|
return;
|
|
@@ -342,24 +442,8 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
342
442
|
if (dependencyProgressTimer.current !== null)
|
|
343
443
|
window.clearTimeout(dependencyProgressTimer.current);
|
|
344
444
|
dependencyAbort.current?.abort(createAbortError("playground unmounted"));
|
|
445
|
+
executionLifecycle.current.invalidate("playground unmounted");
|
|
345
446
|
}, []);
|
|
346
|
-
// Tear down the Worker (+ its wasm instance) when the component
|
|
347
|
-
// unmounts or workerUrl changes. Without this an SPA navigation away
|
|
348
|
-
// from the playground leaks one Worker per mount; a workerUrl swap
|
|
349
|
-
// leaks the previous Worker forever.
|
|
350
|
-
//
|
|
351
|
-
// Reset the dependency graph too. Its exact versions, requests, roots, and
|
|
352
|
-
// runtime files describe only the worker generation that is being closed.
|
|
353
|
-
//
|
|
354
|
-
(0, react_1.useEffect)(() => {
|
|
355
|
-
setEditorExtraLibs({});
|
|
356
|
-
return () => {
|
|
357
|
-
void client.reset();
|
|
358
|
-
installedDependencies.current = new Map();
|
|
359
|
-
dependencyRoots.current = new Set();
|
|
360
|
-
runtimeDependencyFiles.current = {};
|
|
361
|
-
};
|
|
362
|
-
}, [client]);
|
|
363
447
|
const onPickExample = (0, react_1.useCallback)((id) => {
|
|
364
448
|
const example = examples.find((e) => e.id === id);
|
|
365
449
|
if (example) {
|
|
@@ -374,10 +458,8 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
374
458
|
const onExecute = (0, react_1.useCallback)(async () => {
|
|
375
459
|
if (!executeBundle)
|
|
376
460
|
return;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// the source mid-Execute without the running compile being torn down.
|
|
380
|
-
const epoch = ++executeEpoch.current;
|
|
461
|
+
const attempt = executionLifecycle.current.begin();
|
|
462
|
+
const generation = compilerLifecycle.current.capture();
|
|
381
463
|
setExecuting(true);
|
|
382
464
|
setBundleError(null);
|
|
383
465
|
// Clear the previous run's console output up front. Without this the
|
|
@@ -388,7 +470,7 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
388
470
|
setConsoleMessages([]);
|
|
389
471
|
const messages = [];
|
|
390
472
|
const push = (type, args) => {
|
|
391
|
-
if (
|
|
473
|
+
if (!attempt.isCurrent() || !generation.isCurrent())
|
|
392
474
|
return;
|
|
393
475
|
messages.push({ type, value: args });
|
|
394
476
|
setConsoleMessages([...messages]);
|
|
@@ -403,20 +485,26 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
403
485
|
const currentSource = latestSource.current;
|
|
404
486
|
const currentVersion = sourceVersion.current;
|
|
405
487
|
const dependencyError = await installDependenciesForSource(currentSource, currentVersion);
|
|
406
|
-
if (
|
|
488
|
+
if (!attempt.isCurrent() || !generation.isCurrent())
|
|
407
489
|
return;
|
|
408
490
|
if (dependencyError) {
|
|
491
|
+
if (await recoverTerminalWorker(dependencyError, generation))
|
|
492
|
+
return;
|
|
409
493
|
push("error", [dependencyError]);
|
|
410
494
|
return;
|
|
411
495
|
}
|
|
412
496
|
const service = await createCompilerService();
|
|
497
|
+
if (!attempt.isCurrent() || !generation.isCurrent())
|
|
498
|
+
return;
|
|
413
499
|
const compiled = await service.bundle({
|
|
414
500
|
source: currentSource,
|
|
415
501
|
options,
|
|
416
502
|
});
|
|
417
|
-
if (
|
|
503
|
+
if (!attempt.isCurrent() || !generation.isCurrent())
|
|
418
504
|
return;
|
|
419
505
|
if (compiled.type === "error") {
|
|
506
|
+
if (await recoverTerminalWorker(compiled.value, generation))
|
|
507
|
+
return;
|
|
420
508
|
const message = typeof compiled.value === "string"
|
|
421
509
|
? compiled.value
|
|
422
510
|
: (compiled.value?.message ??
|
|
@@ -439,22 +527,24 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
439
527
|
await executeBundle(code, {
|
|
440
528
|
console: sandboxConsole,
|
|
441
529
|
runtimeFiles: runtimeDependencyFiles.current,
|
|
530
|
+
signal: attempt.signal,
|
|
442
531
|
});
|
|
532
|
+
if (!generation.isCurrent())
|
|
533
|
+
return;
|
|
443
534
|
}
|
|
444
535
|
catch (error) {
|
|
445
536
|
push("error", [error]);
|
|
446
537
|
}
|
|
447
538
|
}
|
|
448
539
|
catch (error) {
|
|
449
|
-
if (
|
|
540
|
+
if (!attempt.isCurrent() || !generation.isCurrent())
|
|
541
|
+
return;
|
|
542
|
+
if (await recoverTerminalWorker(error, generation))
|
|
450
543
|
return;
|
|
451
544
|
push("error", [error]);
|
|
452
545
|
}
|
|
453
546
|
finally {
|
|
454
|
-
|
|
455
|
-
// compileEpoch above. With a separate executeEpoch this is robust
|
|
456
|
-
// against concurrent click + edit interleavings.
|
|
457
|
-
if (executeEpoch.current === epoch)
|
|
547
|
+
if (attempt.finish())
|
|
458
548
|
setExecuting(false);
|
|
459
549
|
}
|
|
460
550
|
// `source` is intentionally NOT a dep: the body snapshots
|
|
@@ -467,6 +557,7 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
467
557
|
executeBundle,
|
|
468
558
|
installDependenciesForSource,
|
|
469
559
|
options,
|
|
560
|
+
recoverTerminalWorker,
|
|
470
561
|
]);
|
|
471
562
|
const allDiagnostics = (0, react_1.useMemo)(() => {
|
|
472
563
|
const fromCompile = [];
|
|
@@ -534,25 +625,22 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
534
625
|
return String(e);
|
|
535
626
|
}
|
|
536
627
|
})() }), (0, jsx_runtime_1.jsx)("button", { onClick: () => {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const epoch = ++retryEpoch.current;
|
|
628
|
+
const failedGeneration = compilerLifecycle.current.capture();
|
|
629
|
+
const retryGeneration = invalidateCompilerGeneration("compiler retry requested", failedGeneration);
|
|
630
|
+
if (retryGeneration === undefined)
|
|
631
|
+
return;
|
|
542
632
|
void (async () => {
|
|
543
633
|
await client.reset();
|
|
544
|
-
if (
|
|
634
|
+
if (!retryGeneration.isCurrent())
|
|
545
635
|
return;
|
|
546
|
-
setBootError(null);
|
|
547
|
-
setBootPhase("booting");
|
|
548
636
|
try {
|
|
549
637
|
await createCompilerService();
|
|
550
|
-
if (
|
|
638
|
+
if (!retryGeneration.isCurrent())
|
|
551
639
|
return;
|
|
552
640
|
setBootPhase("ready");
|
|
553
641
|
}
|
|
554
642
|
catch (err) {
|
|
555
|
-
if (
|
|
643
|
+
if (!retryGeneration.isCurrent())
|
|
556
644
|
return;
|
|
557
645
|
setBootError(err);
|
|
558
646
|
setBootPhase("failed");
|
|
@@ -572,7 +660,7 @@ function PlaygroundShell({ workerUrl, defaultScript, examples = [], exampleGroup
|
|
|
572
660
|
: result.type === "error"
|
|
573
661
|
? (JSON.stringify(result.value, null, 2) ??
|
|
574
662
|
String(result.value))
|
|
575
|
-
: result.value })) })] })] }), bundleError && ((0, jsx_runtime_1.jsxs)("div", { className: "shrink-0 border-t border-red-300 bg-red-50 px-4 py-1.5 font-mono text-[11px] text-red-700", children: ["Bundle failed \u2014 ", bundleError] })), executeBundle && ((0, jsx_runtime_1.jsxs)("div", { className: "flex h-48 shrink-0 flex-col border-t border-[#c7dff4] bg-[#f7fbff]", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between border-b border-[#d8e7f4] px-4 py-1.5", children: [(0, jsx_runtime_1.jsx)("span", { className: "font-mono text-[11px] text-slate-500", children: "console output" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [consoleMessages.length > 0 && ((0, jsx_runtime_1.jsx)("button", { onClick: () => setConsoleMessages([]), className: "px-2 py-1 font-mono text-[10px] text-slate-500 transition-colors hover:text-[#235a97]", children: "Clear" })), (0, jsx_runtime_1.jsxs)("button", { onClick: onExecute, disabled: executing, className: "rounded-md bg-[#3178c6] px-3 py-1 font-mono text-[11px] text-white transition-colors hover:bg-[#235a97] disabled:opacity-50", title: "Cmd/Ctrl+Enter", children: ["\u25B6 ", executing ? "Executing…" : "Execute"] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex-1 min-h-0", children: (0, jsx_runtime_1.jsx)(ConsoleViewer_1.ConsoleViewer, { messages: consoleMessages }) })] })), (0, jsx_runtime_1.jsx)(DiagnosticsPanel_1.DiagnosticsPanel, { diagnostics: allDiagnostics }), optionsOpen && ((0, jsx_runtime_1.jsx)(OptionsPanel_1.OptionsPanel, { options: options, onChange:
|
|
663
|
+
: result.value })) })] })] }), bundleError && ((0, jsx_runtime_1.jsxs)("div", { className: "shrink-0 border-t border-red-300 bg-red-50 px-4 py-1.5 font-mono text-[11px] text-red-700", children: ["Bundle failed \u2014 ", bundleError] })), executeBundle && ((0, jsx_runtime_1.jsxs)("div", { className: "flex h-48 shrink-0 flex-col border-t border-[#c7dff4] bg-[#f7fbff]", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between border-b border-[#d8e7f4] px-4 py-1.5", children: [(0, jsx_runtime_1.jsx)("span", { className: "font-mono text-[11px] text-slate-500", children: "console output" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [consoleMessages.length > 0 && ((0, jsx_runtime_1.jsx)("button", { onClick: () => setConsoleMessages([]), className: "px-2 py-1 font-mono text-[10px] text-slate-500 transition-colors hover:text-[#235a97]", children: "Clear" })), (0, jsx_runtime_1.jsxs)("button", { onClick: onExecute, disabled: executing, className: "rounded-md bg-[#3178c6] px-3 py-1 font-mono text-[11px] text-white transition-colors hover:bg-[#235a97] disabled:opacity-50", title: "Cmd/Ctrl+Enter", children: ["\u25B6 ", executing ? "Executing…" : "Execute"] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex-1 min-h-0", children: (0, jsx_runtime_1.jsx)(ConsoleViewer_1.ConsoleViewer, { messages: consoleMessages }) })] })), (0, jsx_runtime_1.jsx)(DiagnosticsPanel_1.DiagnosticsPanel, { diagnostics: allDiagnostics }), optionsOpen && ((0, jsx_runtime_1.jsx)(OptionsPanel_1.OptionsPanel, { options: options, onChange: updateOptions, onClose: () => setOptionsOpen(false), toggles: optionToggles })), (0, jsx_runtime_1.jsx)(DependencyProgressModal_1.DependencyProgressModal, { progress: dependencyProgress, packages: dependencyPackageNames })] }));
|
|
576
664
|
}
|
|
577
665
|
function defaultResultCaption(_options) {
|
|
578
666
|
return "dist/playground.js";
|
|
@@ -587,12 +675,6 @@ function describeUnknownError(error) {
|
|
|
587
675
|
return String(error);
|
|
588
676
|
}
|
|
589
677
|
}
|
|
590
|
-
function normalizeClientError(error) {
|
|
591
|
-
if (error instanceof Error) {
|
|
592
|
-
return { name: error.name, message: error.message, stack: error.stack };
|
|
593
|
-
}
|
|
594
|
-
return { name: "Error", message: describeUnknownError(error) };
|
|
595
|
-
}
|
|
596
678
|
function wait(ms) {
|
|
597
679
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
598
680
|
}
|