From a9cffa9102bee5b2ff7fe12fb4bac5a71e8d1284 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Wed, 13 Sep 2023 19:46:42 -0500 Subject: [PATCH] tests, working, some refactors --- apps/tests/aws-runtime/package.json | 2 + apps/tests/aws-runtime/test/test-service.ts | 211 ++++++++++++++++-- apps/tests/aws-runtime/test/tester.test.ts | 10 + .../@eventual/aws-cdk/src/entity-service.ts | 3 + .../@eventual/aws-cdk/src/socket-service.ts | 80 ++++--- packages/@eventual/aws-cdk/src/utils.ts | 1 + .../@eventual/aws-cdk/src/workflow-service.ts | 6 + .../aws-runtime/src/clients/socket-client.ts | 23 +- packages/@eventual/aws-runtime/src/create.ts | 2 +- .../aws-runtime/src/handlers/socket-worker.ts | 9 +- packages/@eventual/cli/src/display/event.ts | 19 +- .../send-socket-call-executor.ts | 10 - .../call-executors/socket-call-executor.ts | 14 ++ .../core-runtime/src/clients/socket-client.ts | 15 +- .../core-runtime/src/handlers/index.ts | 2 +- ...ket-handler-worker.ts => socket-worker.ts} | 2 +- .../core-runtime/src/handlers/worker.ts | 6 +- .../src/local/clients/socket-client.ts | 4 + .../core-runtime/src/transaction-executor.ts | 4 +- .../src/workflow/call-eventual-factory.ts | 2 +- .../src/workflow/call-executor.ts | 7 +- .../send-socket-call.ts | 109 +++++++-- packages/@eventual/core/src/internal/calls.ts | 36 ++- .../core/src/internal/workflow-events.ts | 74 ++++-- packages/@eventual/core/src/socket.ts | 43 +++- pnpm-lock.yaml | 73 +++++- 26 files changed, 618 insertions(+), 149 deletions(-) delete mode 100644 packages/@eventual/core-runtime/src/call-executors/send-socket-call-executor.ts create mode 100644 packages/@eventual/core-runtime/src/call-executors/socket-call-executor.ts rename packages/@eventual/core-runtime/src/handlers/{socket-handler-worker.ts => socket-worker.ts} (96%) diff --git a/apps/tests/aws-runtime/package.json b/apps/tests/aws-runtime/package.json index b1147a3e2..898f59acf 100644 --- a/apps/tests/aws-runtime/package.json +++ b/apps/tests/aws-runtime/package.json @@ -21,6 +21,7 @@ "@eventual/cli": "workspace:^", "@eventual/client": "workspace:^", "@eventual/core": "workspace:^", + "ws": "^8.14.1", "zod": "^3.21.4" }, "devDependencies": { @@ -30,6 +31,7 @@ "@types/aws-lambda": "^8.10.115", "@types/jest": "^29.5.1", "@types/node": "^18", + "@types/ws": "^8.5.5", "aws-cdk": "^2.80.0", "esbuild": "^0.17.4", "jest": "^29", diff --git a/apps/tests/aws-runtime/test/test-service.ts b/apps/tests/aws-runtime/test/test-service.ts index c66698590..b7f34004f 100644 --- a/apps/tests/aws-runtime/test/test-service.ts +++ b/apps/tests/aws-runtime/test/test-service.ts @@ -28,6 +28,7 @@ import { sendSignal, sendTaskHeartbeat, signal, + socket, subscription, task, time, @@ -36,6 +37,7 @@ import { } from "@eventual/core"; import type openapi from "openapi3-ts"; import { Readable } from "stream"; +import { WebSocket } from "ws"; import z from "zod"; import { AsyncWriterTestEvent } from "./async-writer-handler.js"; @@ -1378,23 +1380,198 @@ export const searchBlog = command( } ); -// check types of entity -function streamShouldHaveEmptyIfNoInclude() { - counter.stream("", {}, (item) => { - if (item.operation === "modify") { - // @ts-expect-error - no oldValue without includeOld: true - item.oldValue!.namespace; +/** + * Socket Test! + * + * 1. a command start a workflow that will communicate with the sockets. + * 2. the command starts 2 web sockets against socket1 + * 3. on each connection, the socket will send a signal to the workflow + * 4. when the workflow get the connection, it will send a start signal to the socket + * 5. the socket will send a "n" (local id for the connection) and value to the socket, which will forward the message to the workflow + * 6. when the workflow has received 2 connections with n and v values, it will send a message to each socket with the n and v incremented by 1 + * 7. the socket will resolve the value in the connection, completing the test + */ + +interface SocketMessage { + id: string; + v: number; +} + +export const socket1 = socket("socket1", { + $connect: async ({ connectionId, query }) => { + console.log(query); + const { id, n } = (query ?? {}) as { n?: string; id?: string }; + if (!id || !n) { + throw new Error("Missing ID"); } - }); - counter.stream( - "", - { - includeOld: true, - }, - (item) => { - if (item.operation === "modify") { - item.oldValue.namespace; - } + console.log("sending signal to", id); + await socketConnectSignal.sendSignal(id, { + connectionId, + n: Number(n), + }); + console.log("signal sent to", id); + }, + $disconnect: async () => undefined, + $default: async ({ connectionId, body }) => { + console.log(body); + const data = body ? (JSON.parse(body) as SocketMessage) : undefined; + console.log("data", data); + if (!data) { + throw new Error("Expected data"); } - ); + console.log("sending signal to", data.id); + await socketMessageSignal.sendSignal(data.id, { + ...data, + connectionId, + }); + console.log("signal sent to", data.id); + }, +}); + +export const socketConnectSignal = signal<{ connectionId: string; n: number }>( + "socketConnectSignal" +); +export const socketMessageSignal = signal<{ + connectionId: string; + v: number; +}>("socketMessageSignal"); + +interface StartSocketEvent { + type: "start"; + n: number; + v: number; +} + +interface DataSocketEvent { + type: "data"; + n: number; + v: number; +} + +export const socketWorkflow = workflow( + "socketWorkflow", + { timeout: duration(15, "minutes") }, + async () => { + const connections: Record = {}; + + socketConnectSignal.onSignal(async ({ connectionId, n }) => { + connections[connectionId] = { n }; + await socket1.send( + connectionId, + JSON.stringify({ + type: "start", + n, + v: n + 1, + } satisfies StartSocketEvent) + ); + }); + + socketMessageSignal.onSignal(({ connectionId, v }) => { + const entry = connections[connectionId]; + if (entry) { + entry.v = v; + } + }); + + await condition( + () => + Object.keys(connections).length > 1 && + Object.values(connections).every((c) => c.v !== undefined) + ); + + await Promise.all( + Object.entries(connections).map(async ([connectionId, { n, v }]) => { + await socket1.send( + connectionId, + JSON.stringify({ + type: "data", + n, + v: (v ?? 0) + 1, + } satisfies DataSocketEvent) + ); + }) + ); + + // close the connections + await Promise.all( + Object.entries(connections).map(async ([connectionId]) => { + await socket1.delete(connectionId); + }) + ); + } +); + +export const socketTest = command( + "socketTest", + { handlerTimeout: duration(5, "minute") }, + async () => { + // start workflow that waits for connections + const { executionId } = await socketWorkflow.startExecution({ + input: undefined, + }); + const encodedId = encodeURIComponent(executionId); + + console.log("pre-socket"); + + const ws1 = new WebSocket(`${socket1.wssEndpoint}?id=${encodedId}&n=0`); + const ws2 = new WebSocket(`${socket1.wssEndpoint}?id=${encodedId}&n=1`); + + console.log("setup-socket"); + + const running1 = setupWS(executionId, ws1); + const running2 = setupWS(executionId, ws2); + + console.log("waiting..."); + + return await Promise.all([running1, running2]); + } +); + +function setupWS(executionId: string, ws: WebSocket) { + let n: number | undefined; + let v: number | undefined; + return new Promise((resolve, reject) => { + ws.on("error", (err) => { + console.log("error", err); + reject(err); + }); + ws.on("message", (data) => { + try { + console.log(n, "message"); + const d = (data as Buffer).toString("utf8"); + console.log(d); + const event = JSON.parse(d) as StartSocketEvent | DataSocketEvent; + if (event.type === "start") { + n = event.n; + // after connecting, we will send our "n" and incremented "value" back. + ws.send( + JSON.stringify({ + id: executionId, + v: event.v + 1, + } satisfies SocketMessage) + ); + } else if (event.type === "data") { + v = event.v; + } else { + console.log("unexpected event", event); + reject(event); + } + } catch (err) { + console.error(err); + reject(err); + } + }); + ws.on("close", (code, reason) => { + try { + console.log(code, reason.toString("utf-8")); + console.log(n, "close", v); + if (n === undefined) { + throw new Error("n was not set"); + } + resolve(v ?? -1); + } catch (err) { + reject(err); + } + }); + }); } diff --git a/apps/tests/aws-runtime/test/tester.test.ts b/apps/tests/aws-runtime/test/tester.test.ts index 0038b6770..426f4e5e1 100644 --- a/apps/tests/aws-runtime/test/tester.test.ts +++ b/apps/tests/aws-runtime/test/tester.test.ts @@ -444,6 +444,16 @@ test("test service context", async () => { }); }); +test("socket test", async () => { + const rpcResponse = await ( + await fetch(`${url}/${commandRpcPath({ name: "socketTest" })}`, { + method: "POST", + }) + ).json(); + + expect(rpcResponse).toEqual([3, 4]); +}); + if (!process.env.TEST_LOCAL) { test("index.search", async () => { const serviceClient = new ServiceClient({ diff --git a/packages/@eventual/aws-cdk/src/entity-service.ts b/packages/@eventual/aws-cdk/src/entity-service.ts index d48d148ea..4b6b368c9 100644 --- a/packages/@eventual/aws-cdk/src/entity-service.ts +++ b/packages/@eventual/aws-cdk/src/entity-service.ts @@ -139,6 +139,9 @@ export class EntityService { this.configureReadWriteEntityTable(this.transactionWorker); props.workflowService.configureSendSignal(this.transactionWorker); props.eventService.configureEmit(this.transactionWorker); + props.socketService.configureInvokeSocketEndpoints( + this.transactionWorker + ); } } diff --git a/packages/@eventual/aws-cdk/src/socket-service.ts b/packages/@eventual/aws-cdk/src/socket-service.ts index 04559dc0e..2723c9a1c 100644 --- a/packages/@eventual/aws-cdk/src/socket-service.ts +++ b/packages/@eventual/aws-cdk/src/socket-service.ts @@ -1,28 +1,28 @@ -import { IWebSocketApi, WebSocketApi } from "@aws-cdk/aws-apigatewayv2-alpha"; -import { WebSocketLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; import { - ENV_NAMES, - socketServiceSocketName, - socketServiceSocketSuffix, -} from "@eventual/aws-runtime"; + IWebSocketApi, + IWebSocketStage, + WebSocketApi, + WebSocketNoneAuthorizer, + WebSocketStage, +} from "@aws-cdk/aws-apigatewayv2-alpha"; +import { WebSocketLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; +import { ENV_NAMES, socketServiceSocketName } from "@eventual/aws-runtime"; import { SocketFunction } from "@eventual/core-runtime"; import { SocketUrls } from "@eventual/core/internal/index.js"; -import { - Effect, - IGrantable, - IPrincipal, - PolicyStatement, -} from "aws-cdk-lib/aws-iam"; +import { IGrantable, IPrincipal } from "aws-cdk-lib/aws-iam"; import type { Function, FunctionProps } from "aws-cdk-lib/aws-lambda"; -import { Duration, Stack } from "aws-cdk-lib/core"; +import { Duration } from "aws-cdk-lib/core"; import { Construct } from "constructs"; import { SpecHttpApiProps } from "./constructs/spec-http-api.js"; import { DeepCompositePrincipal } from "./deep-composite-principal.js"; import { EventualResource } from "./resource.js"; -import { WorkerServiceConstructProps } from "./service-common.js"; +import { + WorkerServiceConstructProps, + configureWorkerCalls, +} from "./service-common.js"; import { ServiceFunction } from "./service-function.js"; import { ServiceLocal } from "./service.js"; -import { ServiceEntityProps, serviceApiArn } from "./utils.js"; +import { ServiceEntityProps } from "./utils.js"; export type ApiOverrides = Omit; @@ -51,12 +51,12 @@ export class SocketService { */ public readonly sockets: Sockets; - constructor(private props: SocketsProps) { + constructor(props: SocketsProps) { const socketsScope = new Construct(props.serviceScope, "Sockets"); this.sockets = Object.fromEntries( - Object.entries(props.build.sockets).map(([name, socket]) => [ - name, + props.build.sockets.map((socket) => [ + socket.spec.name, new Socket(socketsScope, { serviceProps: props, socketService: this, @@ -76,8 +76,8 @@ export class SocketService { [ name, { - http: socket.gateway.apiEndpoint, - wss: socket.gateway.apiEndpoint.replace("https://", "wss://"), + http: socket.gatewayStage.url.replace("wss://", "https://"), + wss: socket.gatewayStage.url, } satisfies SocketUrls, ] as const ) @@ -95,25 +95,11 @@ export class SocketService { } public grantInvokeSocketEndpoints(grantable: IGrantable) { - grantable.grantPrincipal.addToPrincipalPolicy( - this.executeApiPolicyStatement() + // generally we want to compute the grants, but apigateway urls use the generated appid and not the name + Object.values(this.sockets).map((s) => + s.gateway.grantManageConnections(grantable) ); } - - private executeApiPolicyStatement() { - return new PolicyStatement({ - actions: ["execute-api:*"], - effect: Effect.ALLOW, - resources: [ - serviceApiArn( - this.props.serviceName, - Stack.of(this.props.serviceScope), - socketServiceSocketSuffix("*"), - false - ), - ], - }); - } } interface SocketProps { @@ -131,7 +117,8 @@ export interface ISocket { class Socket extends Construct implements EventualResource, ISocket { public grantPrincipal: IPrincipal; - public gateway: IWebSocketApi; + public gateway: WebSocketApi; + public gatewayStage: IWebSocketStage; public handler: Function; constructor(scope: Construct, props: SocketProps) { @@ -155,7 +142,7 @@ class Socket extends Construct implements EventualResource, ISocket { overrides: props.serviceProps.overrides?.[socketName], }); - const integration = new WebSocketLambdaIntegration("default", this.handler); + configureWorkerCalls(props.serviceProps, this.handler); this.gateway = new WebSocketApi(this, "Gateway", { apiName: socketServiceSocketName( @@ -163,16 +150,25 @@ class Socket extends Construct implements EventualResource, ISocket { socketName ), defaultRouteOptions: { - integration, + // https://stackoverflow.com/a/72716478 + // must create one integration per... + integration: new WebSocketLambdaIntegration("default", this.handler), }, connectRouteOptions: { - integration, + integration: new WebSocketLambdaIntegration("Connect", this.handler), + authorizer: new WebSocketNoneAuthorizer(), }, disconnectRouteOptions: { - integration, + integration: new WebSocketLambdaIntegration("Disconnect", this.handler), }, }); + this.gatewayStage = new WebSocketStage(this, "Stage", { + stageName: "default", + webSocketApi: this.gateway, + autoDeploy: true, + }); + this.grantPrincipal = props.local ? new DeepCompositePrincipal( this.handler.grantPrincipal, diff --git a/packages/@eventual/aws-cdk/src/utils.ts b/packages/@eventual/aws-cdk/src/utils.ts index b79a13e82..0169f1c75 100644 --- a/packages/@eventual/aws-cdk/src/utils.ts +++ b/packages/@eventual/aws-cdk/src/utils.ts @@ -140,6 +140,7 @@ export function serviceApiArn( resource: sanitized ? socketServiceSocketName(serviceName, nameSuffix) : `${serviceName}-${nameSuffix}`, + resourceName: "*/*/*/*", arnFormat: ArnFormat.SLASH_RESOURCE_NAME, }); } diff --git a/packages/@eventual/aws-cdk/src/workflow-service.ts b/packages/@eventual/aws-cdk/src/workflow-service.ts index aa5ee4f55..d560985b5 100644 --- a/packages/@eventual/aws-cdk/src/workflow-service.ts +++ b/packages/@eventual/aws-cdk/src/workflow-service.ts @@ -37,6 +37,7 @@ import type { TaskService } from "./task-service.js"; import type { SearchService } from "./search/search-service"; import { ServiceConstructProps } from "./service-common"; import { QueueService } from "./queue-service"; +import { SocketService } from "./socket-service"; export interface WorkflowsProps extends ServiceConstructProps { bucketService: LazyInterface>; @@ -45,6 +46,7 @@ export interface WorkflowsProps extends ServiceConstructProps { eventService: EventService; overrides?: WorkflowServiceOverrides; schedulerService: LazyInterface; + socketService: LazyInterface; taskService: LazyInterface; queueService: LazyInterface>; } @@ -488,6 +490,10 @@ export class WorkflowService { * Queue Calls */ this.props.queueService.configureSendMessage(this.orchestrator); + /** + * Socket Calls + */ + this.props.socketService.configureInvokeSocketEndpoints(this.orchestrator); } private readonly ENV_MAPPINGS = { diff --git a/packages/@eventual/aws-runtime/src/clients/socket-client.ts b/packages/@eventual/aws-runtime/src/clients/socket-client.ts index 238ac8338..bf07951f6 100644 --- a/packages/@eventual/aws-runtime/src/clients/socket-client.ts +++ b/packages/@eventual/aws-runtime/src/clients/socket-client.ts @@ -1,14 +1,19 @@ import { - ApiGatewayManagementApiClient, + DeleteConnectionCommand, PostToConnectionCommand, + type ApiGatewayManagementApiClient, } from "@aws-sdk/client-apigatewaymanagementapi"; -import { LazyValue, SocketClient, getLazy } from "@eventual/core-runtime"; +import { + getLazy, + type LazyValue, + type SocketClient, +} from "@eventual/core-runtime"; import type { SocketUrls } from "@eventual/core/internal"; export type SocketEndpoints = Record; export interface AWSSocketClientProps { - apiGatewayManagementClientFactory: ( + apiGatewayManagementClientRetriever: ( socketUrl: string ) => ApiGatewayManagementApiClient; socketUrls: LazyValue; @@ -22,7 +27,7 @@ export class AWSSocketClient implements SocketClient { connectionId: string, input: string | Buffer ): Promise { - const client = this.props.apiGatewayManagementClientFactory( + const client = this.props.apiGatewayManagementClientRetriever( this.socketUrls(socketName).http ); @@ -34,6 +39,16 @@ export class AWSSocketClient implements SocketClient { ); } + public async delete(socketName: string, connectionId: string): Promise { + const client = this.props.apiGatewayManagementClientRetriever( + this.socketUrls(socketName).http + ); + + await client.send( + new DeleteConnectionCommand({ ConnectionId: connectionId }) + ); + } + public socketUrls(socketName: string): SocketUrls { const endpoints = getLazy(this.props.socketUrls)[socketName]; if (!endpoints) { diff --git a/packages/@eventual/aws-runtime/src/create.ts b/packages/@eventual/aws-runtime/src/create.ts index b878ae2f3..fdb02bc35 100644 --- a/packages/@eventual/aws-runtime/src/create.ts +++ b/packages/@eventual/aws-runtime/src/create.ts @@ -139,7 +139,7 @@ export const createApiGatewayManagementClient = /* @__PURE__ */ memoize( export const createSocketClient = /* @__PURE__ */ memoize(() => { return new AWSSocketClient({ socketUrls, - apiGatewayManagementClientFactory: (url) => + apiGatewayManagementClientRetriever: (url) => createApiGatewayManagementClient({ socketUrl: url }), }); }); diff --git a/packages/@eventual/aws-runtime/src/handlers/socket-worker.ts b/packages/@eventual/aws-runtime/src/handlers/socket-worker.ts index 5d1cb3df9..ce0260d07 100644 --- a/packages/@eventual/aws-runtime/src/handlers/socket-worker.ts +++ b/packages/@eventual/aws-runtime/src/handlers/socket-worker.ts @@ -1,3 +1,7 @@ +import serviceSpec from "@eventual/injected/spec"; +// the user's entry point will register streams as a side effect. +import "@eventual/injected/entry"; + import { SocketHandlerWorkerEvent, createSocketHandlerWorker, @@ -17,7 +21,6 @@ import { createSocketClient, } from "../create.js"; import { serviceName, serviceUrl, socketName } from "../env.js"; -import serviceSpec from "../injected/service-spec.js"; const worker = createSocketHandlerWorker({ bucketStore: createBucketStore(), @@ -72,5 +75,7 @@ export default async ( }; } - return undefined; + return { + statusCode: 200, + }; }; diff --git a/packages/@eventual/cli/src/display/event.ts b/packages/@eventual/cli/src/display/event.ts index d7f1f2111..ca6fe185c 100644 --- a/packages/@eventual/cli/src/display/event.ts +++ b/packages/@eventual/cli/src/display/event.ts @@ -9,6 +9,7 @@ import { isEntityRequest, isSignalReceived, isSignalSent, + isSocketRequest, isTaskScheduled, isTransactionRequest, type BucketRequest, @@ -35,6 +36,19 @@ export function displayEvent(event: WorkflowEvent) { }`, ...(isCallEvent(event) ? [ + ...("operation" in event.event + ? typeof event.event.operation === "object" && + "operation" in event.event.operation + ? [`Operation: ${event.event.operation.operation}`] + : [`Operation: ${event.event.operation}`] + : []), + ...(isSocketRequest(event.event) + ? [`Socket: ${event.event.operation.socketName}`] + : []), + ...(isSocketRequest(event.event) && + event.event.operation.operation === "send" + ? [`Input: ${event.event.operation.input}`] + : []), ...(isChildWorkflowScheduled(event.event) || isTaskScheduled(event.event) ? [`Task Name: ${JSON.stringify(event.event.name)}`] @@ -42,7 +56,9 @@ export function displayEvent(event: WorkflowEvent) { ...(isTransactionRequest(event.event) ? [`Transaction Name: ${event.event.transactionName}`] : []), - ...("signalId" in event ? [`Signal Id: ${event.signalId}`] : []), + ...("signalId" in event.event + ? [`Signal Id: ${event.event.signalId}`] + : []), ...((isChildWorkflowScheduled(event.event) || isTransactionRequest(event.event) || isTaskScheduled(event.event)) && @@ -63,6 +79,7 @@ export function displayEvent(event: WorkflowEvent) { ...(isSignalReceived(event) && event.payload ? [`Payload: ${JSON.stringify(event.payload)}`] : []), + ...("signalId" in event ? [`Signal Id: ${event.signalId}`] : []), ...("result" in event ? [`Result: ${JSON.stringify(event.result)}`] : []), ...("output" in event ? [`Output: ${JSON.stringify(event.output)}`] : []), ...("error" in event diff --git a/packages/@eventual/core-runtime/src/call-executors/send-socket-call-executor.ts b/packages/@eventual/core-runtime/src/call-executors/send-socket-call-executor.ts deleted file mode 100644 index e8ede3738..000000000 --- a/packages/@eventual/core-runtime/src/call-executors/send-socket-call-executor.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CallOutput, SocketSendCall } from "@eventual/core/internal"; -import type { CallExecutor } from "../call-executor.js"; -import type { SocketClient } from "../clients/socket-client.js"; - -export class SocketSendCallExecutor implements CallExecutor { - constructor(private socketClient: SocketClient) {} - public execute(call: SocketSendCall): Promise> { - return this.socketClient.send(call.name, call.connectionId, call.input); - } -} diff --git a/packages/@eventual/core-runtime/src/call-executors/socket-call-executor.ts b/packages/@eventual/core-runtime/src/call-executors/socket-call-executor.ts new file mode 100644 index 000000000..51421f625 --- /dev/null +++ b/packages/@eventual/core-runtime/src/call-executors/socket-call-executor.ts @@ -0,0 +1,14 @@ +import type { CallOutput, SocketCall } from "@eventual/core/internal"; +import type { CallExecutor } from "../call-executor.js"; +import type { SocketClient } from "../clients/socket-client.js"; + +export class SocketCallExecutor implements CallExecutor { + constructor(private socketClient: SocketClient) {} + public execute(call: SocketCall): Promise> { + return this.socketClient[call.operation.operation]( + call.operation.socketName, + // @ts-ignore - typescript won't let me case the params... + ...call.operation.params + ); + } +} diff --git a/packages/@eventual/core-runtime/src/clients/socket-client.ts b/packages/@eventual/core-runtime/src/clients/socket-client.ts index 84c2ca44e..178734bef 100644 --- a/packages/@eventual/core-runtime/src/clients/socket-client.ts +++ b/packages/@eventual/core-runtime/src/clients/socket-client.ts @@ -1,10 +1,11 @@ -import { SocketUrls } from "@eventual/core/internal"; +import { Socket } from "@eventual/core"; +import { SocketMethod, SocketUrls } from "@eventual/core/internal"; -export interface SocketClient { - send( +export type SocketClient = { + [K in keyof Pick]: ( socketName: string, - connectionId: string, - input: Buffer | string - ): Promise; + ...args: Parameters + ) => ReturnType; +} & { socketUrls(socketName: string): SocketUrls; -} +}; diff --git a/packages/@eventual/core-runtime/src/handlers/index.ts b/packages/@eventual/core-runtime/src/handlers/index.ts index 8331b855e..017eb208c 100644 --- a/packages/@eventual/core-runtime/src/handlers/index.ts +++ b/packages/@eventual/core-runtime/src/handlers/index.ts @@ -4,7 +4,7 @@ export * from "./entity-stream-worker.js"; export * from "./orchestrator.js"; export * from "./queue-handler-worker.js"; export * from "./schedule-forwarder.js"; -export * from "./socket-handler-worker.js"; +export * from "./socket-worker.js"; export * from "./subscription-worker.js"; export * from "./task-fallback-handler.js"; export * from "./task-worker.js"; diff --git a/packages/@eventual/core-runtime/src/handlers/socket-handler-worker.ts b/packages/@eventual/core-runtime/src/handlers/socket-worker.ts similarity index 96% rename from packages/@eventual/core-runtime/src/handlers/socket-handler-worker.ts rename to packages/@eventual/core-runtime/src/handlers/socket-worker.ts index 3ca0dd85b..dc011af50 100644 --- a/packages/@eventual/core-runtime/src/handlers/socket-handler-worker.ts +++ b/packages/@eventual/core-runtime/src/handlers/socket-worker.ts @@ -25,7 +25,7 @@ export function createSocketHandlerWorker( dependencies: SocketHandlerDependencies ): SocketHandlerWorker { return createEventualWorker( - { serviceType: ServiceType.QueueHandlerWorker, ...dependencies }, + { serviceType: ServiceType.SocketWorker, ...dependencies }, async (socketName, event) => { const socket = getEventualResource("Socket", socketName); if (!socket) throw new Error(`Socket ${socketName} does not exist`); diff --git a/packages/@eventual/core-runtime/src/handlers/worker.ts b/packages/@eventual/core-runtime/src/handlers/worker.ts index f8ea05f42..fce7b30b2 100644 --- a/packages/@eventual/core-runtime/src/handlers/worker.ts +++ b/packages/@eventual/core-runtime/src/handlers/worker.ts @@ -10,7 +10,7 @@ import { BucketCallExecutor } from "../call-executors/bucket-call-executor.js"; import { EntityCallExecutor } from "../call-executors/entity-call-executor.js"; import { QueueCallExecutor } from "../call-executors/queue-call-executor.js"; import { SearchCallExecutor } from "../call-executors/search-call-executor.js"; -import { SocketSendCallExecutor } from "../call-executors/send-socket-call-executor.js"; +import { SocketCallExecutor } from "../call-executors/socket-call-executor.js"; import { ServiceClientExecutor } from "../call-executors/service-client-executor.js"; import type { OpenSearchClient } from "../clients/open-search-client.js"; import type { QueueClient } from "../clients/queue-client.js"; @@ -92,7 +92,7 @@ export function createEventualWorker( : unsupportedProperty; const [socketCallExecutor, socketUrlPropertyRetriever] = props.socketClient ? [ - new SocketSendCallExecutor(props.socketClient), + new SocketCallExecutor(props.socketClient), new SocketUrlPropertyRetriever(props.socketClient), ] : [unsupportedExecutor, unsupportedProperty]; @@ -143,7 +143,7 @@ export function createEventualWorker( SignalHandlerCall: unsupportedExecutor, SearchCall: openSearchExecutor, SendSignalCall: serviceClientExecutor, - SocketSendCall: socketCallExecutor, + SocketCall: socketCallExecutor, StartWorkflowCall: serviceClientExecutor, // directly calling a task does not work outside of a workflow TaskCall: unsupportedExecutor, diff --git a/packages/@eventual/core-runtime/src/local/clients/socket-client.ts b/packages/@eventual/core-runtime/src/local/clients/socket-client.ts index eab30ed90..8b0fa04d3 100644 --- a/packages/@eventual/core-runtime/src/local/clients/socket-client.ts +++ b/packages/@eventual/core-runtime/src/local/clients/socket-client.ts @@ -10,6 +10,10 @@ export class LocalSocketClient implements SocketClient { throw new Error("Method not implemented."); } + public delete(_socketName: string, _connectionId: string): Promise { + throw new Error("Method not implemented."); + } + public socketUrls(_socketName: string): SocketUrls { throw new Error("Method not implemented."); } diff --git a/packages/@eventual/core-runtime/src/transaction-executor.ts b/packages/@eventual/core-runtime/src/transaction-executor.ts index 7ceae1e85..6a0252d3b 100644 --- a/packages/@eventual/core-runtime/src/transaction-executor.ts +++ b/packages/@eventual/core-runtime/src/transaction-executor.ts @@ -29,7 +29,7 @@ import { } from "./call-executor.js"; import { EmitEventsCallExecutor } from "./call-executors/emit-events-call-executor.js"; import { SendSignalCallExecutor } from "./call-executors/send-signal-call-executor.js"; -import { SocketSendCallExecutor } from "./call-executors/send-socket-call-executor.js"; +import { SocketCallExecutor } from "./call-executors/socket-call-executor.js"; import type { EventClient } from "./clients/event-client.js"; import type { ExecutionQueueClient } from "./clients/execution-queue-client.js"; import { enterEventualCallHookScope } from "./eventual-hook.js"; @@ -430,7 +430,7 @@ export function createTransactionCallExecutor( BucketCall: unsupportedExecutor, ConditionCall: unsupportedExecutor, EmitEventsCall: new EmitEventsCallExecutor(deps.eventClient), - SocketSendCall: new SocketSendCallExecutor(deps.socketClient), + SocketCall: new SocketCallExecutor(deps.socketClient), // the transaction execution handles this itself EntityCall: unsupportedExecutor, ExpectSignalCall: unsupportedExecutor, diff --git a/packages/@eventual/core-runtime/src/workflow/call-eventual-factory.ts b/packages/@eventual/core-runtime/src/workflow/call-eventual-factory.ts index 605bda7ce..24dffe2ef 100644 --- a/packages/@eventual/core-runtime/src/workflow/call-eventual-factory.ts +++ b/packages/@eventual/core-runtime/src/workflow/call-eventual-factory.ts @@ -81,7 +81,7 @@ export function createDefaultEventualFactory(): AllWorkflowEventualFactory { SearchCall: new SearchCallEventualFactory(), SendSignalCall: new SendSignalEventualFactory(), SignalHandlerCall: new RegisterSignalHandlerCallFactory(), - SocketSendCall: new SendSocketCallEventualFactory(), + SocketCall: new SendSocketCallEventualFactory(), StartWorkflowCall: unsupportedFactory, TaskCall: new TaskCallEventualFactory(), TaskRequestCall: unsupportedFactory, // TODO: support task requests (succeed, fail, heartbeat) diff --git a/packages/@eventual/core-runtime/src/workflow/call-executor.ts b/packages/@eventual/core-runtime/src/workflow/call-executor.ts index 0867b3a8b..6297f9b34 100644 --- a/packages/@eventual/core-runtime/src/workflow/call-executor.ts +++ b/packages/@eventual/core-runtime/src/workflow/call-executor.ts @@ -6,7 +6,6 @@ import { type EventualPromise, } from "@eventual/core/internal"; import { EmitEventsCallExecutor } from "../call-executors/emit-events-call-executor.js"; -import { SocketSendCallExecutor } from "../call-executors/send-socket-call-executor.js"; import type { EventClient } from "../clients/event-client.js"; import type { ExecutionQueueClient } from "../clients/execution-queue-client.js"; import type { OpenSearchClient } from "../clients/open-search-client.js"; @@ -26,6 +25,7 @@ import { NoOpWorkflowExecutor } from "./call-executors-and-factories/no-op-call- import { createSearchWorkflowQueueExecutor } from "./call-executors-and-factories/open-search-client-call.js"; import { createQueueCallWorkflowCallExecutor } from "./call-executors-and-factories/queue-call.js"; import { SendSignalWorkflowCallExecutor } from "./call-executors-and-factories/send-signal-call.js"; +import { createSocketWorkflowQueueExecutor } from "./call-executors-and-factories/send-socket-call.js"; import { SimpleWorkflowExecutorAdaptor } from "./call-executors-and-factories/simple-workflow-executor-adaptor.js"; import { TaskCallWorkflowExecutor } from "./call-executors-and-factories/task-call.js"; import { createTransactionWorkflowQueueExecutor } from "./call-executors-and-factories/transaction-call.js"; @@ -65,8 +65,9 @@ export function createDefaultWorkflowCallExecutor( EmitEventsCall: new SimpleWorkflowExecutorAdaptor( new EmitEventsCallExecutor(deps.eventClient) ), - SocketSendCall: new SimpleWorkflowExecutorAdaptor( - new SocketSendCallExecutor(deps.socketClient) + SocketCall: createSocketWorkflowQueueExecutor( + deps.socketClient, + deps.executionQueueClient ), EntityCall: createEntityWorkflowQueueExecutor( deps.entityStore, diff --git a/packages/@eventual/core-runtime/src/workflow/call-executors-and-factories/send-socket-call.ts b/packages/@eventual/core-runtime/src/workflow/call-executors-and-factories/send-socket-call.ts index a8dd0ff2e..4fe3664a5 100644 --- a/packages/@eventual/core-runtime/src/workflow/call-executors-and-factories/send-socket-call.ts +++ b/packages/@eventual/core-runtime/src/workflow/call-executors-and-factories/send-socket-call.ts @@ -1,32 +1,103 @@ +import { EventualError } from "@eventual/core"; import { - type SocketSendCall, + SocketRequestFailed, + SocketRequestSucceeded, WorkflowCallHistoryType, + WorkflowEventType, + isSocketCallOperation, + type SocketCall, + type SocketMethod, + type SocketOperation, } from "@eventual/core/internal"; -import { Result } from "../../result.js"; -import { EventualFactory } from "../call-eventual-factory.js"; -import { EventualDefinition } from "../eventual-definition.js"; +import { SocketCallExecutor } from "../../call-executors/socket-call-executor.js"; +import { ExecutionQueueClient } from "../../clients/execution-queue-client.js"; +import { SocketClient } from "../../clients/socket-client.js"; +import { Result, normalizeError } from "../../result.js"; +import type { EventualFactory } from "../call-eventual-factory.js"; +import { createEvent } from "../events.js"; +import { EventualDefinition, Trigger } from "../eventual-definition.js"; +import { WorkflowTaskQueueExecutorAdaptor } from "./task-queue-executor-adaptor.js"; + +export function createSocketWorkflowQueueExecutor( + socketClient: SocketClient, + queueClient: ExecutionQueueClient +) { + return new WorkflowTaskQueueExecutorAdaptor( + new SocketCallExecutor(socketClient), + queueClient, + async (call: SocketCall, result, { executionTime, seq }) => { + return createEvent( + { + type: WorkflowEventType.SocketRequestSucceeded, + operation: call.operation.operation, + result, + seq, + }, + executionTime + ); + }, + (call, err, { executionTime, seq }) => { + return createEvent( + { + type: WorkflowEventType.SocketRequestFailed, + operation: call.operation.operation, + seq, + ...normalizeError(err), + }, + executionTime + ); + } + ); +} export class SendSocketCallEventualFactory - implements EventualFactory + implements EventualFactory { - public initializeEventual(call: SocketSendCall): EventualDefinition { + public initializeEventual(call: SocketCall): EventualDefinition { return { + triggers: [ + Trigger.onWorkflowEvent( + WorkflowEventType.SocketRequestSucceeded, + (event) => { + return Result.resolved(event.result); + } + ), + Trigger.onWorkflowEvent( + WorkflowEventType.SocketRequestFailed, + (event) => { + return Result.failed(new EventualError(event.error, event.message)); + } + ), + ], createCallEvent: (seq) => { - const [input, base64] = - call.input instanceof Buffer - ? [call.input.toString("base64"), true] - : [call.input, false]; + if (isSocketCallOperation("send", call)) { + const [connectionId, input] = call.operation.params; + const [data, base64] = + input instanceof Buffer + ? [input.toString("base64"), true] + : [input, false]; - return { - type: WorkflowCallHistoryType.SocketMessageSent, - seq, - connectionId: call.connectionId, - input, - isBase64Encoded: base64, - socketName: call.name, - }; + return { + type: WorkflowCallHistoryType.SocketRequest, + seq, + operation: { + operation: "send", + connectionId, + input: data, + isBase64Encoded: base64, + socketName: call.operation.socketName, + }, + }; + } else { + return { + type: WorkflowCallHistoryType.SocketRequest, + seq, + operation: call.operation as SocketOperation< + Exclude + >, + }; + } }, - result: Result.resolved(undefined), }; } } diff --git a/packages/@eventual/core/src/internal/calls.ts b/packages/@eventual/core/src/internal/calls.ts index 124804681..e9b2a0010 100644 --- a/packages/@eventual/core/src/internal/calls.ts +++ b/packages/@eventual/core/src/internal/calls.ts @@ -14,6 +14,7 @@ import type { } from "../queue.js"; import type { DurationSchedule, Schedule } from "../schedule.js"; import type { SearchIndex } from "../search/search-index.js"; +import { Socket } from "../socket.js"; import type { Task } from "../task.js"; import type { Workflow, WorkflowExecutionOptions } from "../workflow.js"; import type { SignalTarget } from "./signal.js"; @@ -31,7 +32,7 @@ export type Call = | SignalHandlerCall | SearchCall | SendSignalCall - | SocketSendCall + | SocketCall | StartWorkflowCall | TaskCall | TaskRequestCall @@ -51,7 +52,7 @@ export enum CallKind { SearchCall = 11, SendSignalCall = 6, SignalHandlerCall = 5, - SocketSendCall = 16, + SocketCall = 16, StartWorkflowCall = 13, TaskCall = 0, TaskRequestCall = 12, @@ -459,12 +460,31 @@ export interface InvokeTransactionCall transactionName: string; } -export function isSocketSendCall(a: any): a is SocketSendCall { - return isCallOfKind(CallKind.SocketSendCall, a); +export type SocketMethod = Exclude< + { + [op in keyof Socket]: [Socket[op]] extends [Function] ? op : never; + }[keyof Socket], + undefined +>; + +export type SocketOperation = { + operation: Op; + socketName: string; + params: Parameters; +}; + +export function isSocketCall(a: any): a is SocketCall { + return isCallOfKind(CallKind.SocketCall, a); } -export interface SocketSendCall extends CallBase { - name: string; - connectionId: string; - input: Buffer | string; +export interface SocketCall + extends CallBase { + operation: SocketOperation; +} + +export function isSocketCallOperation( + op: Op, + operation: SocketCall +): operation is SocketCall { + return operation.operation.operation === op; } diff --git a/packages/@eventual/core/src/internal/workflow-events.ts b/packages/@eventual/core/src/internal/workflow-events.ts index 336ee4be6..213c04e4a 100644 --- a/packages/@eventual/core/src/internal/workflow-events.ts +++ b/packages/@eventual/core/src/internal/workflow-events.ts @@ -8,6 +8,8 @@ import type { EntityOperation, QueueOperation, SearchOperation, + SocketMethod, + SocketOperation, } from "./calls.js"; import type { SignalTarget } from "./signal.js"; import { or } from "./util.js"; @@ -64,7 +66,11 @@ export enum WorkflowEventType { EntityRequestSucceeded = 53, QueueRequestSucceeded = 56, QueueRequestFailed = 64, + SearchRequestSucceeded = 62, + SearchRequestFailed = 63, SignalReceived = 24, + SocketRequestFailed = 65, + SocketRequestSucceeded = 66, TaskSucceeded = 46, TaskFailed = 57, TaskHeartbeatTimedOut = 58, @@ -77,8 +83,6 @@ export enum WorkflowEventType { WorkflowRunCompleted = 80, WorkflowRunStarted = 15, WorkflowTimedOut = 90, - SearchRequestSucceeded = 62, - SearchRequestFailed = 63, } export enum WorkflowCallHistoryType { @@ -89,7 +93,7 @@ export enum WorkflowCallHistoryType { EventsEmitted = 3, SearchRequest = 4, SignalSent = 5, - SocketMessageSent = 11, + SocketRequest = 11, TaskScheduled = 7, TimerScheduled = 8, TransactionRequest = 9, @@ -116,7 +120,7 @@ export type WorkflowCallHistoryEvent = | EventsEmitted | QueueRequest | SignalSent - | SocketMessageSent + | SocketRequest | TaskScheduled | TimerScheduled | TransactionRequest; @@ -133,9 +137,11 @@ export type CompletionEvent = | EntityRequestSucceeded | QueueRequestSucceeded | QueueRequestFailed - | SignalReceived | SearchRequestSucceeded | SearchRequestFailed + | SignalReceived + | SocketRequestFailed + | SocketRequestSucceeded | TaskFailed | TaskHeartbeatTimedOut | TaskSucceeded @@ -159,7 +165,11 @@ export const isCompletionEvent = /* @__PURE__ */ or( isEntityRequestSucceeded, isQueueRequestFailed, isQueueRequestSucceeded, + isSearchRequestFailed, + isSearchRequestSucceeded, isSignalReceived, + isSocketRequestFailed, + isSocketRequestSucceeded, isTaskSucceeded, isTaskFailed, isTaskHeartbeatTimedOut, @@ -167,9 +177,7 @@ export const isCompletionEvent = /* @__PURE__ */ or( isTransactionRequestFailed, isTransactionRequestSucceeded, isWorkflowTimedOut, - isWorkflowRunStarted, - isSearchRequestFailed, - isSearchRequestSucceeded + isWorkflowRunStarted ); /** @@ -584,18 +592,50 @@ export function isEventsEmitted( return event.type === WorkflowCallHistoryType.EventsEmitted; } -export interface SocketMessageSent - extends CallEventBase { - socketName: string; - connectionId: string; - input: string; - isBase64Encoded: boolean; +export interface SocketRequest + extends CallEventBase { + operation: + | SocketOperation> + | { + operation: "send"; + socketName: string; + connectionId: string; + input: string; + isBase64Encoded: boolean; + }; +} + +export interface SocketRequestSucceeded + extends CallEventResultBase { + name?: string; + operation: SocketMethod; + result: any; +} + +export interface SocketRequestFailed + extends CallEventResultBase { + operation: SocketMethod; + name?: string; + error: string; + message: string; } -export function isSocketMessageSent( +export function isSocketRequest( event: WorkflowCallHistoryEvent -): event is SocketMessageSent { - return event.type === WorkflowCallHistoryType.SocketMessageSent; +): event is SocketRequest { + return event.type === WorkflowCallHistoryType.SocketRequest; +} + +export function isSocketRequestSucceeded( + event: WorkflowEvent +): event is SocketRequestSucceeded { + return event.type === WorkflowEventType.SocketRequestSucceeded; +} + +export function isSocketRequestFailed( + event: WorkflowEvent +): event is SocketRequestFailed { + return event.type === WorkflowEventType.SocketRequestFailed; } export interface SearchRequest diff --git a/packages/@eventual/core/src/socket.ts b/packages/@eventual/core/src/socket.ts index cdd3afbc6..1e1ffd4fd 100644 --- a/packages/@eventual/core/src/socket.ts +++ b/packages/@eventual/core/src/socket.ts @@ -1,5 +1,10 @@ import type { FunctionRuntimeProps } from "./function-props.js"; -import { CallKind, createCall, type SocketSendCall } from "./internal/calls.js"; +import { CallKind, createCall, type SocketCall } from "./internal/calls.js"; +import { + createEventualProperty, + PropertyKind, + SocketUrlsProperty, +} from "./internal/properties.js"; import { registerEventualResource } from "./internal/resources.js"; import { isSourceLocation, type SocketSpec } from "./internal/service-spec.js"; import { parseArgs } from "./internal/util.js"; @@ -35,8 +40,11 @@ export type SocketHandlers = { export type Socket = SocketSpec & { kind: "Socket"; handlers: SocketHandlers; + wssEndpoint: string; + httpEndpoint: string; } & { send: (connectionId: string, input: Buffer | string) => Promise; + delete: (connectionId: string) => Promise; }; export type SocketOptions = FunctionRuntimeProps; @@ -55,12 +63,35 @@ export function socket( kind: "Socket", handlerTimeout: options?.handlerTimeout, memorySize: options?.memorySize, - send(connectionId, input) { + get wssEndpoint() { + return getEventualHook().getEventualProperty( + createEventualProperty(PropertyKind.SocketUrls, { socketName: name }) + ).wss; + }, + get httpEndpoint() { + return getEventualHook().getEventualProperty( + createEventualProperty(PropertyKind.SocketUrls, { socketName: name }) + ).http; + }, + send(...params) { + return getEventualHook().executeEventualCall( + createCall(CallKind.SocketCall, { + operation: { + operation: "send", + socketName: name, + params, + }, + }) + ); + }, + delete(...params) { return getEventualHook().executeEventualCall( - createCall(CallKind.SocketSendCall, { - name, - connectionId, - input, + createCall(CallKind.SocketCall, { + operation: { + operation: "delete", + socketName: name, + params, + }, }) ); }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9030d01a1..520689389 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,6 +248,9 @@ importers: '@eventual/core': specifier: workspace:^ version: link:../../../packages/@eventual/core + ws: + specifier: ^8.14.1 + version: 8.14.1 zod: specifier: ^3.21.4 version: 3.21.4 @@ -270,6 +273,9 @@ importers: '@types/node': specifier: ^18 version: 18.0.0 + '@types/ws': + specifier: ^8.5.5 + version: 8.5.5 aws-cdk: specifier: ^2.80.0 version: 2.80.0 @@ -4280,14 +4286,14 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 18.0.0 + '@types/node': 18.11.8 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) + jest-config: 29.5.0(@types/node@18.11.8)(ts-node@10.9.1) jest-haste-map: 29.5.0 jest-message-util: 29.5.0 jest-regex-util: 29.4.3 @@ -4480,7 +4486,7 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.0.0 + '@types/node': 18.11.8 '@types/yargs': 17.0.24 chalk: 4.1.2 dev: true @@ -5865,6 +5871,12 @@ packages: '@types/node': 18.11.8 dev: false + /@types/ws@8.5.5: + resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} + dependencies: + '@types/node': 18.11.8 + dev: true + /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -10265,6 +10277,46 @@ packages: - supports-color dev: true + /jest-config@29.5.0(@types/node@18.11.8)(ts-node@10.9.1): + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.22.1 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.11.8 + babel-jest: 29.5.0(@babel/core@7.22.1) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1(@swc/core@1.3.19)(@types/node@18.0.0)(typescript@5.0.4) + transitivePeerDependencies: + - supports-color + dev: true + /jest-diff@29.5.0: resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10508,7 +10560,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 18.0.0 + '@types/node': 18.11.8 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -14710,6 +14762,19 @@ packages: optional: true dev: true + /ws@8.14.1: + resolution: {integrity: sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml2js@0.4.19: resolution: {integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==} dependencies: