Giter VIP home page Giter VIP logo

subscriptionless's Introduction

About

GraphQL subscriptions for AWS Lambda and API Gateway WebSockets.

Have all the functionality of GraphQL subscriptions on a stateful server without the cost.

Note: This project uses the graphql-ws protocol under the hood.

⚠️ Limitations

Seriously, read this first before you even think about using this.

This is in beta

This is beta and should be treated as such.

AWS API Gateway Limitations

There are a few noteworthy limitations to the AWS API Gateway WebSocket implementation.

Note: If you work on AWS and want to run through this, hit me up!

Socket timeouts

Default socket idleness detection in API Gateway is unpredictable.

It is strongly recommended to use socket idleness detection listed here. Alternatively, client->server pinging can be used to keep a connection alive.

Socket errors

API Gateway's current socket closing functionality doesn't support any kind of message/payload. Along with this, graphql-ws won't support error messages.

Because 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).

Setup

Create a subscriptionless instance.

import { createInstance } from 'subscriptionless';

const instance = createInstance({
  schema,
});

Export the handler.

export const gatewayHandler = instance.gatewayHandler;

Configure API Gateway

Set up API Gateway to route WebSocket events to the exported handler.

💾 serverless framework example
functions:
  websocket:
    name: my-subscription-lambda
    handler: ./handler.gatewayHandler
    events:
      - websocket:
          route: $connect
      - websocket:
          route: $disconnect
      - websocket:
          route: $default
💾 terraform example
resource "aws_apigatewayv2_api" "ws" {
  name                       = "websocket-api"
  protocol_type              = "WEBSOCKET"
  route_selection_expression = "$request.body.action"
}

resource "aws_apigatewayv2_route" "default_route" {
  api_id    = aws_apigatewayv2_api.ws.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.default_integration.id}"
}

resource "aws_apigatewayv2_route" "connect_route" {
  api_id    = aws_apigatewayv2_api.ws.id
  route_key = "$connect"
  target    = "integrations/${aws_apigatewayv2_integration.default_integration.id}"
}

resource "aws_apigatewayv2_route" "disconnect_route" {
  api_id    = aws_apigatewayv2_api.ws.id
  route_key = "$disconnect"
  target    = "integrations/${aws_apigatewayv2_integration.default_integration.id}"
}

resource "aws_apigatewayv2_integration" "default_integration" {
  api_id           = aws_apigatewayv2_api.ws.id
  integration_type = "AWS_PROXY"
  integration_uri  = aws_lambda_function.gateway_handler.invoke_arn
}

resource "aws_lambda_permission" "apigateway_invoke_lambda" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.gateway_handler.function_name
  principal     = "apigateway.amazonaws.com"
}

resource "aws_apigatewayv2_deployment" "ws" {
  api_id = aws_apigatewayv2_api.ws.id

  triggers = {
    redeployment = sha1(join(",", tolist([
      jsonencode(aws_apigatewayv2_integration.default_integration),
      jsonencode(aws_apigatewayv2_route.default_route),
      jsonencode(aws_apigatewayv2_route.connect_route),
      jsonencode(aws_apigatewayv2_route.disconnect_route),
    ])))
  }

  depends_on = [
    aws_apigatewayv2_route.default_route,
    aws_apigatewayv2_route.connect_route,
    aws_apigatewayv2_route.disconnect_route
  ]
}

resource "aws_apigatewayv2_stage" "ws" {
  api_id        = aws_apigatewayv2_api.ws.id
  name          = "example"
  deployment_id = aws_apigatewayv2_deployment.ws.id
}

Create DynanmoDB tables for state

In-flight connections and subscriptions need to be persisted.

📖 Changing DynamoDB table names

Use the tableNames argument to override the default table names.

const instance = createInstance({
  /* ... */
  tableNames: {
    connections: 'my_connections',
    subscriptions: 'my_subscriptions',
  },
});
💾 serverless framework example
resources:
  Resources:
    # Table for tracking connections
    connectionsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.CONNECTIONS_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        TimeToLiveSpecification:
          AttributeName: ttl
          Enabled: true
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
    # Table for tracking subscriptions
    subscriptionsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.SUBSCRIPTIONS_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: topic
            AttributeType: S
          - AttributeName: connectionId
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
          - AttributeName: topic
            KeyType: RANGE
        GlobalSecondaryIndexes:
          - IndexName: ConnectionIndex
            KeySchema:
              - AttributeName: connectionId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
          - IndexName: TopicIndex
            KeySchema:
              - AttributeName: topic
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
        TimeToLiveSpecification:
          AttributeName: ttl
          Enabled: true
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
💾 terraform example
resource "aws_dynamodb_table" "connections-table" {
  name           = "subscriptionless_connections"
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key = "id"

  attribute {
    name = "id"
    type = "S"
  }

  ttl {
    attribute_name = "ttl"
    enabled        = true
  }
}

resource "aws_dynamodb_table" "subscriptions-table" {
  name           = "subscriptionless_subscriptions"
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key = "id"
  range_key = "topic"

  attribute {
    name = "id"
    type = "S"
  }

  attribute {
    name = "topic"
    type = "S"
  }

  attribute {
    name = "connectionId"
    type = "S"
  }

  global_secondary_index {
    name               = "ConnectionIndex"
    hash_key           = "connectionId"
    write_capacity     = 1
    read_capacity      = 1
    projection_type    = "ALL"
  }

  global_secondary_index {
    name               = "TopicIndex"
    hash_key           = "topic"
    write_capacity     = 1
    read_capacity      = 1
    projection_type    = "ALL"
  }

  ttl {
    attribute_name = "ttl"
    enabled        = true
  }
}

Configure idleness detection (ping/pong)

Set up server->client pinging for socket idleness detection.

Note: While not a hard requirement, this is strongly recommended.

📖 Configuring instance

Pass a ping argument to configure delays and what state machine to invoke.

const instance = createInstance({
  /* ... */
  ping: {
    interval: 60, // Rate in seconds to send ping message
    timeout: 30, // Threshold for pong response before closing socket
    machineArn: process.env.MACHINE_ARN, // State machine to invoke
  },
});

Export the resulting handler for use by the state machine.

export const stateMachineHandler = instance.stateMachineHandler;
💾 serverless framework example

Create a function which exports the aforementioned machine handler.

functions:
  machine:
    handler: src/handler.stateMachineHandler

Use the serverless-step-functions plugin to create a state machine which invokes the machine handler.

stepFunctions:
  stateMachines:
    ping:
      role: !GetAtt IamRoleLambdaExecution.Arn
      definition:
        StartAt: Wait
        States:
          Eval:
            Type: Task
            Resource: !GetAtt machine.Arn
            Next: Choose
          Wait:
            Type: Wait
            SecondsPath: '$.seconds'
            Next: Eval
          Choose:
            Type: Choice
            Choices:
              - Not:
                  Variable: '$.state'
                  StringEquals: 'ABORT'
                Next: Wait
            Default: End
          End:
            Type: Pass
            End: true

The state machine arn can be passed to your websocket handler function via outputs.

Note: naming of resources will be dependent the function/machine naming in the serverless config.

functions:
  subscription:
    handler: src/handler.gatewayHandler
    environment:
      PING_STATE_MACHINE_ARN: ${self:resources.Outputs.PingStateMachine.Value}
    # ...

resources:
  Outputs:
    PingStateMachine:
      Value:
        Ref: PingStepFunctionsStateMachine

On connection_init, the state machine will be invoked. Ensure that the websocket handler has the following permissions.

- Effect: Allow
  Resource: !GetAtt PingStepFunctionsStateMachine.Arn
  Action:
    - states:StartExecution

The state machine itself will need the following permissions

- Effect: Allow
  Resource: !GetAtt connectionsTable.Arn
  Action:
    - dynamodb:GetItem
    - dynamodb:UpdateItem
- Effect: Allow
  Resource: '*'
  Action:
    - execute-api:*

Note: For a full reproduction, see the example project.

💾 terraform example

Create a function which can be invoked by the state machine.

resource "aws_lambda_function" "machine" {
  function_name    = "machine"
  runtime          = "nodejs14.x"
  filename         = data.archive_file.handler.output_path
  source_code_hash = data.archive_file.handler.output_base64sha256
  handler          = "example.stateMachineHandler"
  role             = aws_iam_role.state_machine_function.arn

  environment {
    variables = {
      CONNECTIONS_TABLE   = aws_dynamodb_table.connections.id
      SUBSCRIPTIONS_TABLE = aws_dynamodb_table.subscriptions.id
    }
  }
}

Create the following state machine which will be invoked by the gateway handler.

resource "aws_sfn_state_machine" "ping_state_machine" {
  name     = "ping-state-machine"
  role_arn = aws_iam_role.state_machine.arn
  definition = jsonencode({
    StartAt = "Wait"
    States = {
      Wait = {
        Type        = "Wait"
        SecondsPath = "$.seconds"
        Next        = "Eval"
      }
      Eval = {
        Type     = "Task"
        Resource = aws_lambda_function.machine.arn
        Next     = "Choose"
      }
      Choose = {
        Type = "Choice"
        Choices = [{
          Not = {
            Variable     = "$.state"
            StringEquals = "ABORT"
          }
          Next = "Wait"
        }]
        Default = "End"
      }
      End = {
        Type = "Pass"
        End  = true
      }
    }
  })
}

The state machine arn can be passed to your websocket handler via an environment variable.

resource "aws_lambda_function" "gateway_handler" {
  # ...

  environment {
    variables = {
      # ...
      PING_STATE_MACHINE_ARN = aws_sfn_state_machine.ping_state_machine.arn
    }
  }
}

Note: For a full reproduction, see the example project.

Usage

PubSub

subscriptionless uses it's own PubSub implementation which loosely implements the Apollo PubSub Interface.

Note: Unlike the Apollo PubSub library, this implementation is (mostly) stateless

📖 Subscribing to topics

Use the subscribe function to associate incoming subscriptions with a topic.

import { subscribe } from 'subscriptionless/subscribe';

export const resolver = {
  Subscribe: {
    mySubscription: {
      resolve: (event, args, context) => {/* ... */}
      subscribe: subscribe('MY_TOPIC'),
    }
  }
}
📖 Filtering events

Wrap any subscribe function call in a withFilter to provide filter conditions.

Note: If a function is provided, it will be called on subscription start and must return a serializable object.

import { withFilter, subscribe } from 'subscriptionless/subscribe';

// Subscription agnostic filter
withFilter(subscribe('MY_TOPIC'), {
  attr1: '`attr1` must have this value',
  attr2: {
    attr3: 'Nested attributes work fine',
  },
});

// Subscription specific filter
withFilter(subscribe('MY_TOPIC'), (root, args, context, info) => ({
  userId: args.userId,
}));
📖 Concatenating topic subscriptions

Join multiple topic subscriptions together using concat.

import { concat, subscribe } from 'subscriptionless/subscribe';

concat(subscribe('TOPIC_1'), subscribe('TOPIC_2'));
📖 Publishing events

Use the publish on your subscriptionless instance to publish events to active subscriptions.

instance.publish({
  type: 'MY_TOPIC',
  payload: 'HELLO',
});

Events can come from many sources

// SNS Event
export const snsHandler = (event) =>
  Promise.all(
    event.Records.map((r) =>
      instance.publish({
        topic: r.Sns.TopicArn.substring(r.Sns.TopicArn.lastIndexOf(':') + 1), // Get topic name (e.g. "MY_TOPIC")
        payload: JSON.parse(r.Sns.Message),
      })
    )
  );

// Manual Invocation
export const invocationHandler = (payload) =>
  instance.publish({ topic: 'MY_TOPIC', payload });

Context

Context values are accessible in all resolver level functions (resolve, subscribe, onSubscribe and onComplete).

📖 Default value

Assuming no context argument is provided, the default value is an object containing a connectionParams attribute.

This attribute contains the (optionally parsed) payload from connection_init.

export const resolver = {
  Subscribe: {
    mySubscription: {
      resolve: (event, args, context) => {
        console.log(context.connectionParams); // payload from connection_init
      },
    },
  },
};
📖 Setting static context value

An object can be provided via the context attribute when calling createInstance.

const instance = createInstance({
  /* ... */
  context: {
    myAttr: 'hello',
  },
});

The default values (above) will be appended to this object prior to execution.

📖 Setting dynamic context value

A function (optionally async) can be provided via the context attribute when calling createInstance.

The default context value is passed as an argument.

const instance = createInstance({
  /* ... */
  context: ({ connectionParams }) => ({
    myAttr: 'hello',
    user: connectionParams.user,
  }),
});

Side effects

Side effect handlers can be declared on subscription fields to handle onSubscribe (start) and onComplete (stop) events.

📖 Enabling side effects

For onSubscribe and onComplete side effects to work, resolvers must first be passed to prepareResolvers prior to schema construction.

import { prepareResolvers } from 'subscriptionless/subscribe';

const schema = makeExecutableSchema({
  typedefs,
  resolvers: prepareResolvers(resolvers),
});
📖 Adding side-effect handlers
export const resolver = {
  Subscribe: {
    mySubscription: {
      resolve: (event, args, context) => {
        /* ... */
      },
      subscribe: subscribe('MY_TOPIC'),
      onSubscribe: (root, args) => {
        /* Do something on subscription start */
      },
      onComplete: (root, args) => {
        /* Do something on subscription stop */
      },
    },
  },
};

Events

Global events can be provided when calling createInstance to track the execution cycle of the lambda.

📖 Connect (onConnect)

Called on an incoming API Gateway $connect event.

const instance = createInstance({
  /* ... */
  onConnect: ({ event }) => {
    /* */
  },
});
📖 Disconnect (onDisconnect)

Called on an incoming API Gateway $disconnect event.

const instance = createInstance({
  /* ... */
  onDisconnect: ({ event }) => {
    /* */
  },
});
📖 Authorization (connection_init)

Called on incoming graphql-ws connection_init message.

onConnectionInit can be used to verify the connection_init payload prior to persistence.

Note: Any sensitive data in the incoming message should be removed at this stage.

const instance = createInstance({
  /* ... */
  onConnectionInit: ({ message }) => {
    const token = message.payload.token;

    if (!myValidation(token)) {
      throw Error('Token validation failed');
    }

    // Prevent sensitive data from being written to DB
    return {
      ...message.payload,
      token: undefined,
    };
  },
});

By default, the (optionally parsed) payload will be accessible via context.

📖 Subscribe (onSubscribe)

Subscribe (onSubscribe)

Called on incoming graphql-ws subscribe message.

const instance = createInstance({
  /* ... */
  onSubscribe: ({ event, message }) => {
    /* */
  },
});
📖 Complete (onComplete)

Called on graphql-ws complete message.

const instance = createInstance({
  /* ... */
  onComplete: ({ event, message }) => {
    /* */
  },
});
📖 Ping (onPing)

Called on incoming graphql-ws ping message.

const instance = createInstance({
  /* ... */
  onPing: ({ event, message }) => {
    /* */
  },
});
📖 Pong (onPong)

Called on incoming graphql-ws pong message.

const instance = createInstance({
  /* ... */
  onPong: ({ event, message }) => {
    /* */
  },
});
📖 Error (onError)

Called on unexpected errors during resolution of API Gateway or graphql-ws events.

const instance = createInstance({
  /* ... */
  onError: (error, context) => {
    /* */
  },
});

subscriptionless's People

Contributors

andyrichardson avatar oyed avatar reconbot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

subscriptionless's Issues

Add CloudFlare support

About

Cloudflare supports serverless sockets (source)!

The storage and socket implementations differs substantially to AWS so a decent amount of work would need to be done (e.g. #7).

I've made some headway but it would be first good to know if there is demand for something like this.

Add sendMessage handler

I'm using architect for my dev environment and infrastructure management. The dev environment has a api gateway, ddb, lambda and sns, simulators. They're incredibly enjoyable to use locally. However, sending websocket messages in the dev environment doesn't work the same way as it does it production and I need to use it's method instead of the AWS Manager object directly.

I'd like to be able to provide my own async function to send messages, so I can use subscriptionless with architect.

The hook could be used to modify the message or prevent sending. I made up some trite examples of why you might want it, but I bet there are real use cases besides the dev server. And of course the simple case of "if the hook is set, just use that hook" works for me.

const instance = createInstance({
  /* ... */
  onSendMessage: async ({ message }) => {
    // prevent sending
    if (containsSensitiveData(message)) {
       return false
    }
    // send through another channel?
    if(highPriority(message)){
       await fastSend(message)
    }
    // send normally
    return message
  },
});

Happy to do the PR (or not) if this is something you'd add to the project.

One or more parameter values were invalid: Condition parameter type does not match schema type

firstly, thanks for this package, it seems a lot more promising than aws-lambda-graphql which we're currently using. I've followed all of the setup steps and I've got our API deploying w/ subscriptionless, but I'm getting the following error when calling instance.publish:

One or more parameter values were invalid: Condition parameter type does not match schema type

I think this is a dynamodb error...has anyone encountered this?

Add TTL for subscriptions

About

For the most part, the information being persisted in DynamoDB isn't useful for anything other than simulating a stack in memory.

DynamoDB supports TTL for cleaning up old data. In our case, we can be fairly confident that any data that is over 2 hours old is stale due to this exceeding the connection duration limit imposed by AWS

Changes

  • Add TTL option to createInstance which takes the number of minutes(?) to be appended to the creation time for TTL value (default 180)
  • Add TTL values to datamodels
  • Update serverless example to use TTL
  • Update terraform example to use TTL
  • Update example project serverless config to use TTL

withFilter example & typings

Loving the library! I'm trying to add filters to my subscriptions, but can't seem to understand the format.

Would it be possible to have the example in the repo use it?

I also noticed that since it doesn't return an AsyncIterator, typings kinda fail for graphql-codegen generated types

Mutation and Query Support

I've been using the Altair graphql client and it's got a behavior. If a websocket url is set it uses it for all queries, mutations and subscriptions. I looked into the graphql-ws client and it's an option to turn on and off. However, queries and mutations throw an error as they don't have a subscribe method.

TypeError: field.subscribe is not a function

Add storage adapters

About

It would be nice to add support multiple storage mechanisms.

Example

import { createInstance } from 'subscriptionless';
import { DynamoDBPersistence } from 'subscriptionless/dynamodb';

const instance = createInstance({
  schema,
  storage: DynamoDBPersistence({
    dynamodb,
    tableNames: {
      connections: 'my_connections'
    }
  })
});

Notes

Whether this is of any value still remains to be seen.

Drop a comment if you have a use case where something other than DynamoDB would be of use.

Context connectionId

I'd like the connectionId to always be availble in the context if possible, so I can do tracking of connections to auth information in the connectionParams.

Add batching by topic for published events

About

Resolvers should be called with an array of events (due to batching of events).

Note: Needs exploration to see if this is necessary and/or common practice

Example

Before

{
 // ...
  resolve: (event) => {
    console.log(event) // { topic: 'MY_TOPIC', payload: {} }
  }
}

After

{
 // ...
  resolve: (events) => {
    console.log(events) // [{ topic: 'MY_TOPIC', payload: {} }]
  }
}

Socket idleness detection

About

Ping/Pong

For whatever reason, AWS API Gateway does not support WebSocket protocol level ping/pong. This means early detection of unclean client disconnects is near impossible.

(graphql-ws will not implement subprotocol level ping/pong) (which is understandable).

Socket idleness

API Gateway considers an idle connection to be one where no messages have been sent on the socket for a fixed duration (currently 10 minutes).

Again, the WebSocket spec has support for detecting idle connections (ping/pong) but API Gateway doesn't use it. This means, in the case where both parties are connected, and no message is sent on the socket for the defined duration (direction agnostic), API Gateway will close the socket.

A quick fix for this is to set up immediate reconnection on the client side.

Feature request on AWS DIscussion forums

Subscription Input

I'm trying to reject a subscription based upon the arguments to the subscription.

My code looks like this (using nexusjs)

export const watchChannel = extendType({
  type: 'Subscription',
  definition(t) {
    t.field('watchChannel', {
      type: Channel,
      args: {
        id: nonNull(idArg()),
      },
      async subscribe(root, args, context){
        console.log({ root, args, context })
        const { id } = args
        const { db } = context
        if (!await db.channel.get(id)) {
          throw new Error('Unknown channel')
        }
        return subscribe('NumbersStationUpdate') 
        // you could call it - doesn't matter 
        // return subscribe('NumbersStationUpdate')(root, args, context)
      },
      resolve({ payload }) {
        return payload.num
      },
    })
  },
})
  • The typedef of subscribe is a (root: any, args: any, context: any) => Promise<AsyncGenerator<T>> (optional promise I believe). The SubscribePsuedoIterable return doesn't satisfy that. Additionally the T types the input to resolve() in nextJs it would be nice to control that.
  • I know we have other hooks, like onSubscribe for example but it can't choose what we subscribe to, as that's static on the subscribe.definitions property - I'd like it to be dynamic
  • Ideally I'd get an error event and maybe a complete event? (I'm a little light on the protocol it doesn't seem to specify the correct behavior) Currently the subscribe function doesn't execute. And Errors in the onSubscribe hook disconnect the websocket instead of providing an error event.

I'd like to propose an alternative. If the function doesn't have a .definitions property, execute it (and resolve it) and see if the result has a .definitions property, then create the subscription objects.

Also we can provide a finished Async iterator from subscribe() so the types check out and it just wont do anything, but then we could allow a typed response from subscribe

Thoughts?

Optimize execution of user event handlers

About

For now, all user-provided event handlers are executed sequentially.

await onSubscribe(); // User side-effect - blocking
// ...
// subscriptionless handling logic

It would be nice to have the option between:

  • sequential execution (user handler -> subscriptionless handler -> response)
  • concurrent execution (user handler and subscriptionless handler -> response)

Note: onConnectionInit handler should always be executed sequentially for authentication purposes

Use alongside Apollo Server

From what I can gather from the README, this seems like it's a replacement to running a GraphQL Server, like Apollo, and executing Queries/Mutations over WebSocket as well.

Would it be possible to run it alongside an Apollo instance, purely to handle Subscription? My immediate thought was to just pass the same schema to createInstance that I do to Apollo, and use createInstance for the publish function via my existing Context.

Sockets closed by the server don't contain a "Close Code" or "Close Reason"

About

API Gateway's current socket closing functionality doesn't support any kind of close code or close reason. Along with this, graphql-ws won't support error messages (which is understandable).

Feature request on AWS Discussion Forums

Constraint

Because 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).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.