Skip to content

Commit 84d8cae

Browse files
authored
@remotion/studio: Request element install targets on demand (#9025)
* `@remotion/studio`: Update element install target only on change * `@remotion/studio`: Request element install targets on demand * `@remotion/studio-server`: Make install target request ID nullable
1 parent f3dce8a commit 84d8cae

7 files changed

Lines changed: 124 additions & 57 deletions

File tree

packages/studio-server/src/preview-server/element-install-state.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const ELEMENT_INSTALL_TARGET_MAX_AGE = 5000;
22

33
export type ElementInstallTarget = {
4+
requestId: string | null;
45
clientId: string;
56
compositionFile: string | null;
67
compositionId: string | null;
@@ -32,7 +33,7 @@ export const updateElementInstallTarget = (
3233
});
3334
};
3435

35-
export const getElementInstallTarget = () => {
36+
export const getElementInstallTarget = (requestId: string | null) => {
3637
const now = Date.now();
3738
let bestTarget: ElementInstallTarget | null = null;
3839

@@ -42,6 +43,10 @@ export const getElementInstallTarget = () => {
4243
continue;
4344
}
4445

46+
if (requestId !== null && currentTarget.requestId !== requestId) {
47+
continue;
48+
}
49+
4550
if (bestTarget === null || compareTargets(currentTarget, bestTarget) > 0) {
4651
bestTarget = currentTarget;
4752
}

packages/studio-server/src/preview-server/routes/update-element-install-target.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export const updateElementInstallTargetHandler: ApiHandler<
1717
throw new Error('Invalid client ID');
1818
}
1919

20+
if (input.requestId !== null && typeof input.requestId !== 'string') {
21+
throw new Error('Invalid request ID');
22+
}
23+
2024
if (!isFiniteTimestamp(input.lastFocusedAt)) {
2125
throw new Error('Invalid focus timestamp');
2226
}

packages/studio-server/src/routes.ts

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {reloadPreviouslySuppressedFiles} from './preview-server/watch-ignore-nex
3838
import type {RemotionConfigResponse} from './remotion-config-response';
3939
const loggedStaticFileHints = new Set<string>();
4040
const ELEMENT_INSTALL_FOCUS_MAX_AGE = 5 * 60 * 1000;
41+
const ELEMENT_INSTALL_TARGET_RESPONSE_WAIT = 250;
4142

4243
const static404 = (response: ServerResponse): Promise<void> => {
4344
response.writeHead(404);
@@ -124,11 +125,13 @@ const handleElementInstallOptions = ({
124125
};
125126

126127
const handleElementInstallTarget = ({
128+
liveEventsServer,
127129
request,
128130
response,
129131
remotionRoot,
130132
gitSource,
131133
}: {
134+
liveEventsServer: LiveEventsServer;
132135
request: IncomingMessage;
133136
response: ServerResponse;
134137
remotionRoot: string;
@@ -142,30 +145,42 @@ const handleElementInstallTarget = ({
142145
return Promise.resolve();
143146
}
144147

145-
const target = getElementInstallTarget();
146-
const now = Date.now();
147-
const targetIsLive =
148-
target !== null && now - target.updatedAt < ELEMENT_INSTALL_TARGET_MAX_AGE;
149-
const host = request.headers.host ?? null;
150-
const port = host?.split(':').at(-1) ?? null;
151-
setElementInstallCorsHeaders({request, response});
152-
response.writeHead(200, {'Content-Type': 'application/json'});
153-
response.end(
154-
JSON.stringify({
155-
type: 'remotion-studio',
156-
projectName: getProjectName({
157-
basename: path.basename,
158-
gitSource,
159-
resolvedRemotionRoot: remotionRoot,
160-
}),
161-
port: port === null ? null : Number(port),
162-
lastFocusedAt: target?.lastFocusedAt ?? null,
163-
canInstall: target !== null && target.canInstall && targetIsLive,
164-
activeCompositionId: target?.compositionId ?? null,
165-
readOnly: target?.readOnly ?? false,
166-
}),
167-
);
168-
return Promise.resolve();
148+
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
149+
150+
liveEventsServer.sendEventToClient({
151+
type: 'request-element-install-target',
152+
requestId,
153+
});
154+
155+
return new Promise<void>((resolve) => {
156+
setTimeout(() => {
157+
const target = getElementInstallTarget(requestId);
158+
const now = Date.now();
159+
const targetIsLive =
160+
target !== null &&
161+
now - target.updatedAt < ELEMENT_INSTALL_TARGET_MAX_AGE;
162+
const host = request.headers.host ?? null;
163+
const port = host?.split(':').at(-1) ?? null;
164+
setElementInstallCorsHeaders({request, response});
165+
response.writeHead(200, {'Content-Type': 'application/json'});
166+
response.end(
167+
JSON.stringify({
168+
type: 'remotion-studio',
169+
projectName: getProjectName({
170+
basename: path.basename,
171+
gitSource,
172+
resolvedRemotionRoot: remotionRoot,
173+
}),
174+
port: port === null ? null : Number(port),
175+
lastFocusedAt: target?.lastFocusedAt ?? null,
176+
canInstall: target !== null && target.canInstall && targetIsLive,
177+
activeCompositionId: target?.compositionId ?? null,
178+
readOnly: target?.readOnly ?? false,
179+
}),
180+
);
181+
resolve();
182+
}, ELEMENT_INSTALL_TARGET_RESPONSE_WAIT);
183+
});
169184
};
170185

171186
const handleRequestElementInstall = async ({
@@ -206,7 +221,7 @@ const handleRequestElementInstall = async ({
206221
return;
207222
}
208223

209-
const target = getElementInstallTarget();
224+
const target = getElementInstallTarget(null);
210225
const now = Date.now();
211226
const targetIsLive =
212227
target !== null &&
@@ -641,6 +656,7 @@ export const handleRoutes = ({
641656

642657
if (url.pathname === '/api/element-install-target') {
643658
return handleElementInstallTarget({
659+
liveEventsServer,
644660
request,
645661
response,
646662
remotionRoot,

packages/studio-server/src/test/element-install-state.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ test('uses the most recently focused Studio target even when older tabs keep upd
99
clearElementInstallStateForTests();
1010

1111
updateElementInstallTarget({
12+
requestId: null,
1213
clientId: 'older-tab',
1314
compositionFile: '/project/src/older.tsx',
1415
compositionId: 'older-composition',
@@ -17,6 +18,7 @@ test('uses the most recently focused Studio target even when older tabs keep upd
1718
readOnly: false,
1819
});
1920
updateElementInstallTarget({
21+
requestId: null,
2022
clientId: 'focused-tab',
2123
compositionFile: '/project/src/focused.tsx',
2224
compositionId: 'focused-composition',
@@ -25,6 +27,7 @@ test('uses the most recently focused Studio target even when older tabs keep upd
2527
readOnly: false,
2628
});
2729
updateElementInstallTarget({
30+
requestId: null,
2831
clientId: 'older-tab',
2932
compositionFile: '/project/src/older.tsx',
3033
compositionId: 'older-composition',
@@ -33,13 +36,14 @@ test('uses the most recently focused Studio target even when older tabs keep upd
3336
readOnly: false,
3437
});
3538

36-
expect(getElementInstallTarget()?.clientId).toBe('focused-tab');
39+
expect(getElementInstallTarget(null)?.clientId).toBe('focused-tab');
3740
});
3841

3942
test('falls back to update recency when focus timestamps match', async () => {
4043
clearElementInstallStateForTests();
4144

4245
updateElementInstallTarget({
46+
requestId: null,
4347
clientId: 'first-tab',
4448
compositionFile: '/project/src/first.tsx',
4549
compositionId: 'first-composition',
@@ -51,6 +55,7 @@ test('falls back to update recency when focus timestamps match', async () => {
5155
await new Promise((resolve) => setTimeout(resolve, 2));
5256

5357
updateElementInstallTarget({
58+
requestId: null,
5459
clientId: 'second-tab',
5560
compositionFile: '/project/src/second.tsx',
5661
compositionId: 'second-composition',
@@ -59,5 +64,33 @@ test('falls back to update recency when focus timestamps match', async () => {
5964
readOnly: false,
6065
});
6166

62-
expect(getElementInstallTarget()?.clientId).toBe('second-tab');
67+
expect(getElementInstallTarget(null)?.clientId).toBe('second-tab');
68+
});
69+
70+
test('can select a target for a specific request', () => {
71+
clearElementInstallStateForTests();
72+
73+
updateElementInstallTarget({
74+
requestId: 'first-request',
75+
clientId: 'first-tab',
76+
compositionFile: '/project/src/first.tsx',
77+
compositionId: 'first-composition',
78+
canInstall: true,
79+
lastFocusedAt: 1000,
80+
readOnly: false,
81+
});
82+
updateElementInstallTarget({
83+
requestId: 'second-request',
84+
clientId: 'second-tab',
85+
compositionFile: '/project/src/second.tsx',
86+
compositionId: 'second-composition',
87+
canInstall: true,
88+
lastFocusedAt: 2000,
89+
readOnly: false,
90+
});
91+
92+
expect(getElementInstallTarget('first-request')?.clientId).toBe('first-tab');
93+
expect(getElementInstallTarget('second-request')?.clientId).toBe(
94+
'second-tab',
95+
);
6396
});

packages/studio-shared/src/api-requests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ export type ElementInstallRequest = {
768768
};
769769

770770
export type UpdateElementInstallTargetRequest = {
771+
requestId: string | null;
771772
clientId: string;
772773
compositionFile: string | null;
773774
compositionId: string | null;

packages/studio-shared/src/event-source-event.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export type EventSourceEvent =
7676
undoFile: string | null;
7777
redoFile: string | null;
7878
}
79+
| {
80+
type: 'request-element-install-target';
81+
requestId: string;
82+
}
7983
| {
8084
type: 'element-install-request';
8185
request: ElementInstallRequest;

packages/studio/src/components/Canvas.tsx

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -811,39 +811,33 @@ export const Canvas: React.FC<{
811811
fetchMetadata();
812812
}, [fetchMetadata]);
813813

814-
const updateElementInstallTarget = useCallback(() => {
815-
if (previewServerClientId === null) {
816-
return;
817-
}
818-
819-
callApi('/api/update-element-install-target', {
820-
clientId: previewServerClientId,
821-
compositionFile: canInstallElements ? compositionFile : null,
822-
compositionId: canInstallElements ? currentCompositionId : null,
823-
canInstall: canInstallElements,
824-
lastFocusedAt: lastFocusedAtRef.current,
825-
readOnly: window.remotion_isReadOnlyStudio,
826-
}).catch(() => undefined);
827-
}, [
828-
canInstallElements,
829-
compositionFile,
830-
currentCompositionId,
831-
previewServerClientId,
832-
]);
833-
834-
useEffect(() => {
835-
updateElementInstallTarget();
836-
const interval = window.setInterval(updateElementInstallTarget, 2000);
814+
const updateElementInstallTarget = useCallback(
815+
(requestId: string) => {
816+
if (previewServerClientId === null) {
817+
return;
818+
}
837819

838-
return () => {
839-
window.clearInterval(interval);
840-
};
841-
}, [updateElementInstallTarget]);
820+
callApi('/api/update-element-install-target', {
821+
requestId,
822+
clientId: previewServerClientId,
823+
compositionFile: canInstallElements ? compositionFile : null,
824+
compositionId: canInstallElements ? currentCompositionId : null,
825+
canInstall: canInstallElements,
826+
lastFocusedAt: lastFocusedAtRef.current,
827+
readOnly: window.remotion_isReadOnlyStudio,
828+
}).catch(() => undefined);
829+
},
830+
[
831+
canInstallElements,
832+
compositionFile,
833+
currentCompositionId,
834+
previewServerClientId,
835+
],
836+
);
842837

843838
useEffect(() => {
844839
const markFocused = () => {
845840
lastFocusedAtRef.current = Date.now();
846-
updateElementInstallTarget();
847841
};
848842

849843
window.addEventListener('focus', markFocused);
@@ -853,7 +847,17 @@ export const Canvas: React.FC<{
853847
window.removeEventListener('focus', markFocused);
854848
document.removeEventListener('pointerdown', markFocused, {capture: true});
855849
};
856-
}, [updateElementInstallTarget]);
850+
}, []);
851+
852+
useEffect(() => {
853+
return subscribeToEvent('request-element-install-target', (event) => {
854+
if (event.type !== 'request-element-install-target') {
855+
return;
856+
}
857+
858+
updateElementInstallTarget(event.requestId);
859+
});
860+
}, [subscribeToEvent, updateElementInstallTarget]);
857861

858862
useEffect(() => {
859863
if (installingElementName === null) {

0 commit comments

Comments
 (0)