mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: detect circular signal dependencies (#129)
This commit is contained in:
@@ -14,6 +14,7 @@ export interface LogPayload {
|
||||
level?: LogLevel;
|
||||
message: string;
|
||||
stack?: string;
|
||||
remarks?: string;
|
||||
object?: any;
|
||||
durationMs?: number;
|
||||
[K: string]: any;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {FullSceneDescription, Scene, SceneDescription} from './scenes';
|
||||
import {FullSceneDescription, Scene} from './scenes';
|
||||
import {Meta, Metadata} from './Meta';
|
||||
import {EventDispatcher, ValueDispatcher} from './events';
|
||||
import {CanvasColorSpace, CanvasOutputMimeType, Vector2} from './types';
|
||||
|
||||
@@ -136,6 +136,7 @@ export abstract class GeneratorScene<T>
|
||||
public async render(context: CanvasRenderingContext2D): Promise<void> {
|
||||
let promises = DependencyContext.consumePromises();
|
||||
let iterations = 0;
|
||||
startScene(this);
|
||||
do {
|
||||
iterations++;
|
||||
await Promise.all(promises.map(handle => handle.promise));
|
||||
@@ -147,6 +148,7 @@ export abstract class GeneratorScene<T>
|
||||
|
||||
promises = DependencyContext.consumePromises();
|
||||
} while (promises.length > 0 && iterations < 10);
|
||||
endScene(this);
|
||||
|
||||
if (iterations > 1) {
|
||||
this.project.logger.debug(`render iterations: ${iterations}`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useLogger} from '../utils';
|
||||
import {errorToLog, useLogger} from '../utils';
|
||||
import {DependencyContext} from './DependencyContext';
|
||||
|
||||
export interface Computed<TValue> {
|
||||
@@ -30,8 +30,7 @@ export class ComputedContext<TValue> extends DependencyContext<any> {
|
||||
this.last = this.factory(...args);
|
||||
} catch (e: any) {
|
||||
useLogger().error({
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
...errorToLog(e),
|
||||
inspect: this.owner?.key,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {FlagDispatcher, Subscribable} from '../events';
|
||||
import {DetailedError} from '../utils';
|
||||
|
||||
export interface PromiseHandle<T> {
|
||||
promise: Promise<T>;
|
||||
@@ -8,6 +9,7 @@ export interface PromiseHandle<T> {
|
||||
}
|
||||
|
||||
export class DependencyContext<TOwner = void> {
|
||||
protected static collectionSet = new Set<DependencyContext<any>>();
|
||||
protected static collectionStack: DependencyContext<any>[] = [];
|
||||
protected static promises: PromiseHandle<any>[] = [];
|
||||
|
||||
@@ -61,14 +63,24 @@ export class DependencyContext<TOwner = void> {
|
||||
}
|
||||
|
||||
protected startCollecting() {
|
||||
if (DependencyContext.collectionSet.has(this)) {
|
||||
throw new DetailedError(
|
||||
'A circular dependency occurred between signals.',
|
||||
`This can happen when signals reference each other in a loop.
|
||||
Try using the attached stack trace to locate said loop.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.stack = new Error().stack;
|
||||
DependencyContext.collectionSet.add(this);
|
||||
DependencyContext.collectionStack.push(this);
|
||||
}
|
||||
|
||||
protected finishCollecting() {
|
||||
this.stack = undefined;
|
||||
DependencyContext.collectionSet.delete(this);
|
||||
if (DependencyContext.collectionStack.pop() !== this) {
|
||||
throw new Error('collectStart/collectEnd was called out of order');
|
||||
throw new Error('collectStart/collectEnd was called out of order.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
TimingFunction,
|
||||
tween,
|
||||
} from '../tweening';
|
||||
import {useLogger} from '../utils';
|
||||
import {errorToLog, useLogger} from '../utils';
|
||||
import {ThreadGenerator} from '../threading';
|
||||
import {run} from '../flow';
|
||||
import {DependencyContext} from './DependencyContext';
|
||||
@@ -133,8 +133,7 @@ export class SignalContext<
|
||||
this.last = this.parse(this.current());
|
||||
} catch (e: any) {
|
||||
useLogger().error({
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
...errorToLog(e),
|
||||
inspect: (<any>this.owner)?.key,
|
||||
});
|
||||
}
|
||||
|
||||
5
packages/core/src/utils/DetailedError.ts
Normal file
5
packages/core/src/utils/DetailedError.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class DetailedError extends Error {
|
||||
public constructor(message: string, public readonly remarks?: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
9
packages/core/src/utils/errorToLog.ts
Normal file
9
packages/core/src/utils/errorToLog.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {LogPayload} from '../Logger';
|
||||
|
||||
export function errorToLog(error: any): LogPayload {
|
||||
return {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
remarks: error.remarks,
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ export function getContext(
|
||||
): CanvasRenderingContext2D {
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('Could not create a 2D context');
|
||||
throw new Error('Could not create a 2D context.');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
export * from './capitalize';
|
||||
export * from './DetailedError';
|
||||
export * from './errorToLog';
|
||||
export * from './getContext';
|
||||
export * from './range';
|
||||
export * from './useProject';
|
||||
|
||||
@@ -8,7 +8,7 @@ const sceneStack: Scene[] = [];
|
||||
export function useScene(): Scene {
|
||||
const scene = sceneStack.at(-1);
|
||||
if (!scene) {
|
||||
throw new Error('The scene is not available in the current context');
|
||||
throw new Error('The scene is not available in the current context.');
|
||||
}
|
||||
return scene;
|
||||
}
|
||||
@@ -19,6 +19,6 @@ export function startScene(scene: Scene) {
|
||||
|
||||
export function endScene(scene: Scene) {
|
||||
if (sceneStack.pop() !== scene) {
|
||||
throw new Error('startScene/endScene was called out of order');
|
||||
throw new Error('startScene/endScene was called out of order.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {Thread} from '../threading';
|
||||
import {DetailedError} from '../utils';
|
||||
|
||||
const threadStack: Thread[] = [];
|
||||
|
||||
@@ -8,7 +9,11 @@ const threadStack: Thread[] = [];
|
||||
export function useThread(): Thread {
|
||||
const thread = threadStack.at(-1);
|
||||
if (!thread) {
|
||||
throw new Error('The thread is not available in the current context');
|
||||
throw new DetailedError(
|
||||
'The thread is not available in the current context.',
|
||||
`<code>useThread()</code> can only be called from within generator functions.
|
||||
It's not available during rendering.`,
|
||||
);
|
||||
}
|
||||
return thread;
|
||||
}
|
||||
@@ -19,6 +24,6 @@ export function startThread(thread: Thread) {
|
||||
|
||||
export function endThread(thread: Thread) {
|
||||
if (threadStack.pop() !== thread) {
|
||||
throw new Error('startThread/endThread was called out of order');
|
||||
throw new Error('startThread/endThread was called out of order.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ $colors: (
|
||||
border-left: 4px solid transparent;
|
||||
background-color: var(--background-color-dark);
|
||||
border-radius: 4px;
|
||||
line-height: 24px;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
@@ -38,7 +39,6 @@ $colors: (
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
line-height: 24px;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
|
||||
@@ -53,35 +53,43 @@ $colors: (
|
||||
|
||||
.duration {
|
||||
background-color: var(--surface-color);
|
||||
line-height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remarks pre,
|
||||
.code {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 4px;
|
||||
line-height: 24px;
|
||||
padding: 8px;
|
||||
margin: 8px 0 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stack {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
margin-top: 8px;
|
||||
|
||||
--scrollbar-background: var(--background-color-dark);
|
||||
}
|
||||
|
||||
.remarks {
|
||||
margin-top: 16px;
|
||||
.section {
|
||||
margin: 16px 8px 8px;
|
||||
|
||||
p {
|
||||
margin: 8px;
|
||||
pre {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin: 8px -8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--surface-color);
|
||||
padding: 2px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,15 +60,25 @@ export function Log({payload}: LogProps) {
|
||||
</div>
|
||||
{hasBody && open && (
|
||||
<div>
|
||||
{userEntry && <SourceCodeFrame entry={userEntry} />}
|
||||
{entries && <StackTrace entries={entries} />}
|
||||
{object && <pre className={styles.code}>{object}</pre>}
|
||||
{payload.remarks && (
|
||||
<div
|
||||
className={styles.remarks}
|
||||
className={clsx(styles.section, styles.remarks)}
|
||||
dangerouslySetInnerHTML={{__html: payload.remarks}}
|
||||
/>
|
||||
)}
|
||||
{object && (
|
||||
<div className={styles.section}>
|
||||
Related object:
|
||||
<pre className={styles.code}>{object}</pre>
|
||||
</div>
|
||||
)}
|
||||
{entries && (
|
||||
<div className={styles.section}>
|
||||
The problem occurred here:
|
||||
{userEntry && <SourceCodeFrame entry={userEntry} />}
|
||||
<StackTrace entries={entries} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,7 @@ export default ({
|
||||
` config.name = '${name}';`,
|
||||
` config.logger.warn({`,
|
||||
` message: 'A project instance was exported instead of a project factory.',`,
|
||||
` remarks: \`<p>Use the "makeProject()" function instead:</p><pre>import {makeProject} from '@motion-canvas/core';\nexport default makeProject({\n // Configuration and scenes go here.\n});</pre>\`,`,
|
||||
` remarks: \`Use the <code>makeProject()</code> function instead:<pre>import {makeProject} from '@motion-canvas/core';\nexport default makeProject({\n // Configuration and scenes go here.\n});</pre>\`,`,
|
||||
` stack: config.creationStack,`,
|
||||
` });`,
|
||||
` return config;`,
|
||||
|
||||
Reference in New Issue
Block a user