Skip to content

Commit d25337b

Browse files
authored
Implement Request Batches with callBatch (#306)
* add support for request batches * add tests and make `options` optional * feedback (README + Options type)
1 parent f0d9e29 commit d25337b

File tree

6 files changed

+267
-50
lines changed

6 files changed

+267
-50
lines changed

README.md

+47
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,53 @@ await obs.call('SetCurrentProgramScene', {sceneName: 'Gameplay'});
194194
const {inputMuted} = obs.call('ToggleInputMute', {inputName: 'Camera'});
195195
```
196196

197+
### Sending Batch Requests
198+
199+
```ts
200+
callBatch(requests: RequestBatchRequest[], options?: RequestBatchOptions): Promise<ResponseMessage[]>
201+
```
202+
203+
Multiple requests can be batched together into a single message sent to obs-websocket using the `callBatch` method. The full request list is sent over the socket at once, obs-websocket executes the requests based on the `options` provided, then returns the full list of results once all have finished.
204+
205+
Parameter | Description
206+
---|---
207+
`requests`<br />`RequestBatchRequest[]` | The list of requests to be sent ([see obs-websocket documentation](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)). Each request follows the same structure as individual requests sent to [`call`](#sending-requests).
208+
`options`<br />`RequestBatchOptions (optional)` | Options controlling how obs-websocket will execute the request list.
209+
`options.executionType`<br />`RequestBatchExecutionType (optional)` | The mode of execution obs-websocket will run the batch in
210+
`options.haltOnFailure`<br />`boolean (optional)` | Whether obs-websocket should stop executing the batch if one request fails
211+
212+
Returns promise that resolve with a list of `results`, one for each request that was executed.
213+
214+
```ts
215+
// Execute a transition sequence to a different scene with a specific transition.
216+
const results = await obs.callBatch([
217+
{
218+
requestType: 'GetVersion',
219+
},
220+
{
221+
requestType: 'SetCurrentPreviewScene',
222+
requestData: {sceneName: 'Scene 5'},
223+
},
224+
{
225+
requestType: 'SetCurrentSceneTransition',
226+
requestData: {transitionName: 'Fade'},
227+
},
228+
{
229+
requestType: 'Sleep',
230+
requestData: {sleepMillis: 100},
231+
},
232+
{
233+
requestType: 'TriggerStudioModeTransition',
234+
}
235+
])
236+
```
237+
238+
Currently, obs-websocket-js is not able to infer the types of ResponseData to any specific request's response. To use the data safely, cast it to the appropriate type for the request that was sent.
239+
240+
```ts
241+
(results[0].responseData as OBSResponseTypes['GetVersion']).obsVersion //=> 28.0.0
242+
```
243+
197244
### Receiving Events
198245

199246
```ts

scripts/build-types.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const source = `/**
104104
* This file is autogenerated by scripts/generate-obs-typings.js
105105
* To update this with latest changes do npm run generate:obs-types
106106
*/
107-
import {JsonArray, JsonObject, JsonValue} from 'type-fest';
107+
import {Merge, JsonArray, JsonObject, JsonValue} from 'type-fest';
108108
109109
${generateEnum('WebSocketOpCode', ENUMS.WebSocketOpCode)}
110110
@@ -164,6 +164,10 @@ export interface IncomingMessageTypes {
164164
* obs-websocket is responding to a request coming from a client
165165
*/
166166
[WebSocketOpCode.RequestResponse]: ResponseMessage;
167+
/**
168+
* obs-websocket is responding to a batch request coming from a client
169+
*/
170+
[WebSocketOpCode.RequestBatchResponse]: ResponseBatchMessage;
167171
}
168172
169173
export interface OutgoingMessageTypes {
@@ -197,6 +201,10 @@ export interface OutgoingMessageTypes {
197201
* Client is making a request to obs-websocket. Eg get current scene, create source.
198202
*/
199203
[WebSocketOpCode.Request]: RequestMessage;
204+
/**
205+
* Client is making a batch request to obs-websocket.
206+
*/
207+
[WebSocketOpCode.RequestBatch]: RequestBatchMessage;
200208
}
201209
202210
type EventMessage<T = keyof OBSEventTypes> = T extends keyof OBSEventTypes ? {
@@ -214,13 +222,43 @@ export type RequestMessage<T = keyof OBSRequestTypes> = T extends keyof OBSReque
214222
requestData: OBSRequestTypes[T];
215223
} : never;
216224
225+
export type RequestBatchRequest<T = keyof OBSRequestTypes> = T extends keyof OBSRequestTypes ? OBSRequestTypes[T] extends never ? {
226+
requestType: T;
227+
requestId?: string;
228+
} : {
229+
requestType: T;
230+
requestId?: string;
231+
requestData: OBSRequestTypes[T];
232+
} : never;
233+
234+
export type RequestBatchOptions = {
235+
/**
236+
* The mode of execution obs-websocket will run the batch in
237+
*/
238+
executionType?: RequestBatchExecutionType;
239+
/**
240+
* Whether obs-websocket should stop executing the batch if one request fails
241+
*/
242+
haltOnFailure?: boolean;
243+
}
244+
245+
export type RequestBatchMessage = Merge<RequestBatchOptions, {
246+
requestId: string;
247+
requests: RequestBatchRequest[];
248+
}>;
249+
217250
export type ResponseMessage<T = keyof OBSResponseTypes> = T extends keyof OBSResponseTypes ? {
218251
requestType: T;
219252
requestId: string;
220253
requestStatus: {result: true; code: number} | {result: false; code: number; comment: string};
221254
responseData: OBSResponseTypes[T];
222255
} : never;
223256
257+
export type ResponseBatchMessage = {
258+
requestId: string;
259+
results: ResponseMessage[];
260+
}
261+
224262
// Events
225263
export interface OBSEventTypes {
226264
${generateObsEventTypes(protocol.events)}

src/base.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import EventEmitter from 'eventemitter3';
44
import WebSocketIpml from 'isomorphic-ws';
55
import {Except, Merge, SetOptional} from 'type-fest';
66

7-
import {OutgoingMessageTypes, WebSocketOpCode, OutgoingMessage, OBSEventTypes, IncomingMessage, IncomingMessageTypes, OBSRequestTypes, OBSResponseTypes, RequestMessage, ResponseMessage} from './types.js';
7+
import {OutgoingMessageTypes, WebSocketOpCode, OutgoingMessage, OBSEventTypes, IncomingMessage, IncomingMessageTypes, OBSRequestTypes, OBSResponseTypes, RequestMessage, RequestBatchExecutionType, RequestBatchRequest, RequestBatchMessage, ResponseMessage, ResponseBatchMessage, RequestBatchOptions} from './types.js';
88
import authenticationHashing from './utils/authenticationHashing.js';
99

1010
export const debug = createDebug('obs-websocket-js');
@@ -147,6 +147,29 @@ export abstract class BaseOBSWebSocket extends EventEmitter<MapValueToArgsArray<
147147
return responseData as OBSResponseTypes[Type];
148148
}
149149

150+
/**
151+
* Send a batch request to obs-websocket
152+
*
153+
* @param requests Array of Request objects (type and data)
154+
* @param options A set of options for how the batch will be executed
155+
* @param options.executionType The mode of execution obs-websocket will run the batch in
156+
* @param options.haltOnFailure Whether obs-websocket should stop executing the batch if one request fails
157+
* @returns RequestBatch response
158+
*/
159+
async callBatch(requests: RequestBatchRequest[], options: RequestBatchOptions = {}): Promise<ResponseMessage[]> {
160+
const requestId = BaseOBSWebSocket.generateMessageId();
161+
const responsePromise = this.internalEventPromise<ResponseBatchMessage>(`res:${requestId}`);
162+
163+
await this.message(WebSocketOpCode.RequestBatch, {
164+
requestId,
165+
requests,
166+
...options,
167+
});
168+
169+
const {results} = await responsePromise;
170+
return results;
171+
}
172+
150173
/**
151174
* Cleanup from socket disconnection
152175
*/
@@ -310,7 +333,8 @@ export abstract class BaseOBSWebSocket extends EventEmitter<MapValueToArgsArray<
310333
return;
311334
}
312335

313-
case WebSocketOpCode.RequestResponse: {
336+
case WebSocketOpCode.RequestResponse:
337+
case WebSocketOpCode.RequestBatchResponse: {
314338
const {requestId} = d;
315339
this.internalListeners.emit(`res:${requestId}`, d);
316340
return;

src/types.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* This file is autogenerated by scripts/generate-obs-typings.js
33
* To update this with latest changes do npm run generate:obs-types
44
*/
5-
import {JsonArray, JsonObject, JsonValue} from 'type-fest';
5+
import {Merge, JsonArray, JsonObject, JsonValue} from 'type-fest';
66

77
export enum WebSocketOpCode {
88
/**
@@ -252,6 +252,10 @@ export interface IncomingMessageTypes {
252252
* obs-websocket is responding to a request coming from a client
253253
*/
254254
[WebSocketOpCode.RequestResponse]: ResponseMessage;
255+
/**
256+
* obs-websocket is responding to a batch request coming from a client
257+
*/
258+
[WebSocketOpCode.RequestBatchResponse]: ResponseBatchMessage;
255259
}
256260

257261
export interface OutgoingMessageTypes {
@@ -285,6 +289,10 @@ export interface OutgoingMessageTypes {
285289
* Client is making a request to obs-websocket. Eg get current scene, create source.
286290
*/
287291
[WebSocketOpCode.Request]: RequestMessage;
292+
/**
293+
* Client is making a batch request to obs-websocket.
294+
*/
295+
[WebSocketOpCode.RequestBatch]: RequestBatchMessage;
288296
}
289297

290298
type EventMessage<T = keyof OBSEventTypes> = T extends keyof OBSEventTypes ? {
@@ -302,13 +310,43 @@ export type RequestMessage<T = keyof OBSRequestTypes> = T extends keyof OBSReque
302310
requestData: OBSRequestTypes[T];
303311
} : never;
304312

313+
export type RequestBatchRequest<T = keyof OBSRequestTypes> = T extends keyof OBSRequestTypes ? OBSRequestTypes[T] extends never ? {
314+
requestType: T;
315+
requestId?: string;
316+
} : {
317+
requestType: T;
318+
requestId?: string;
319+
requestData: OBSRequestTypes[T];
320+
} : never;
321+
322+
export type RequestBatchOptions = {
323+
/**
324+
* The mode of execution obs-websocket will run the batch in
325+
*/
326+
executionType?: RequestBatchExecutionType;
327+
/**
328+
* Whether obs-websocket should stop executing the batch if one request fails
329+
*/
330+
haltOnFailure?: boolean;
331+
};
332+
333+
export type RequestBatchMessage = Merge<RequestBatchOptions, {
334+
requestId: string;
335+
requests: RequestBatchRequest[];
336+
}>;
337+
305338
export type ResponseMessage<T = keyof OBSResponseTypes> = T extends keyof OBSResponseTypes ? {
306339
requestType: T;
307340
requestId: string;
308341
requestStatus: {result: true; code: number} | {result: false; code: number; comment: string};
309342
responseData: OBSResponseTypes[T];
310343
} : never;
311344

345+
export type ResponseBatchMessage = {
346+
requestId: string;
347+
results: ResponseMessage[];
348+
};
349+
312350
// Events
313351
export interface OBSEventTypes {
314352
CurrentSceneCollectionChanging: {

tests/helpers/dev-server.ts

+62-46
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import sha256 from 'crypto-js/sha256.js';
33
import Base64 from 'crypto-js/enc-base64.js';
44
import {JsonObject} from 'type-fest';
55
import {AddressInfo, WebSocketServer} from 'ws';
6-
import {IncomingMessage, WebSocketOpCode, OutgoingMessage} from '../../src/types.js';
6+
import {IncomingMessage, WebSocketOpCode, OutgoingMessage, ResponseBatchMessage, OBSRequestTypes, ResponseMessage} from '../../src/types.js';
77

88
export interface MockServer {
99
server: WebSocketServer;
@@ -54,6 +54,44 @@ const REQUEST_HANDLERS: Record<string, (req?: JsonObject | void) => FailureRespo
5454
/* eslint-enable @typescript-eslint/naming-convention */
5555
};
5656

57+
function handleRequestData<T extends keyof OBSRequestTypes>(requestId: string | undefined, requestType: T, requestData: OBSRequestTypes[T]) {
58+
if (!(requestType in REQUEST_HANDLERS)) {
59+
return {
60+
requestType,
61+
requestId,
62+
requestStatus: {
63+
result: false,
64+
code: 204,
65+
comment: 'unknown type',
66+
},
67+
};
68+
}
69+
70+
const responseData = REQUEST_HANDLERS[requestType](requestData);
71+
72+
if (responseData instanceof FailureResponse) {
73+
return {
74+
requestType,
75+
requestId,
76+
requestStatus: {
77+
result: false,
78+
code: responseData.code,
79+
comment: responseData.message,
80+
},
81+
};
82+
}
83+
84+
return {
85+
requestType,
86+
requestId,
87+
requestStatus: {
88+
result: true,
89+
code: 100,
90+
},
91+
responseData,
92+
};
93+
}
94+
5795
export async function makeServer(
5896
authenticate?: boolean,
5997
): Promise<MockServer> {
@@ -134,55 +172,33 @@ export async function makeServer(
134172
break;
135173
case WebSocketOpCode.Request: {
136174
const {requestData, requestId, requestType} = message.d;
137-
if (!(requestType in REQUEST_HANDLERS)) {
138-
send({
139-
op: WebSocketOpCode.RequestResponse,
140-
d: {
141-
// @ts-expect-error don't care
142-
requestType,
143-
requestId,
144-
requestStatus: {
145-
result: false,
146-
code: 204,
147-
comment: 'unknown type',
148-
},
149-
},
150-
});
151-
break;
152-
}
175+
const responseData = handleRequestData(requestId, requestType, requestData);
176+
send({
177+
op: WebSocketOpCode.RequestResponse,
178+
// @ts-expect-error RequestTypes and ResponseTypes are non-overlapping according to ts
179+
d: responseData,
180+
});
181+
break;
182+
}
183+
184+
case WebSocketOpCode.RequestBatch: {
185+
const {requests, requestId, haltOnFailure: shouldHalt} = message.d;
186+
187+
const response: ResponseBatchMessage = {requestId, results: []};
153188

154-
const responseData = REQUEST_HANDLERS[requestType](requestData);
155-
156-
if (responseData instanceof FailureResponse) {
157-
send({
158-
op: WebSocketOpCode.RequestResponse,
159-
d: {
160-
// @ts-expect-error don't care
161-
requestType,
162-
requestId,
163-
requestStatus: {
164-
result: false,
165-
code: responseData.code,
166-
comment: responseData.message,
167-
},
168-
},
169-
});
170-
break;
189+
for (const request of requests) {
190+
// @ts-expect-error requestData only exists on _some_ request types, not all
191+
const result = handleRequestData(request.requestId, request.requestType, request.requestData);
192+
response.results.push(result as ResponseMessage);
193+
194+
if (!result.requestStatus.result && shouldHalt) {
195+
break;
196+
}
171197
}
172198

173199
send({
174-
op: WebSocketOpCode.RequestResponse,
175-
d: {
176-
// @ts-expect-error don't care
177-
requestType,
178-
requestId,
179-
requestStatus: {
180-
result: true,
181-
code: 100,
182-
},
183-
// @ts-expect-error don't care
184-
responseData,
185-
},
200+
op: WebSocketOpCode.RequestBatchResponse,
201+
d: response,
186202
});
187203

188204
break;

0 commit comments

Comments
 (0)