aivee 0.1.0-alpha.0 → 0.1.0-dev.d34a6ba
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/LICENSE +21 -21
- package/README.md +120 -118
- package/docs/architecture.md +401 -401
- package/docs/aspect-ratio-presets.md +72 -72
- package/docs/releasing.md +19 -19
- package/docs/usage.md +1193 -1193
- package/package.json +52 -52
- package/requirements-otio.txt +1 -1
- package/src/capabilities.mjs +307 -307
- package/src/cli.mjs +646 -646
- package/src/doctor.mjs +133 -133
- package/src/edits/analysis-to-edl.mjs +43 -43
- package/src/edits/detect-silence.mjs +51 -51
- package/src/edits/find-mistake-candidates.mjs +42 -42
- package/src/edits/plan-shot-placements.mjs +105 -105
- package/src/edits/plan-silence-cuts.mjs +49 -49
- package/src/edits/retime-transcript.mjs +105 -105
- package/src/files.mjs +80 -80
- package/src/index.mjs +44 -44
- package/src/interchange/otio.mjs +204 -204
- package/src/interchange/otio_bridge.py +521 -521
- package/src/media/analysis-provider.mjs +75 -75
- package/src/media/compatibility.mjs +45 -45
- package/src/media/ffmpeg-capabilities.mjs +129 -129
- package/src/media/frame-rate.mjs +58 -58
- package/src/media/frame-time.mjs +22 -22
- package/src/media/preview-assets.mjs +350 -350
- package/src/media/probe.mjs +149 -149
- package/src/media/render-cuts.mjs +90 -90
- package/src/media/scene-detection.mjs +107 -107
- package/src/media/scene-review.mjs +205 -205
- package/src/project/assets.mjs +1194 -1194
- package/src/project/create.mjs +100 -100
- package/src/project/presets.mjs +44 -44
- package/src/render/checkpoints.mjs +192 -192
- package/src/render/formats.mjs +163 -163
- package/src/render/hyperframes.mjs +590 -590
- package/src/render/process.mjs +111 -111
- package/src/render/progress.mjs +46 -46
- package/src/render/timeline-control.mjs +53 -53
- package/src/timeline/assemble.mjs +119 -119
- package/src/timeline/audio-effects.mjs +75 -75
- package/src/timeline/audio-mastering.mjs +236 -236
- package/src/timeline/audio-mix.mjs +105 -105
- package/src/timeline/audio-track-effects.mjs +139 -139
- package/src/timeline/audio-volume.mjs +63 -63
- package/src/timeline/background.mjs +8 -8
- package/src/timeline/blend-mode.mjs +34 -34
- package/src/timeline/caption-layouts.mjs +62 -61
- package/src/timeline/captions.mjs +80 -80
- package/src/timeline/edit.mjs +965 -965
- package/src/timeline/fingerprint.mjs +6 -6
- package/src/timeline/from-edl.mjs +31 -31
- package/src/timeline/history-store.mjs +281 -281
- package/src/timeline/history.mjs +69 -69
- package/src/timeline/inspect.mjs +117 -117
- package/src/timeline/keyframes.mjs +158 -158
- package/src/timeline/layers.mjs +18 -18
- package/src/timeline/mask.mjs +53 -53
- package/src/timeline/project.mjs +52 -52
- package/src/timeline/render.mjs +759 -759
- package/src/timeline/sidechain-render.mjs +209 -209
- package/src/timeline/source-fit.mjs +15 -15
- package/src/timeline/speech-bubble.mjs +26 -26
- package/src/timeline/speed-ramp.mjs +188 -188
- package/src/timeline/track-state.mjs +22 -22
- package/src/timeline/transition.mjs +34 -34
- package/src/timeline/validate.mjs +322 -322
- package/src/transcript/corrections.mjs +21 -21
- package/src/transcript/normalize.mjs +17 -17
- package/src/transcript/provider.mjs +42 -42
- package/src/transcript/subtitles.mjs +57 -57
- package/src/transcript/validate.mjs +39 -39
package/src/render/process.mjs
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
export function runProcess(command, args, { cwd, signal, stdio = 'inherit', windowsHide = true, onStdout, onStderr } = {}) {
|
|
4
|
-
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
5
|
-
return new Promise((resolve, reject) => {
|
|
6
|
-
const capture = stdio === 'capture';
|
|
7
|
-
const child = spawn(command, args, {
|
|
8
|
-
cwd,
|
|
9
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : stdio,
|
|
10
|
-
windowsHide,
|
|
11
|
-
detached: process.platform !== 'win32',
|
|
12
|
-
});
|
|
13
|
-
let settled = false;
|
|
14
|
-
let killPromise;
|
|
15
|
-
let forceTimer;
|
|
16
|
-
const outputLimit = 16 * 1024 * 1024;
|
|
17
|
-
const output = { stdout: [], stderr: [], stdoutBytes: 0, stderrBytes: 0 };
|
|
18
|
-
const captureOutput = (stream, key, byteKey, onChunk) => {
|
|
19
|
-
stream?.on('data', (chunk) => {
|
|
20
|
-
onChunk?.(chunk);
|
|
21
|
-
if (output[byteKey] >= outputLimit) return;
|
|
22
|
-
const retained = chunk.subarray(0, outputLimit - output[byteKey]);
|
|
23
|
-
output[key].push(retained);
|
|
24
|
-
output[byteKey] += retained.length;
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
if (capture) {
|
|
28
|
-
captureOutput(child.stdout, 'stdout', 'stdoutBytes', onStdout);
|
|
29
|
-
captureOutput(child.stderr, 'stderr', 'stderrBytes', onStderr);
|
|
30
|
-
}
|
|
31
|
-
const finish = (callback, value) => {
|
|
32
|
-
if (settled) return;
|
|
33
|
-
settled = true;
|
|
34
|
-
if (forceTimer) clearTimeout(forceTimer);
|
|
35
|
-
signal?.removeEventListener('abort', abort);
|
|
36
|
-
callback(value);
|
|
37
|
-
};
|
|
38
|
-
const abort = () => {
|
|
39
|
-
if (killPromise) return;
|
|
40
|
-
killPromise = terminateProcessTree(child, (timer) => { forceTimer = timer; });
|
|
41
|
-
};
|
|
42
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
43
|
-
child.once('error', (error) => {
|
|
44
|
-
finish(reject, signal?.aborted ? abortReason(signal) : error);
|
|
45
|
-
});
|
|
46
|
-
child.once('close', async (code, signalName) => {
|
|
47
|
-
if (killPromise) await killPromise;
|
|
48
|
-
if (signal?.aborted) {
|
|
49
|
-
finish(reject, abortReason(signal));
|
|
50
|
-
} else if (code === 0) {
|
|
51
|
-
finish(resolve, capture ? { stdout: Buffer.concat(output.stdout).toString('utf8'), stderr: Buffer.concat(output.stderr).toString('utf8') } : true);
|
|
52
|
-
} else {
|
|
53
|
-
const stderr = capture ? Buffer.concat(output.stderr).toString('utf8').trim() : '';
|
|
54
|
-
finish(reject, new Error(`${command} exited with ${signalName ? `signal ${signalName}` : `code ${code}`}.${stderr ? ` ${stderr}` : ''}`));
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
if (signal?.aborted) abort();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function terminateProcessTree(child, setForceTimer) {
|
|
62
|
-
if (!child.pid) {
|
|
63
|
-
child.kill('SIGTERM');
|
|
64
|
-
return Promise.resolve();
|
|
65
|
-
}
|
|
66
|
-
if (process.platform === 'win32') {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
69
|
-
stdio: 'ignore',
|
|
70
|
-
windowsHide: true,
|
|
71
|
-
});
|
|
72
|
-
let settled = false;
|
|
73
|
-
const fallbackTimer = setTimeout(() => {
|
|
74
|
-
if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
|
|
75
|
-
}, 2000);
|
|
76
|
-
setForceTimer(fallbackTimer);
|
|
77
|
-
const finish = () => {
|
|
78
|
-
if (settled) return;
|
|
79
|
-
settled = true;
|
|
80
|
-
resolve();
|
|
81
|
-
};
|
|
82
|
-
killer.once('error', () => {
|
|
83
|
-
child.kill('SIGTERM');
|
|
84
|
-
finish();
|
|
85
|
-
});
|
|
86
|
-
killer.once('close', (code) => {
|
|
87
|
-
if (code !== 0 && child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
|
|
88
|
-
finish();
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
95
|
-
} catch {
|
|
96
|
-
child.kill('SIGTERM');
|
|
97
|
-
}
|
|
98
|
-
const forceTimer = setTimeout(() => {
|
|
99
|
-
try { process.kill(-child.pid, 'SIGKILL'); } catch { /* process tree already exited */ }
|
|
100
|
-
}, 1500);
|
|
101
|
-
forceTimer.unref();
|
|
102
|
-
setForceTimer(forceTimer);
|
|
103
|
-
return Promise.resolve();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function abortReason(signal) {
|
|
107
|
-
if (signal.reason instanceof Error) return signal.reason;
|
|
108
|
-
const error = new Error('Render cancelled.');
|
|
109
|
-
error.name = 'AbortError';
|
|
110
|
-
return error;
|
|
111
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function runProcess(command, args, { cwd, signal, stdio = 'inherit', windowsHide = true, onStdout, onStderr } = {}) {
|
|
4
|
+
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const capture = stdio === 'capture';
|
|
7
|
+
const child = spawn(command, args, {
|
|
8
|
+
cwd,
|
|
9
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : stdio,
|
|
10
|
+
windowsHide,
|
|
11
|
+
detached: process.platform !== 'win32',
|
|
12
|
+
});
|
|
13
|
+
let settled = false;
|
|
14
|
+
let killPromise;
|
|
15
|
+
let forceTimer;
|
|
16
|
+
const outputLimit = 16 * 1024 * 1024;
|
|
17
|
+
const output = { stdout: [], stderr: [], stdoutBytes: 0, stderrBytes: 0 };
|
|
18
|
+
const captureOutput = (stream, key, byteKey, onChunk) => {
|
|
19
|
+
stream?.on('data', (chunk) => {
|
|
20
|
+
onChunk?.(chunk);
|
|
21
|
+
if (output[byteKey] >= outputLimit) return;
|
|
22
|
+
const retained = chunk.subarray(0, outputLimit - output[byteKey]);
|
|
23
|
+
output[key].push(retained);
|
|
24
|
+
output[byteKey] += retained.length;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
if (capture) {
|
|
28
|
+
captureOutput(child.stdout, 'stdout', 'stdoutBytes', onStdout);
|
|
29
|
+
captureOutput(child.stderr, 'stderr', 'stderrBytes', onStderr);
|
|
30
|
+
}
|
|
31
|
+
const finish = (callback, value) => {
|
|
32
|
+
if (settled) return;
|
|
33
|
+
settled = true;
|
|
34
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
35
|
+
signal?.removeEventListener('abort', abort);
|
|
36
|
+
callback(value);
|
|
37
|
+
};
|
|
38
|
+
const abort = () => {
|
|
39
|
+
if (killPromise) return;
|
|
40
|
+
killPromise = terminateProcessTree(child, (timer) => { forceTimer = timer; });
|
|
41
|
+
};
|
|
42
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
43
|
+
child.once('error', (error) => {
|
|
44
|
+
finish(reject, signal?.aborted ? abortReason(signal) : error);
|
|
45
|
+
});
|
|
46
|
+
child.once('close', async (code, signalName) => {
|
|
47
|
+
if (killPromise) await killPromise;
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
finish(reject, abortReason(signal));
|
|
50
|
+
} else if (code === 0) {
|
|
51
|
+
finish(resolve, capture ? { stdout: Buffer.concat(output.stdout).toString('utf8'), stderr: Buffer.concat(output.stderr).toString('utf8') } : true);
|
|
52
|
+
} else {
|
|
53
|
+
const stderr = capture ? Buffer.concat(output.stderr).toString('utf8').trim() : '';
|
|
54
|
+
finish(reject, new Error(`${command} exited with ${signalName ? `signal ${signalName}` : `code ${code}`}.${stderr ? ` ${stderr}` : ''}`));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (signal?.aborted) abort();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function terminateProcessTree(child, setForceTimer) {
|
|
62
|
+
if (!child.pid) {
|
|
63
|
+
child.kill('SIGTERM');
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
69
|
+
stdio: 'ignore',
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
});
|
|
72
|
+
let settled = false;
|
|
73
|
+
const fallbackTimer = setTimeout(() => {
|
|
74
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
|
|
75
|
+
}, 2000);
|
|
76
|
+
setForceTimer(fallbackTimer);
|
|
77
|
+
const finish = () => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
resolve();
|
|
81
|
+
};
|
|
82
|
+
killer.once('error', () => {
|
|
83
|
+
child.kill('SIGTERM');
|
|
84
|
+
finish();
|
|
85
|
+
});
|
|
86
|
+
killer.once('close', (code) => {
|
|
87
|
+
if (code !== 0 && child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
|
|
88
|
+
finish();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
95
|
+
} catch {
|
|
96
|
+
child.kill('SIGTERM');
|
|
97
|
+
}
|
|
98
|
+
const forceTimer = setTimeout(() => {
|
|
99
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { /* process tree already exited */ }
|
|
100
|
+
}, 1500);
|
|
101
|
+
forceTimer.unref();
|
|
102
|
+
setForceTimer(forceTimer);
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function abortReason(signal) {
|
|
107
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
108
|
+
const error = new Error('Render cancelled.');
|
|
109
|
+
error.name = 'AbortError';
|
|
110
|
+
return error;
|
|
111
|
+
}
|
package/src/render/progress.mjs
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
export function emitRenderProgress(onProgress, stage, phase, details = {}) {
|
|
2
|
-
if (!onProgress) return;
|
|
3
|
-
try {
|
|
4
|
-
const percent = phase === 'completed' ? 100 : phase === 'started' ? 0 : undefined;
|
|
5
|
-
const result = onProgress({ stage, phase, ...(percent !== undefined ? { percent } : {}), ...details });
|
|
6
|
-
if (result && typeof result.then === 'function') Promise.resolve(result).catch(warnProgressCallback);
|
|
7
|
-
} catch (error) {
|
|
8
|
-
warnProgressCallback(error);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createFfmpegProgressParser(onProgress, stage, durationSeconds, details = {}) {
|
|
13
|
-
let pending = '';
|
|
14
|
-
let currentTimeUs;
|
|
15
|
-
const parseLine = (line) => {
|
|
16
|
-
const separator = line.indexOf('=');
|
|
17
|
-
if (separator < 0) return;
|
|
18
|
-
const key = line.slice(0, separator);
|
|
19
|
-
const value = line.slice(separator + 1);
|
|
20
|
-
if (key === 'out_time_us') currentTimeUs = Number(value);
|
|
21
|
-
if (key !== 'progress' || !Number.isFinite(currentTimeUs)) return;
|
|
22
|
-
const completed = Math.max(0, currentTimeUs / 1_000_000);
|
|
23
|
-
const percent = Math.max(0, Math.min(99, completed / durationSeconds * 100));
|
|
24
|
-
emitRenderProgress(onProgress, stage, 'progress', { percent, completed, total: durationSeconds, ...details });
|
|
25
|
-
};
|
|
26
|
-
return {
|
|
27
|
-
push(chunk) {
|
|
28
|
-
pending += chunk.toString('utf8');
|
|
29
|
-
let newline;
|
|
30
|
-
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
31
|
-
parseLine(pending.slice(0, newline).replace(/\r$/, ''));
|
|
32
|
-
pending = pending.slice(newline + 1);
|
|
33
|
-
}
|
|
34
|
-
if (pending.length > 4096) pending = pending.slice(-4096);
|
|
35
|
-
},
|
|
36
|
-
flush() {
|
|
37
|
-
if (pending) parseLine(pending);
|
|
38
|
-
pending = '';
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function warnProgressCallback(error) {
|
|
44
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
45
|
-
process.emitWarning(`Progress callback failed: ${detail}`);
|
|
46
|
-
}
|
|
1
|
+
export function emitRenderProgress(onProgress, stage, phase, details = {}) {
|
|
2
|
+
if (!onProgress) return;
|
|
3
|
+
try {
|
|
4
|
+
const percent = phase === 'completed' ? 100 : phase === 'started' ? 0 : undefined;
|
|
5
|
+
const result = onProgress({ stage, phase, ...(percent !== undefined ? { percent } : {}), ...details });
|
|
6
|
+
if (result && typeof result.then === 'function') Promise.resolve(result).catch(warnProgressCallback);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
warnProgressCallback(error);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createFfmpegProgressParser(onProgress, stage, durationSeconds, details = {}) {
|
|
13
|
+
let pending = '';
|
|
14
|
+
let currentTimeUs;
|
|
15
|
+
const parseLine = (line) => {
|
|
16
|
+
const separator = line.indexOf('=');
|
|
17
|
+
if (separator < 0) return;
|
|
18
|
+
const key = line.slice(0, separator);
|
|
19
|
+
const value = line.slice(separator + 1);
|
|
20
|
+
if (key === 'out_time_us') currentTimeUs = Number(value);
|
|
21
|
+
if (key !== 'progress' || !Number.isFinite(currentTimeUs)) return;
|
|
22
|
+
const completed = Math.max(0, currentTimeUs / 1_000_000);
|
|
23
|
+
const percent = Math.max(0, Math.min(99, completed / durationSeconds * 100));
|
|
24
|
+
emitRenderProgress(onProgress, stage, 'progress', { percent, completed, total: durationSeconds, ...details });
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
push(chunk) {
|
|
28
|
+
pending += chunk.toString('utf8');
|
|
29
|
+
let newline;
|
|
30
|
+
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
31
|
+
parseLine(pending.slice(0, newline).replace(/\r$/, ''));
|
|
32
|
+
pending = pending.slice(newline + 1);
|
|
33
|
+
}
|
|
34
|
+
if (pending.length > 4096) pending = pending.slice(-4096);
|
|
35
|
+
},
|
|
36
|
+
flush() {
|
|
37
|
+
if (pending) parseLine(pending);
|
|
38
|
+
pending = '';
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function warnProgressCallback(error) {
|
|
44
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
45
|
+
process.emitWarning(`Progress callback failed: ${detail}`);
|
|
46
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
export function createTimelineRenderControl(signal, timeoutSeconds) {
|
|
2
|
-
if (signal !== undefined && (typeof signal.aborted !== 'boolean' || typeof signal.addEventListener !== 'function')) {
|
|
3
|
-
throw new TypeError('Render signal must be an AbortSignal.');
|
|
4
|
-
}
|
|
5
|
-
if (timeoutSeconds !== undefined
|
|
6
|
-
&& (!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 86_400)) {
|
|
7
|
-
throw new TypeError('Render timeoutSeconds must be an integer from 1 to 86400.');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const controller = new AbortController();
|
|
11
|
-
const deadline = timeoutSeconds === undefined ? undefined : Date.now() + timeoutSeconds * 1000;
|
|
12
|
-
const timeoutError = () => {
|
|
13
|
-
const error = new Error(`Render exceeded the configured timeout of ${timeoutSeconds} seconds.`);
|
|
14
|
-
error.name = 'TimeoutError';
|
|
15
|
-
return error;
|
|
16
|
-
};
|
|
17
|
-
const relayAbort = () => controller.abort(signal.reason ?? new DOMException('Render was aborted.', 'AbortError'));
|
|
18
|
-
if (signal?.aborted) relayAbort();
|
|
19
|
-
else signal?.addEventListener('abort', relayAbort, { once: true });
|
|
20
|
-
const timer = deadline === undefined ? undefined : setTimeout(() => controller.abort(timeoutError()), timeoutSeconds * 1000);
|
|
21
|
-
timer?.unref();
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
signal: controller.signal,
|
|
25
|
-
assertActive() {
|
|
26
|
-
if (signal?.aborted && !controller.signal.aborted) relayAbort();
|
|
27
|
-
if (deadline !== undefined && Date.now() >= deadline) controller.abort(timeoutError());
|
|
28
|
-
if (controller.signal.aborted) throw controller.signal.reason ?? new DOMException('Render was aborted.', 'AbortError');
|
|
29
|
-
},
|
|
30
|
-
timeoutMs() {
|
|
31
|
-
if (deadline === undefined) return undefined;
|
|
32
|
-
const remaining = deadline - Date.now();
|
|
33
|
-
if (remaining <= 0) {
|
|
34
|
-
controller.abort(timeoutError());
|
|
35
|
-
this.assertActive();
|
|
36
|
-
}
|
|
37
|
-
return Math.max(1, Math.ceil(remaining));
|
|
38
|
-
},
|
|
39
|
-
processError(error) {
|
|
40
|
-
if (signal?.aborted && !controller.signal.aborted) relayAbort();
|
|
41
|
-
if (controller.signal.aborted) return controller.signal.reason;
|
|
42
|
-
if (error?.code === 'ETIMEDOUT' || (deadline !== undefined && Date.now() >= deadline)) {
|
|
43
|
-
controller.abort(timeoutError());
|
|
44
|
-
return controller.signal.reason;
|
|
45
|
-
}
|
|
46
|
-
return error;
|
|
47
|
-
},
|
|
48
|
-
dispose() {
|
|
49
|
-
if (timer) clearTimeout(timer);
|
|
50
|
-
signal?.removeEventListener('abort', relayAbort);
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
1
|
+
export function createTimelineRenderControl(signal, timeoutSeconds) {
|
|
2
|
+
if (signal !== undefined && (typeof signal.aborted !== 'boolean' || typeof signal.addEventListener !== 'function')) {
|
|
3
|
+
throw new TypeError('Render signal must be an AbortSignal.');
|
|
4
|
+
}
|
|
5
|
+
if (timeoutSeconds !== undefined
|
|
6
|
+
&& (!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 86_400)) {
|
|
7
|
+
throw new TypeError('Render timeoutSeconds must be an integer from 1 to 86400.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const deadline = timeoutSeconds === undefined ? undefined : Date.now() + timeoutSeconds * 1000;
|
|
12
|
+
const timeoutError = () => {
|
|
13
|
+
const error = new Error(`Render exceeded the configured timeout of ${timeoutSeconds} seconds.`);
|
|
14
|
+
error.name = 'TimeoutError';
|
|
15
|
+
return error;
|
|
16
|
+
};
|
|
17
|
+
const relayAbort = () => controller.abort(signal.reason ?? new DOMException('Render was aborted.', 'AbortError'));
|
|
18
|
+
if (signal?.aborted) relayAbort();
|
|
19
|
+
else signal?.addEventListener('abort', relayAbort, { once: true });
|
|
20
|
+
const timer = deadline === undefined ? undefined : setTimeout(() => controller.abort(timeoutError()), timeoutSeconds * 1000);
|
|
21
|
+
timer?.unref();
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
assertActive() {
|
|
26
|
+
if (signal?.aborted && !controller.signal.aborted) relayAbort();
|
|
27
|
+
if (deadline !== undefined && Date.now() >= deadline) controller.abort(timeoutError());
|
|
28
|
+
if (controller.signal.aborted) throw controller.signal.reason ?? new DOMException('Render was aborted.', 'AbortError');
|
|
29
|
+
},
|
|
30
|
+
timeoutMs() {
|
|
31
|
+
if (deadline === undefined) return undefined;
|
|
32
|
+
const remaining = deadline - Date.now();
|
|
33
|
+
if (remaining <= 0) {
|
|
34
|
+
controller.abort(timeoutError());
|
|
35
|
+
this.assertActive();
|
|
36
|
+
}
|
|
37
|
+
return Math.max(1, Math.ceil(remaining));
|
|
38
|
+
},
|
|
39
|
+
processError(error) {
|
|
40
|
+
if (signal?.aborted && !controller.signal.aborted) relayAbort();
|
|
41
|
+
if (controller.signal.aborted) return controller.signal.reason;
|
|
42
|
+
if (error?.code === 'ETIMEDOUT' || (deadline !== undefined && Date.now() >= deadline)) {
|
|
43
|
+
controller.abort(timeoutError());
|
|
44
|
+
return controller.signal.reason;
|
|
45
|
+
}
|
|
46
|
+
return error;
|
|
47
|
+
},
|
|
48
|
+
dispose() {
|
|
49
|
+
if (timer) clearTimeout(timer);
|
|
50
|
+
signal?.removeEventListener('abort', relayAbort);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|