@ttsc/playground 0.22.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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/playground",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Reusable React + Web Worker scaffolding for in-browser ttsc playgrounds built on @ttsc/wasm.",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"react": "^18.0.0",
|
|
40
40
|
"react-dom": "^18.0.0",
|
|
41
41
|
"tgrid": "^1.0.3",
|
|
42
|
-
"@ttsc/wasm": "^0.
|
|
42
|
+
"@ttsc/wasm": "^0.23.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@monaco-editor/react": "^4.6.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"rimraf": "^6.1.2",
|
|
53
53
|
"tgrid": "^1.0.3",
|
|
54
54
|
"typescript": "^7.0.2",
|
|
55
|
-
"@ttsc/wasm": "^0.
|
|
55
|
+
"@ttsc/wasm": "^0.23.0"
|
|
56
56
|
},
|
|
57
57
|
"keywords": [
|
|
58
58
|
"ttsc",
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// service — including plugin success/failure interpretation — can be exercised
|
|
7
7
|
// against a fake `IBootResult` without building or booting WASM. The public
|
|
8
8
|
// wrapper supplies the real `bootTtsc` / `parseResult`.
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
import {
|
|
10
|
+
BootTtscWorkerTerminationError,
|
|
11
|
+
type IBootResult,
|
|
12
|
+
type IBootTtscOptions,
|
|
13
|
+
type ITtscCompileResult,
|
|
14
|
+
type ITtscResult,
|
|
14
15
|
} from "@ttsc/wasm";
|
|
15
16
|
|
|
16
17
|
import type { ICompilerService } from "../../structures/ICompilerService";
|
|
@@ -84,26 +85,51 @@ export function createWorkerCompilerService(
|
|
|
84
85
|
compilerOptions: extraCompilerOptions,
|
|
85
86
|
});
|
|
86
87
|
|
|
87
|
-
// Cache the
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
// Cache the runtime separately from the optional source-pack mount. A mount
|
|
89
|
+
// runs after Go is ready, so retrying a failed mount must reuse that runtime
|
|
90
|
+
// instead of calling bootTtsc again in the same Worker.
|
|
91
|
+
//
|
|
92
|
+
// Pre-runtime failures clear the runtime cache so the next call can retry. A
|
|
93
|
+
// post-go.run failure stays cached because the old runtime cannot be stopped
|
|
94
|
+
// and must never receive a replacement Ready bridge in the same Worker. The
|
|
95
|
+
// UI retry resets the compiler client, which terminates this Worker and
|
|
96
|
+
// creates a safe replacement.
|
|
97
|
+
let runtimeBoot: Promise<IBootResult> | null = null;
|
|
98
|
+
function getRuntimeBoot(): Promise<IBootResult> {
|
|
99
|
+
if (runtimeBoot) return runtimeBoot;
|
|
100
|
+
let attempt!: Promise<IBootResult>;
|
|
101
|
+
attempt = (async () =>
|
|
102
|
+
deps.bootTtsc({
|
|
96
103
|
wasmUrl: options.wasmUrl,
|
|
97
104
|
wasmExecUrl: options.wasmExecUrl,
|
|
98
105
|
apiName: options.apiName,
|
|
99
|
-
})
|
|
106
|
+
}))().catch((err) => {
|
|
107
|
+
if (
|
|
108
|
+
!(err instanceof BootTtscWorkerTerminationError) &&
|
|
109
|
+
runtimeBoot === attempt
|
|
110
|
+
) {
|
|
111
|
+
runtimeBoot = null;
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
});
|
|
115
|
+
runtimeBoot = attempt;
|
|
116
|
+
return attempt;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let boot: Promise<IBootResult> | null = null;
|
|
120
|
+
function getBoot(): Promise<IBootResult> {
|
|
121
|
+
if (boot) return boot;
|
|
122
|
+
let attempt!: Promise<IBootResult>;
|
|
123
|
+
attempt = (async () => {
|
|
124
|
+
const result = await getRuntimeBoot();
|
|
100
125
|
if (typiaPlugin?.mount) await typiaPlugin.mount(result.host, workDir);
|
|
101
126
|
return result;
|
|
102
127
|
})().catch((err) => {
|
|
103
|
-
boot = null;
|
|
128
|
+
if (boot === attempt) boot = null;
|
|
104
129
|
throw err;
|
|
105
130
|
});
|
|
106
|
-
|
|
131
|
+
boot = attempt;
|
|
132
|
+
return attempt;
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
// Serialize every MemFS-mutating call. tgrid queues incoming messages, but
|
|
@@ -1,37 +1,179 @@
|
|
|
1
1
|
import type { IInstallTypiaSourcePackOptions } from "../structures/IInstallTypiaSourcePackOptions";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface SourcePackEntry {
|
|
4
|
+
controller: AbortController;
|
|
5
|
+
promise: Promise<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface SourcePackCancellationReason {
|
|
9
|
+
kind: "abort" | "timeout";
|
|
10
|
+
reason?: unknown;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SourcePackCancellation {
|
|
15
|
+
promise: Promise<never>;
|
|
16
|
+
dispose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_SOURCE_PACK_TIMEOUT_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
const packCache = new Map<string, SourcePackEntry>();
|
|
4
22
|
|
|
5
23
|
/**
|
|
6
|
-
* Fetch the typia source pack JSON
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
24
|
+
* Fetch the typia source pack JSON once per URL.
|
|
25
|
+
*
|
|
26
|
+
* Concurrent callers share one load. A caller abort or deadline cancels that
|
|
27
|
+
* shared attempt; rejection removes it from the cache so the next call retries
|
|
28
|
+
* from scratch. The deadline covers both the response and its JSON body.
|
|
10
29
|
*/
|
|
11
30
|
export function loadTypiaSourcePack(
|
|
12
31
|
options: IInstallTypiaSourcePackOptions,
|
|
13
32
|
): Promise<Record<string, string>> {
|
|
33
|
+
const timeoutMs = resolveSourcePackTimeout(options.timeoutMs);
|
|
14
34
|
const cached = packCache.get(options.url);
|
|
15
|
-
if (cached)
|
|
35
|
+
if (cached) {
|
|
36
|
+
attachSourcePackCancellation(cached, options.signal, timeoutMs);
|
|
37
|
+
return cached.promise;
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
17
41
|
if (!fetchImpl) {
|
|
18
42
|
throw new Error(
|
|
19
43
|
"loadTypiaSourcePack: no fetch implementation available in this environment.",
|
|
20
44
|
);
|
|
21
45
|
}
|
|
46
|
+
|
|
22
47
|
const url = options.url;
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
let phase = `fetching ${url}`;
|
|
50
|
+
const cancellation = createSourcePackCancellation(
|
|
51
|
+
controller.signal,
|
|
52
|
+
() => phase,
|
|
53
|
+
);
|
|
54
|
+
let entry!: SourcePackEntry;
|
|
23
55
|
const promise = (async () => {
|
|
24
|
-
const response = await
|
|
56
|
+
const response = await raceSourcePackCancellation(
|
|
57
|
+
fetchImpl(url, { signal: controller.signal }),
|
|
58
|
+
cancellation.promise,
|
|
59
|
+
controller.signal,
|
|
60
|
+
() => phase,
|
|
61
|
+
);
|
|
25
62
|
if (!response.ok) {
|
|
26
63
|
throw new Error(
|
|
27
64
|
`loadTypiaSourcePack: failed to fetch ${url}: ${response.status}`,
|
|
28
65
|
);
|
|
29
66
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
|
|
68
|
+
phase = `reading JSON from ${url}`;
|
|
69
|
+
return (await raceSourcePackCancellation(
|
|
70
|
+
response.json(),
|
|
71
|
+
cancellation.promise,
|
|
72
|
+
controller.signal,
|
|
73
|
+
() => phase,
|
|
74
|
+
)) as Record<string, string>;
|
|
75
|
+
})()
|
|
76
|
+
.catch((error) => {
|
|
77
|
+
if (packCache.get(url) === entry) packCache.delete(url);
|
|
78
|
+
throw error;
|
|
79
|
+
})
|
|
80
|
+
.finally(cancellation.dispose);
|
|
81
|
+
|
|
82
|
+
entry = { controller, promise };
|
|
83
|
+
packCache.set(url, entry);
|
|
84
|
+
attachSourcePackCancellation(entry, options.signal, timeoutMs);
|
|
36
85
|
return promise;
|
|
37
86
|
}
|
|
87
|
+
|
|
88
|
+
function resolveSourcePackTimeout(timeoutMs: number | undefined): number {
|
|
89
|
+
const value = timeoutMs ?? DEFAULT_SOURCE_PACK_TIMEOUT_MS;
|
|
90
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647) {
|
|
91
|
+
throw new RangeError(
|
|
92
|
+
"loadTypiaSourcePack: timeoutMs must be a positive integer no greater than 2147483647.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function attachSourcePackCancellation(
|
|
99
|
+
entry: SourcePackEntry,
|
|
100
|
+
callerSignal: AbortSignal | undefined,
|
|
101
|
+
timeoutMs: number,
|
|
102
|
+
): void {
|
|
103
|
+
const abortFromCaller = (): void => {
|
|
104
|
+
if (!entry.controller.signal.aborted) {
|
|
105
|
+
entry.controller.abort({
|
|
106
|
+
kind: "abort",
|
|
107
|
+
reason: callerSignal?.reason,
|
|
108
|
+
} satisfies SourcePackCancellationReason);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
if (callerSignal?.aborted) abortFromCaller();
|
|
112
|
+
else callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
113
|
+
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
if (!entry.controller.signal.aborted) {
|
|
116
|
+
entry.controller.abort({
|
|
117
|
+
kind: "timeout",
|
|
118
|
+
timeoutMs,
|
|
119
|
+
} satisfies SourcePackCancellationReason);
|
|
120
|
+
}
|
|
121
|
+
}, timeoutMs);
|
|
122
|
+
const cleanup = (): void => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
125
|
+
};
|
|
126
|
+
void entry.promise.then(cleanup, cleanup);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createSourcePackCancellation(
|
|
130
|
+
signal: AbortSignal,
|
|
131
|
+
getPhase: () => string,
|
|
132
|
+
): SourcePackCancellation {
|
|
133
|
+
let rejectCancellation!: (error: Error) => void;
|
|
134
|
+
const promise = new Promise<never>((_resolve, reject) => {
|
|
135
|
+
rejectCancellation = reject;
|
|
136
|
+
});
|
|
137
|
+
const onAbort = (): void => {
|
|
138
|
+
rejectCancellation(sourcePackCancellationError(signal, getPhase()));
|
|
139
|
+
};
|
|
140
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
141
|
+
if (signal.aborted) onAbort();
|
|
142
|
+
return {
|
|
143
|
+
promise,
|
|
144
|
+
dispose: () => signal.removeEventListener("abort", onAbort),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function raceSourcePackCancellation<T>(
|
|
149
|
+
work: Promise<T>,
|
|
150
|
+
cancellation: Promise<never>,
|
|
151
|
+
signal: AbortSignal,
|
|
152
|
+
getPhase: () => string,
|
|
153
|
+
): Promise<T> {
|
|
154
|
+
try {
|
|
155
|
+
return await Promise.race([work, cancellation]);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (signal.aborted) {
|
|
158
|
+
throw sourcePackCancellationError(signal, getPhase());
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sourcePackCancellationError(
|
|
165
|
+
signal: AbortSignal,
|
|
166
|
+
phase: string,
|
|
167
|
+
): Error {
|
|
168
|
+
const reason = signal.reason as SourcePackCancellationReason | undefined;
|
|
169
|
+
if (reason?.kind === "timeout") {
|
|
170
|
+
return new Error(
|
|
171
|
+
`loadTypiaSourcePack: timed out after ${reason.timeoutMs}ms while ${phase}.`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const error = new Error(`loadTypiaSourcePack: aborted while ${phase}.`);
|
|
176
|
+
const cause = reason?.kind === "abort" ? reason.reason : signal.reason;
|
|
177
|
+
if (cause !== undefined) (error as Error & { cause?: unknown }).cause = cause;
|
|
178
|
+
return error;
|
|
179
|
+
}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
/** Best-effort error normalization for tgrid transport. */
|
|
2
2
|
export function normalizeError(error: unknown): unknown {
|
|
3
|
-
if (error instanceof Error)
|
|
4
|
-
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
const normalized: {
|
|
5
|
+
code?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
name: string;
|
|
8
|
+
stack?: string;
|
|
9
|
+
} = {
|
|
10
|
+
name: error.name,
|
|
11
|
+
message: error.message,
|
|
12
|
+
stack: error.stack,
|
|
13
|
+
};
|
|
14
|
+
const code = (error as Error & { code?: unknown }).code;
|
|
15
|
+
if (typeof code === "string") normalized.code = code;
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
5
18
|
if (
|
|
6
19
|
error &&
|
|
7
20
|
typeof error === "object" &&
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,10 @@ export { DEFAULT_WORK_DIR } from "./compiler/DEFAULT_WORK_DIR";
|
|
|
24
24
|
export { installDependenciesIntoMemFS } from "./compiler/installDependenciesIntoMemFS";
|
|
25
25
|
export { installTypiaSourcePack } from "./compiler/installTypiaSourcePack";
|
|
26
26
|
export { lineColumnOf } from "./compiler/lineColumnOf";
|
|
27
|
-
export {
|
|
27
|
+
export {
|
|
28
|
+
DEFAULT_SOURCE_PACK_TIMEOUT_MS,
|
|
29
|
+
loadTypiaSourcePack,
|
|
30
|
+
} from "./compiler/loadTypiaSourcePack";
|
|
28
31
|
export { mapDiagnostic } from "./compiler/mapDiagnostic";
|
|
29
32
|
export { normalizeError } from "./compiler/normalizeError";
|
|
30
33
|
export { normalizeNodeModulePath } from "./compiler/normalizeNodeModulePath";
|
|
@@ -38,7 +41,10 @@ export { packageNameFromSpecifier } from "./npm/packageNameFromSpecifier";
|
|
|
38
41
|
|
|
39
42
|
// ── typia runtime sandbox (CJS-flavoured require over a pack JSON) ────────
|
|
40
43
|
export { createSandboxRequire } from "./sandbox/createSandboxRequire";
|
|
41
|
-
export {
|
|
44
|
+
export {
|
|
45
|
+
DEFAULT_RUNTIME_PACK_TIMEOUT_MS,
|
|
46
|
+
loadTypiaRuntimePack,
|
|
47
|
+
} from "./sandbox/loadTypiaRuntimePack";
|
|
42
48
|
|
|
43
49
|
// ── React UI surface ──────────────────────────────────────────────────────
|
|
44
50
|
export { ConsoleViewer } from "./react/ConsoleViewer";
|