@superblocksteam/sdk 1.8.2 → 1.9.1

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.
@@ -3,7 +3,11 @@ import {
3
3
  ISocketClient,
4
4
  MethodHandler,
5
5
  MethodHandlers,
6
- MiddlewareHandler,
6
+ RequestContextBase,
7
+ SocketTimeouts,
8
+ SocketErrorException,
9
+ GenericMiddleware,
10
+ NonEmptyArray,
7
11
  } from "../types";
8
12
 
9
13
  interface SocketRequest<Payload = unknown> {
@@ -19,31 +23,61 @@ interface SocketResponse<Payload = unknown> {
19
23
  error: SocketError | null;
20
24
  }
21
25
 
22
- interface SocketMessage<RequestPayload = unknown, ResponsePayload = unknown> {
26
+ export interface SocketMessage<
27
+ RequestPayload = unknown,
28
+ ResponsePayload = unknown
29
+ > {
23
30
  request?: SocketRequest<RequestPayload>;
24
31
  response?: SocketResponse<ResponsePayload>;
25
32
  }
26
33
 
27
- interface SocketError {
34
+ export interface SocketError {
28
35
  message: string;
29
36
  code: number;
30
37
  }
31
38
 
32
- export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
39
+ /**
40
+ * ISocket is a class that wraps a WebSocket connection and provides a simple interface for sending and receiving messages.
41
+ * ISocket is initialized by both server and client and request/response is called symmetrically.
42
+ *
43
+ * ISocket handles has two timeout actions:
44
+ * 1. Connection timeout: If the connection is not closed after a certain time, the connection is closed.
45
+ * - This is useful to prevent a connection from being open indefinitely. This is mainly used in the server side since
46
+ * the client side manages connection lifecycle manually.
47
+ * 2. No response timeout: If the response is not received after a certain time, the request is timed out.
48
+ * - This is mainly used by CLI to handle the case where the server is not responding.
49
+ *
50
+ * It is expected that timeouts should be relatively sorted in the order of connection timeout > request timeout > no response timeout.
51
+ */
52
+ export class ISocket<
53
+ ImplementedMethods,
54
+ CallableMethods,
55
+ RequestContext extends RequestContextBase
56
+ > {
33
57
  private readonly ws: WebSocket;
34
58
  private readonly requestHandlers: MethodHandlers<
35
59
  ImplementedMethods,
36
60
  CallableMethods,
37
61
  RequestContext
38
62
  >;
39
- private readonly responseHandler: {
40
- [requestId: number]: {
63
+ private readonly globalMiddlewares: GenericMiddleware<
64
+ CallableMethods,
65
+ RequestContext
66
+ >[];
67
+ private readonly responseHandler = new Map<
68
+ number,
69
+ {
41
70
  resolve: (data: unknown) => void;
42
71
  reject: (error: SocketError) => void;
43
- };
44
- } = {};
72
+ timeout?: NodeJS.Timeout;
73
+ }
74
+ >();
45
75
  private peerAuthorization?: string;
46
- private nxtRequestId: number;
76
+ protected nxtRequestId: number;
77
+ private connectionTimeout?: NodeJS.Timeout;
78
+ // pino library not available in shared
79
+ private logger: { error: (message?: string) => void };
80
+ private timeouts?: SocketTimeouts;
47
81
 
48
82
  constructor(
49
83
  ws: WebSocket,
@@ -51,90 +85,129 @@ export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
51
85
  ImplementedMethods,
52
86
  CallableMethods,
53
87
  RequestContext
54
- >
88
+ >,
89
+ globalMiddlewares: GenericMiddleware<CallableMethods, RequestContext>[],
90
+ timeouts?: SocketTimeouts,
91
+ logger?: { error: (message?: string) => void }
55
92
  ) {
56
93
  this.ws = ws;
57
94
  this.requestHandlers = requestHandlers;
95
+ this.globalMiddlewares = globalMiddlewares;
58
96
  this.nxtRequestId = 0;
97
+ this.logger = logger ?? { error: console.error };
98
+ this.timeouts = timeouts;
99
+ // reset connection timeout in each message received. It means connection still active
100
+ this.resetConnectionTimeout();
59
101
 
60
- this.ws.addEventListener("message", (event: WebSocket.MessageEvent) => {
61
- const eventData: SocketMessage = JSON.parse(event.data.toString());
62
- void this.handleEvent(eventData);
63
- });
102
+ this.ws.addEventListener(
103
+ "message",
104
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
105
+ async (event: WebSocket.MessageEvent) => {
106
+ const eventData: SocketMessage = JSON.parse(event.data.toString());
107
+ return this.handleMessage(eventData);
108
+ }
109
+ );
64
110
  }
65
111
 
66
- private async handleEvent(eventData: SocketMessage<unknown, unknown>) {
67
- if (eventData.request) {
112
+ protected async handleMessage(message: SocketMessage): Promise<void> {
113
+ this.resetConnectionTimeout();
114
+ if (message.request) {
68
115
  // Split the method string into parts
69
- const parts = eventData.request.method.split(".");
70
- let handlers = this.requestHandlers;
116
+ const parts = message.request.method.split(".");
117
+ let handlers = this.requestHandlers as Record<string, unknown>;
71
118
  for (const part of parts) {
72
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
73
- // @ts-ignore
74
- handlers = handlers[part];
119
+ handlers = handlers[part] as Record<string, unknown>;
75
120
  if (!handlers) {
76
- return this.respondError(eventData.request.id, {
121
+ return this.respondError(message.request.id, {
77
122
  code: 2,
78
- message: `unknown method ${eventData.request.method}`,
123
+ message: `unknown method ${message.request.method}`,
79
124
  });
80
125
  }
81
126
  }
82
127
 
83
128
  if (!Array.isArray(handlers)) {
84
- return this.respondError(eventData.request.id, {
129
+ return this.respondError(message.request.id, {
85
130
  code: 2,
86
131
  message: "unknown method",
87
132
  });
88
133
  }
89
- if (eventData.request.setAuthorization) {
90
- this.peerAuthorization = eventData.request.setAuthorization;
91
- }
92
- const middlewareHandlers = handlers.slice(0, -1) as MiddlewareHandler<
93
- unknown,
94
- CallableMethods,
95
- RequestContext
96
- >[];
97
- const handler = handlers[handlers.length - 1] as MethodHandler<
98
- unknown,
99
- unknown,
134
+ handlers = [...this.globalMiddlewares, ...handlers] as MethodHandlers<
135
+ ImplementedMethods,
100
136
  CallableMethods,
101
137
  RequestContext
102
138
  >;
103
- const reqCtx = {} as RequestContext;
104
- let response: unknown;
105
- // TODO(george): maybe we should not create a new client for each request
139
+ if (message.request.setAuthorization) {
140
+ this.peerAuthorization = message.request.setAuthorization;
141
+ }
142
+ const reqCtx = {
143
+ peerAuthorization: this.peerAuthorization,
144
+ method: message.request.method,
145
+ requestId: message.request.id,
146
+ } as RequestContext;
106
147
  const client = createISocketClient(this);
107
- try {
108
- for (const middlewareHandler of middlewareHandlers) {
109
- await middlewareHandler(
110
- eventData.request.payload,
111
- this.peerAuthorization,
148
+ const payload = message.request.payload;
149
+ let alreadyResponded = false;
150
+ const generateNextFn = (idx: number): (() => Promise<unknown>) => {
151
+ let wasCalled = false;
152
+ return async () => {
153
+ if (alreadyResponded) {
154
+ throw new SocketErrorException(
155
+ 4,
156
+ "next() was called after the response was sent"
157
+ );
158
+ }
159
+ const handler = (
160
+ handlers as unknown as NonEmptyArray<
161
+ MethodHandler<unknown, unknown, CallableMethods, RequestContext>
162
+ >
163
+ )[idx];
164
+ if (!handler) {
165
+ throw new SocketErrorException(
166
+ 5,
167
+ "cannot call past the last handler in the chain"
168
+ );
169
+ }
170
+ if (wasCalled) {
171
+ throw new SocketErrorException(
172
+ 6,
173
+ "next() was called multiple times"
174
+ );
175
+ }
176
+ wasCalled = true;
177
+ return this.callHandler(
178
+ handler,
179
+ payload,
180
+ reqCtx,
112
181
  client,
113
- reqCtx
182
+ generateNextFn(idx + 1)
114
183
  );
115
- }
116
- response = await handler(eventData.request.payload, client, reqCtx);
117
- } catch (error: any) {
118
- return this.respondError(eventData.request.id, {
119
- code: 3,
120
- message: error.toString(),
121
- });
184
+ };
185
+ };
186
+ let response: unknown;
187
+ try {
188
+ // call the first handler in the chain
189
+ response = await generateNextFn(0)();
190
+ } catch (error) {
191
+ const socketError =
192
+ error instanceof SocketErrorException
193
+ ? { code: error.code, message: error.message }
194
+ : { code: 3, message: (error as Error).toString() };
195
+ return this.respondError(message.request.id, socketError);
122
196
  }
123
- this.respond(eventData.request.id, response);
124
- } else if (eventData.response && eventData.response.id) {
125
- if (!this.responseHandler[eventData.response.id]) {
197
+ this.respond(message.request.id, response);
198
+ alreadyResponded = true;
199
+ } else if (message.response && message.response.id) {
200
+ const responseHandler = this.responseHandler.get(message.response.id);
201
+ if (!responseHandler) {
126
202
  return;
127
203
  }
128
- if (eventData.response.error) {
129
- this.responseHandler[eventData.response.id].reject(
130
- eventData.response.error
131
- );
204
+ if (message.response.error) {
205
+ responseHandler.reject(message.response.error);
132
206
  }
133
207
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
- this.responseHandler[eventData.response.id].resolve(
135
- eventData.response.payload as any
136
- );
137
- delete this.responseHandler[eventData.response.id];
208
+ responseHandler.resolve(message.response.payload as any);
209
+ clearTimeout(responseHandler.timeout);
210
+ this.responseHandler.delete(message.response.id);
138
211
  } else {
139
212
  return this.respondError(-1, {
140
213
  code: 3,
@@ -143,6 +216,21 @@ export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
143
216
  }
144
217
  }
145
218
 
219
+ protected async callHandler<
220
+ Params,
221
+ Result,
222
+ CallableMethods,
223
+ RequestContext extends RequestContextBase
224
+ >(
225
+ handler: MethodHandler<Params, Result, CallableMethods, RequestContext>,
226
+ params: Params,
227
+ ctx: RequestContext,
228
+ client: ISocketClient<CallableMethods>,
229
+ next: () => Promise<Result>
230
+ ): Promise<Result> {
231
+ return handler(params, ctx, client, next);
232
+ }
233
+
146
234
  public request<Params, Result>(
147
235
  method: string,
148
236
  params: Params,
@@ -150,10 +238,10 @@ export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
150
238
  ): Promise<Result> {
151
239
  return new Promise<Result>((resolve, reject) => {
152
240
  const requestId = ++this.nxtRequestId;
153
- this.responseHandler[requestId] = {
241
+ this.responseHandler.set(requestId, {
154
242
  resolve: (result) => resolve(result as Result),
155
243
  reject: (error: SocketError) => reject(error),
156
- };
244
+ });
157
245
  const toSend: SocketMessage = {
158
246
  request: {
159
247
  method,
@@ -163,10 +251,11 @@ export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
163
251
  },
164
252
  };
165
253
  this.ws.send(JSON.stringify(toSend));
254
+ this.resetNoResponseTimeout(requestId);
166
255
  });
167
256
  }
168
257
 
169
- private respond<Result>(requestId: number, result: Result): void {
258
+ protected respond<Result>(requestId: number, result: Result): void {
170
259
  const toSend: SocketMessage = {
171
260
  response: {
172
261
  payload: result,
@@ -177,7 +266,7 @@ export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
177
266
  return this.ws.send(JSON.stringify(toSend));
178
267
  }
179
268
 
180
- private respondError(requestId: number, error: SocketError): void {
269
+ protected respondError(requestId: number, error: SocketError): void {
181
270
  const toSend: SocketMessage = {
182
271
  response: {
183
272
  payload: null,
@@ -188,7 +277,66 @@ export class ISocket<ImplementedMethods, CallableMethods, RequestContext> {
188
277
  return this.ws.send(JSON.stringify(toSend));
189
278
  }
190
279
 
280
+ private resetConnectionTimeout(): void {
281
+ if (!this.timeouts?.connectionTimeoutInSeconds) {
282
+ return;
283
+ }
284
+ if (this.connectionTimeout) {
285
+ clearTimeout(this.connectionTimeout);
286
+ }
287
+ // Set a new timeout for the next message
288
+ this.connectionTimeout = setTimeout(
289
+ this.handleConnectionTimeout(),
290
+ this.timeouts?.connectionTimeoutInSeconds * 1000
291
+ );
292
+ }
293
+
294
+ private handleConnectionTimeout() {
295
+ return () => {
296
+ this.logger.error(
297
+ `Connection timed out after ${this.timeouts?.connectionTimeoutInSeconds} seconds`
298
+ );
299
+ this.close();
300
+ };
301
+ }
302
+
303
+ private resetNoResponseTimeout(requestId: number): void {
304
+ if (!this.timeouts?.noResponseTimeoutInSeconds) {
305
+ return;
306
+ }
307
+ const responseHandler = this.responseHandler.get(requestId);
308
+ if (!responseHandler) {
309
+ return;
310
+ }
311
+ const noResponseTimeout = responseHandler.timeout;
312
+ const reject = responseHandler.reject;
313
+ if (responseHandler.timeout) {
314
+ clearTimeout(noResponseTimeout);
315
+ }
316
+ // Set a new timeout for the next message
317
+ responseHandler.timeout = setTimeout(
318
+ this.handleNoResponseTimeout(reject),
319
+ this.timeouts?.noResponseTimeoutInSeconds * 1000
320
+ );
321
+ }
322
+
323
+ private handleNoResponseTimeout(reject: (error: SocketError) => void) {
324
+ return () => {
325
+ const message = `Request timed out after ${this.timeouts?.noResponseTimeoutInSeconds} seconds`;
326
+ this.logger.error(message);
327
+ reject({ code: 7, message });
328
+ };
329
+ }
330
+
191
331
  public close(): void {
332
+ clearTimeout(this.connectionTimeout);
333
+ this.responseHandler.forEach((handler, key) => {
334
+ clearTimeout(handler.timeout);
335
+ this.logger.error(
336
+ `Rejecting pending requestId ${key} due to connection close`
337
+ );
338
+ handler.reject({ code: 8, message: "Connection closed" });
339
+ });
192
340
  this.ws.close();
193
341
  }
194
342
  }
@@ -200,7 +348,7 @@ const proxyTarget = Object.freeze(() => {
200
348
  function createIsocketProxy<
201
349
  ImplementedMethods,
202
350
  CallableMethods,
203
- RequestContext
351
+ RequestContext extends RequestContextBase
204
352
  >(
205
353
  socket: ISocket<ImplementedMethods, CallableMethods, RequestContext>,
206
354
  // if path is undefined, it means the current object is the root object
@@ -238,13 +386,13 @@ function createIsocketProxy<
238
386
  export function createISocketClient<
239
387
  CallableMethods,
240
388
  ImplementedMethods,
241
- RequestContext
389
+ RequestContext extends RequestContextBase
242
390
  >(
243
391
  socket: ISocket<ImplementedMethods, CallableMethods, RequestContext>
244
392
  ): ISocketClient<CallableMethods> {
245
393
  return {
246
394
  close: () => socket.close(),
247
- // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment
395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
396
  call: createIsocketProxy(socket, undefined) as any,
249
397
  };
250
398
  }
@@ -7,8 +7,6 @@ export interface UserMeDto {
7
7
 
8
8
  export interface FlagBootstrap {
9
9
  "ui.enable-resource-signing"?: boolean;
10
- // BE and UI flags are merged so we are using UI flag here
11
- "ui.multi-page.enabled"?: boolean;
12
10
  }
13
11
 
14
12
  export type User = {
@@ -153,3 +151,9 @@ export interface RemoteCommitDto {
153
151
  branchName: string;
154
152
  repositoryId: string;
155
153
  }
154
+
155
+ export type ViewMode =
156
+ | "export-deployed"
157
+ | "export-latest"
158
+ | "export-live"
159
+ | "export-commit";
@@ -1,28 +1,32 @@
1
+ export interface RequestContextBase {
2
+ peerAuthorization?: string;
3
+ method: string;
4
+ requestId: number;
5
+ }
6
+
1
7
  export type MethodHandler<Params, Result, PeerMethods, RequestContext> = (
2
8
  params: Params,
9
+ ctx: RequestContext,
3
10
  peer: ISocketClient<PeerMethods>,
4
- ctx: RequestContext
11
+ next: () => Promise<Result>
5
12
  ) => Promise<Result>;
6
13
 
7
- export type MiddlewareHandler<Params, PeerMethods, RequestContext> = (
8
- params: Params,
9
- peerAuthorization: string | undefined,
10
- peer: ISocketClient<PeerMethods>,
11
- ctx: RequestContext
12
- ) => Promise<void>;
14
+ // A generic middleware is a method handler that can be chained with any other method handler, so it uses `any` for the params and result types
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ export type GenericMiddleware<PeerMethods, RequestContext> = MethodHandler<
17
+ any,
18
+ any,
19
+ PeerMethods,
20
+ RequestContext
21
+ >;
22
+
23
+ export type NonEmptyArray<T> = [...T[], T];
13
24
 
14
25
  export type MethodHandlers<Methods, PeerMethods, RequestContext> = {
15
26
  [Key in keyof Methods]: Methods[Key] extends (
16
27
  params: infer Params
17
28
  ) => Promise<infer Result>
18
- ? [
19
- ...middlewareHandlers: MiddlewareHandler<
20
- Params,
21
- PeerMethods,
22
- RequestContext
23
- >[],
24
- handler: MethodHandler<Params, Result, PeerMethods, RequestContext>
25
- ]
29
+ ? NonEmptyArray<MethodHandler<Params, Result, PeerMethods, RequestContext>>
26
30
  : Methods[Key] extends Record<string, unknown>
27
31
  ? MethodHandlers<Methods[Key], PeerMethods, RequestContext>
28
32
  : never;
@@ -46,3 +50,20 @@ export type ISocketClient<Methods> = {
46
50
  export type MethodSchema<Params, Response> = (
47
51
  params: Params
48
52
  ) => Promise<Response>;
53
+
54
+ /** The exception type can be thrown to cause the default error handler to write a raw error response to the socket */
55
+ export class SocketErrorException extends Error {
56
+ readonly code: number;
57
+ readonly message: string;
58
+
59
+ constructor(code: number, message: string) {
60
+ super(message);
61
+ this.code = code;
62
+ this.message = message;
63
+ }
64
+ }
65
+
66
+ export type SocketTimeouts = {
67
+ noResponseTimeoutInSeconds?: number;
68
+ connectionTimeoutInSeconds?: number;
69
+ };