{"id":13683104,"url":"https://github.com/andyrichardson/subscriptionless","last_synced_at":"2025-03-16T07:32:18.094Z","repository":{"id":45049535,"uuid":"338290900","full_name":"andyrichardson/subscriptionless","owner":"andyrichardson","description":"GraphQL subscriptions (and more) on serverless infrastructure","archived":false,"fork":false,"pushed_at":"2023-04-12T15:03:47.000Z","size":12971,"stargazers_count":92,"open_issues_count":12,"forks_count":3,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-02-22T01:35:32.378Z","etag":null,"topics":["api-gateway","aws","graphql","lambda","serverless-framework","subscriptions","websocket"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andyrichardson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-02-12T10:47:27.000Z","updated_at":"2024-11-17T22:09:41.000Z","dependencies_parsed_at":"2024-01-07T21:05:20.452Z","dependency_job_id":"4a51cfa3-a302-4820-920b-c31026628299","html_url":"https://github.com/andyrichardson/subscriptionless","commit_stats":{"total_commits":60,"total_committers":4,"mean_commits":15.0,"dds":0.09999999999999998,"last_synced_commit":"7851295e9ba002586ae46614bc0e53980b9d5a9a"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrichardson%2Fsubscriptionless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrichardson%2Fsubscriptionless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrichardson%2Fsubscriptionless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrichardson%2Fsubscriptionless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andyrichardson","download_url":"https://codeload.github.com/andyrichardson/subscriptionless/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243806046,"owners_count":20350775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-gateway","aws","graphql","lambda","serverless-framework","subscriptions","websocket"],"created_at":"2024-08-02T13:02:00.399Z","updated_at":"2025-03-16T07:32:17.786Z","avatar_url":"https://github.com/andyrichardson.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"## About\n\nGraphQL subscriptions for AWS Lambda and API Gateway WebSockets.\n\nHave all the functionality of GraphQL subscriptions on a stateful server without the cost.\n\n\u003e Note: This project uses the [graphql-ws protocol](https://github.com/enisdenjo/graphql-ws) under the hood.\n\n## ⚠️ Limitations\n\nSeriously, **read this first** before you even think about using this.\n\n\u003cdetails\u003e\n  \n\u003csummary\u003eThis is in beta\u003c/summary\u003e\n\nThis is beta and should be treated as such.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003eAWS API Gateway Limitations\u003c/summary\u003e\n\nThere are a few noteworthy limitations to the AWS API Gateway WebSocket implementation.\n\n\u003e Note: If you work on AWS and want to run through this, hit me up!\n\n#### Socket timeouts\n\nDefault socket idleness [detection in API Gateway is unpredictable](https://github.com/andyrichardson/subscriptionless/issues/3).\n\nIt is strongly recommended to use socket idleness detection [listed here](#configure-idleness-detection-pingpong). Alternatively, client-\u003eserver pinging can be used to keep a connection alive.\n\n#### Socket errors\n\nAPI Gateway's current socket closing functionality doesn't support any kind of message/payload. Along with this, [graphql-ws won't support error messages](https://github.com/enisdenjo/graphql-ws/issues/112).\n\nBecause of this limitation, there is no clear way to communicate subprotocol errors to the client. In the case of a subprotocol error the socket will be closed by the server (with no meaningful disconnect payload).\n\n\u003c/details\u003e\n\n## Setup\n\n#### Create a subscriptionless instance.\n\n```ts\nimport { createInstance } from 'subscriptionless';\n\nconst instance = createInstance({\n  schema,\n});\n```\n\n#### Export the handler.\n\n```ts\nexport const gatewayHandler = instance.gatewayHandler;\n```\n\n#### Configure API Gateway\n\nSet up API Gateway to route WebSocket events to the exported handler.\n\n\u003cdetails\u003e\n  \u003csummary\u003e💾 serverless framework example\u003c/summary\u003e\n\n```yaml\nfunctions:\n  websocket:\n    name: my-subscription-lambda\n    handler: ./handler.gatewayHandler\n    events:\n      - websocket:\n          route: $connect\n      - websocket:\n          route: $disconnect\n      - websocket:\n          route: $default\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e💾 terraform example\u003c/summary\u003e\n\n```tf\nresource \"aws_apigatewayv2_api\" \"ws\" {\n  name                       = \"websocket-api\"\n  protocol_type              = \"WEBSOCKET\"\n  route_selection_expression = \"$request.body.action\"\n}\n\nresource \"aws_apigatewayv2_route\" \"default_route\" {\n  api_id    = aws_apigatewayv2_api.ws.id\n  route_key = \"$default\"\n  target    = \"integrations/${aws_apigatewayv2_integration.default_integration.id}\"\n}\n\nresource \"aws_apigatewayv2_route\" \"connect_route\" {\n  api_id    = aws_apigatewayv2_api.ws.id\n  route_key = \"$connect\"\n  target    = \"integrations/${aws_apigatewayv2_integration.default_integration.id}\"\n}\n\nresource \"aws_apigatewayv2_route\" \"disconnect_route\" {\n  api_id    = aws_apigatewayv2_api.ws.id\n  route_key = \"$disconnect\"\n  target    = \"integrations/${aws_apigatewayv2_integration.default_integration.id}\"\n}\n\nresource \"aws_apigatewayv2_integration\" \"default_integration\" {\n  api_id           = aws_apigatewayv2_api.ws.id\n  integration_type = \"AWS_PROXY\"\n  integration_uri  = aws_lambda_function.gateway_handler.invoke_arn\n}\n\nresource \"aws_lambda_permission\" \"apigateway_invoke_lambda\" {\n  action        = \"lambda:InvokeFunction\"\n  function_name = aws_lambda_function.gateway_handler.function_name\n  principal     = \"apigateway.amazonaws.com\"\n}\n\nresource \"aws_apigatewayv2_deployment\" \"ws\" {\n  api_id = aws_apigatewayv2_api.ws.id\n\n  triggers = {\n    redeployment = sha1(join(\",\", tolist([\n      jsonencode(aws_apigatewayv2_integration.default_integration),\n      jsonencode(aws_apigatewayv2_route.default_route),\n      jsonencode(aws_apigatewayv2_route.connect_route),\n      jsonencode(aws_apigatewayv2_route.disconnect_route),\n    ])))\n  }\n\n  depends_on = [\n    aws_apigatewayv2_route.default_route,\n    aws_apigatewayv2_route.connect_route,\n    aws_apigatewayv2_route.disconnect_route\n  ]\n}\n\nresource \"aws_apigatewayv2_stage\" \"ws\" {\n  api_id        = aws_apigatewayv2_api.ws.id\n  name          = \"example\"\n  deployment_id = aws_apigatewayv2_deployment.ws.id\n}\n```\n\n\u003c/details\u003e\n\n#### Create DynanmoDB tables for state\n\nIn-flight connections and subscriptions need to be persisted.\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖  Changing DynamoDB table names\u003c/summary\u003e\n\nUse the `tableNames` argument to override the default table names.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  tableNames: {\n    connections: 'my_connections',\n    subscriptions: 'my_subscriptions',\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e💾 serverless framework example\u003c/summary\u003e\n\n```yaml\nresources:\n  Resources:\n    # Table for tracking connections\n    connectionsTable:\n      Type: AWS::DynamoDB::Table\n      Properties:\n        TableName: ${self:provider.environment.CONNECTIONS_TABLE}\n        AttributeDefinitions:\n          - AttributeName: id\n            AttributeType: S\n        KeySchema:\n          - AttributeName: id\n            KeyType: HASH\n        TimeToLiveSpecification:\n          AttributeName: ttl\n          Enabled: true\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n    # Table for tracking subscriptions\n    subscriptionsTable:\n      Type: AWS::DynamoDB::Table\n      Properties:\n        TableName: ${self:provider.environment.SUBSCRIPTIONS_TABLE}\n        AttributeDefinitions:\n          - AttributeName: id\n            AttributeType: S\n          - AttributeName: topic\n            AttributeType: S\n          - AttributeName: connectionId\n            AttributeType: S\n        KeySchema:\n          - AttributeName: id\n            KeyType: HASH\n          - AttributeName: topic\n            KeyType: RANGE\n        GlobalSecondaryIndexes:\n          - IndexName: ConnectionIndex\n            KeySchema:\n              - AttributeName: connectionId\n                KeyType: HASH\n            Projection:\n              ProjectionType: ALL\n            ProvisionedThroughput:\n              ReadCapacityUnits: 1\n              WriteCapacityUnits: 1\n          - IndexName: TopicIndex\n            KeySchema:\n              - AttributeName: topic\n                KeyType: HASH\n            Projection:\n              ProjectionType: ALL\n            ProvisionedThroughput:\n              ReadCapacityUnits: 1\n              WriteCapacityUnits: 1\n        TimeToLiveSpecification:\n          AttributeName: ttl\n          Enabled: true\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e💾 terraform example\u003c/summary\u003e\n\n```tf\nresource \"aws_dynamodb_table\" \"connections-table\" {\n  name           = \"subscriptionless_connections\"\n  billing_mode   = \"PROVISIONED\"\n  read_capacity  = 1\n  write_capacity = 1\n  hash_key = \"id\"\n\n  attribute {\n    name = \"id\"\n    type = \"S\"\n  }\n\n  ttl {\n    attribute_name = \"ttl\"\n    enabled        = true\n  }\n}\n\nresource \"aws_dynamodb_table\" \"subscriptions-table\" {\n  name           = \"subscriptionless_subscriptions\"\n  billing_mode   = \"PROVISIONED\"\n  read_capacity  = 1\n  write_capacity = 1\n  hash_key = \"id\"\n  range_key = \"topic\"\n\n  attribute {\n    name = \"id\"\n    type = \"S\"\n  }\n\n  attribute {\n    name = \"topic\"\n    type = \"S\"\n  }\n\n  attribute {\n    name = \"connectionId\"\n    type = \"S\"\n  }\n\n  global_secondary_index {\n    name               = \"ConnectionIndex\"\n    hash_key           = \"connectionId\"\n    write_capacity     = 1\n    read_capacity      = 1\n    projection_type    = \"ALL\"\n  }\n\n  global_secondary_index {\n    name               = \"TopicIndex\"\n    hash_key           = \"topic\"\n    write_capacity     = 1\n    read_capacity      = 1\n    projection_type    = \"ALL\"\n  }\n\n  ttl {\n    attribute_name = \"ttl\"\n    enabled        = true\n  }\n}\n```\n\n\u003c/details\u003e\n\n#### Configure idleness detection (ping/pong)\n\nSet up server-\u003eclient pinging for socket idleness detection.\n\n\u003e Note: While not a hard requirement, this is [strongly recommended](#%EF%B8%8F-limitations).\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Configuring instance\u003c/summary\u003e\n\nPass a `ping` argument to configure delays and what state machine to invoke.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  ping: {\n    interval: 60, // Rate in seconds to send ping message\n    timeout: 30, // Threshold for pong response before closing socket\n    machineArn: process.env.MACHINE_ARN, // State machine to invoke\n  },\n});\n```\n\nExport the resulting handler for use by the state machine.\n\n```ts\nexport const stateMachineHandler = instance.stateMachineHandler;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e💾 serverless framework example\u003c/summary\u003e\n\nCreate a function which exports the aforementioned machine handler.\n\n```yaml\nfunctions:\n  machine:\n    handler: src/handler.stateMachineHandler\n```\n\nUse the [serverless-step-functions](https://github.com/serverless-operations/serverless-step-functions) plugin to create a state machine which invokes the machine handler.\n\n```yaml\nstepFunctions:\n  stateMachines:\n    ping:\n      role: !GetAtt IamRoleLambdaExecution.Arn\n      definition:\n        StartAt: Wait\n        States:\n          Eval:\n            Type: Task\n            Resource: !GetAtt machine.Arn\n            Next: Choose\n          Wait:\n            Type: Wait\n            SecondsPath: '$.seconds'\n            Next: Eval\n          Choose:\n            Type: Choice\n            Choices:\n              - Not:\n                  Variable: '$.state'\n                  StringEquals: 'ABORT'\n                Next: Wait\n            Default: End\n          End:\n            Type: Pass\n            End: true\n```\n\nThe state machine _arn_ can be passed to your websocket handler function via outputs.\n\n\u003e Note: [naming of resources](https://www.serverless.com/framework/docs/providers/aws/guide/resources/) will be dependent the function/machine naming in the serverless config.\n\n```yaml\nfunctions:\n  subscription:\n    handler: src/handler.gatewayHandler\n    environment:\n      PING_STATE_MACHINE_ARN: ${self:resources.Outputs.PingStateMachine.Value}\n    # ...\n\nresources:\n  Outputs:\n    PingStateMachine:\n      Value:\n        Ref: PingStepFunctionsStateMachine\n```\n\nOn `connection_init`, the state machine will be invoked. Ensure that the websocket handler has the following permissions.\n\n```yaml\n- Effect: Allow\n  Resource: !GetAtt PingStepFunctionsStateMachine.Arn\n  Action:\n    - states:StartExecution\n```\n\nThe state machine itself will need the following permissions\n\n```yaml\n- Effect: Allow\n  Resource: !GetAtt connectionsTable.Arn\n  Action:\n    - dynamodb:GetItem\n    - dynamodb:UpdateItem\n- Effect: Allow\n  Resource: '*'\n  Action:\n    - execute-api:*\n```\n\n\u003e Note: For a full reproduction, see the example project.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e💾 terraform example\u003c/summary\u003e\n\nCreate a function which can be invoked by the state machine.\n\n```tf\nresource \"aws_lambda_function\" \"machine\" {\n  function_name    = \"machine\"\n  runtime          = \"nodejs14.x\"\n  filename         = data.archive_file.handler.output_path\n  source_code_hash = data.archive_file.handler.output_base64sha256\n  handler          = \"example.stateMachineHandler\"\n  role             = aws_iam_role.state_machine_function.arn\n\n  environment {\n    variables = {\n      CONNECTIONS_TABLE   = aws_dynamodb_table.connections.id\n      SUBSCRIPTIONS_TABLE = aws_dynamodb_table.subscriptions.id\n    }\n  }\n}\n```\n\nCreate the following state machine which will be invoked by the gateway handler.\n\n```tf\nresource \"aws_sfn_state_machine\" \"ping_state_machine\" {\n  name     = \"ping-state-machine\"\n  role_arn = aws_iam_role.state_machine.arn\n  definition = jsonencode({\n    StartAt = \"Wait\"\n    States = {\n      Wait = {\n        Type        = \"Wait\"\n        SecondsPath = \"$.seconds\"\n        Next        = \"Eval\"\n      }\n      Eval = {\n        Type     = \"Task\"\n        Resource = aws_lambda_function.machine.arn\n        Next     = \"Choose\"\n      }\n      Choose = {\n        Type = \"Choice\"\n        Choices = [{\n          Not = {\n            Variable     = \"$.state\"\n            StringEquals = \"ABORT\"\n          }\n          Next = \"Wait\"\n        }]\n        Default = \"End\"\n      }\n      End = {\n        Type = \"Pass\"\n        End  = true\n      }\n    }\n  })\n}\n```\n\nThe state machine _arn_ can be passed to your websocket handler via an environment variable.\n\n```tf\nresource \"aws_lambda_function\" \"gateway_handler\" {\n  # ...\n\n  environment {\n    variables = {\n      # ...\n      PING_STATE_MACHINE_ARN = aws_sfn_state_machine.ping_state_machine.arn\n    }\n  }\n}\n```\n\n\u003e Note: For a full reproduction, see the example project.\n\n\u003c/details\u003e\n\n## Usage\n\n### PubSub\n\n`subscriptionless` uses it's own _PubSub_ implementation which loosely implements the [Apollo PubSub Interface](https://github.com/apollographql/graphql-subscriptions#pubsub-implementations).\n\n\u003e Note: Unlike the Apollo `PubSub` library, this implementation is (mostly) stateless\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Subscribing to topics\u003c/summary\u003e\n\nUse the `subscribe` function to associate incoming subscriptions with a topic.\n\n```ts\nimport { subscribe } from 'subscriptionless/subscribe';\n\nexport const resolver = {\n  Subscribe: {\n    mySubscription: {\n      resolve: (event, args, context) =\u003e {/* ... */}\n      subscribe: subscribe('MY_TOPIC'),\n    }\n  }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Filtering events\u003c/summary\u003e\n\nWrap any `subscribe` function call in a `withFilter` to provide filter conditions.\n\n\u003e Note: If a function is provided, it will be called **on subscription start** and must return a serializable object.\n\n```ts\nimport { withFilter, subscribe } from 'subscriptionless/subscribe';\n\n// Subscription agnostic filter\nwithFilter(subscribe('MY_TOPIC'), {\n  attr1: '`attr1` must have this value',\n  attr2: {\n    attr3: 'Nested attributes work fine',\n  },\n});\n\n// Subscription specific filter\nwithFilter(subscribe('MY_TOPIC'), (root, args, context, info) =\u003e ({\n  userId: args.userId,\n}));\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Concatenating topic subscriptions\u003c/summary\u003e\n\nJoin multiple topic subscriptions together using `concat`.\n\n```tsx\nimport { concat, subscribe } from 'subscriptionless/subscribe';\n\nconcat(subscribe('TOPIC_1'), subscribe('TOPIC_2'));\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Publishing events\u003c/summary\u003e\n\nUse the `publish` on your subscriptionless instance to publish events to active subscriptions.\n\n```tsx\ninstance.publish({\n  type: 'MY_TOPIC',\n  payload: 'HELLO',\n});\n```\n\nEvents can come from many sources\n\n```tsx\n// SNS Event\nexport const snsHandler = (event) =\u003e\n  Promise.all(\n    event.Records.map((r) =\u003e\n      instance.publish({\n        topic: r.Sns.TopicArn.substring(r.Sns.TopicArn.lastIndexOf(':') + 1), // Get topic name (e.g. \"MY_TOPIC\")\n        payload: JSON.parse(r.Sns.Message),\n      })\n    )\n  );\n\n// Manual Invocation\nexport const invocationHandler = (payload) =\u003e\n  instance.publish({ topic: 'MY_TOPIC', payload });\n```\n\n\u003c/details\u003e\n\n### Context\n\nContext values are accessible in all resolver level functions (`resolve`, `subscribe`, `onSubscribe` and `onComplete`).\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Default value\u003c/summary\u003e\n\nAssuming no `context` argument is provided, the default value is an object containing a `connectionParams` attribute.\n\nThis attribute contains the [(optionally parsed)](#events) payload from `connection_init`.\n\n```ts\nexport const resolver = {\n  Subscribe: {\n    mySubscription: {\n      resolve: (event, args, context) =\u003e {\n        console.log(context.connectionParams); // payload from connection_init\n      },\n    },\n  },\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Setting static context value\u003c/summary\u003e\n\nAn object can be provided via the `context` attribute when calling `createInstance`.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  context: {\n    myAttr: 'hello',\n  },\n});\n```\n\nThe default values (above) will be appended to this object prior to execution.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Setting dynamic context value\u003c/summary\u003e\n\nA function (optionally async) can be provided via the `context` attribute when calling `createInstance`.\n\nThe default context value is passed as an argument.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  context: ({ connectionParams }) =\u003e ({\n    myAttr: 'hello',\n    user: connectionParams.user,\n  }),\n});\n```\n\n\u003c/details\u003e\n\n### Side effects\n\nSide effect handlers can be declared on subscription fields to handle `onSubscribe` (start) and `onComplete` (stop) events.\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Enabling side effects\u003c/summary\u003e\n\nFor `onSubscribe` and `onComplete` side effects to work, resolvers must first be passed to `prepareResolvers` prior to schema construction.\n\n```ts\nimport { prepareResolvers } from 'subscriptionless/subscribe';\n\nconst schema = makeExecutableSchema({\n  typedefs,\n  resolvers: prepareResolvers(resolvers),\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Adding side-effect handlers\u003c/summary\u003e\n\n```ts\nexport const resolver = {\n  Subscribe: {\n    mySubscription: {\n      resolve: (event, args, context) =\u003e {\n        /* ... */\n      },\n      subscribe: subscribe('MY_TOPIC'),\n      onSubscribe: (root, args) =\u003e {\n        /* Do something on subscription start */\n      },\n      onComplete: (root, args) =\u003e {\n        /* Do something on subscription stop */\n      },\n    },\n  },\n};\n```\n\n\u003c/details\u003e\n\n### Events\n\nGlobal events can be provided when calling `createInstance` to track the execution cycle of the lambda.\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Connect (onConnect)\u003c/summary\u003e\n\nCalled on an incoming API Gateway `$connect` event.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onConnect: ({ event }) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Disconnect (onDisconnect)\u003c/summary\u003e\n\nCalled on an incoming API Gateway `$disconnect` event.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onDisconnect: ({ event }) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Authorization (connection_init)\u003c/summary\u003e\n\nCalled on incoming graphql-ws `connection_init` message.\n\n`onConnectionInit` can be used to verify the `connection_init` payload prior to persistence.\n\n\u003e **Note:** Any sensitive data in the incoming message should be removed at this stage.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onConnectionInit: ({ message }) =\u003e {\n    const token = message.payload.token;\n\n    if (!myValidation(token)) {\n      throw Error('Token validation failed');\n    }\n\n    // Prevent sensitive data from being written to DB\n    return {\n      ...message.payload,\n      token: undefined,\n    };\n  },\n});\n```\n\nBy default, the (optionally parsed) payload will be accessible via [context](#context).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Subscribe (onSubscribe)\u003c/summary\u003e\n\n#### Subscribe (onSubscribe)\n\nCalled on incoming graphql-ws `subscribe` message.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onSubscribe: ({ event, message }) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Complete (onComplete)\u003c/summary\u003e\n\nCalled on graphql-ws `complete` message.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onComplete: ({ event, message }) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Ping (onPing)\u003c/summary\u003e\n\nCalled on incoming graphql-ws `ping` message.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onPing: ({ event, message }) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \n\u003csummary\u003e📖 Pong (onPong)\u003c/summary\u003e\n\nCalled on incoming graphql-ws `pong` message.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onPong: ({ event, message }) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Error (onError)\u003c/summary\u003e\n\nCalled on unexpected errors during resolution of API Gateway or graphql-ws events.\n\n```ts\nconst instance = createInstance({\n  /* ... */\n  onError: (error, context) =\u003e {\n    /* */\n  },\n});\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyrichardson%2Fsubscriptionless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandyrichardson%2Fsubscriptionless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyrichardson%2Fsubscriptionless/lists"}