Giter VIP home page Giter VIP logo

anycable-client's Introduction

Gem Version Build Coverage Status Documentation

AnyCable

AnyCable allows you to use any WebSocket server (written in any language) as a replacement for your Ruby server (such as Faye, Action Cable, etc).

AnyCable uses the same protocol as ActionCable, so you can use its JavaScript client without any monkey-patching.

AnyCable Pro has been launched 🚀

Sponsored by Evil Martians

Requirements

Usage

Check out our 📑 Documentation.

Links

Talks

Building

Generating gRPC files from .proto

  • Install required GRPC gems:
gem install grpc
gem install grpc-tools
  • Re-generate GRPC files (if necessary):
make

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/anycable/anycable.

Please, provide reproduction script (using this template) when submitting bugs if possible.

License

The gem is available as open source under the terms of the MIT License.

Security Contact

To report a security vulnerability, please contact us at [email protected]. We will coordinate the fix and disclosure.

anycable-client's People

Contributors

ardecvz avatar charlie-wasp avatar hellsquirrel avatar omarluq avatar palkan avatar theseally avatar yenshirak 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

Watchers

 avatar  avatar

anycable-client's Issues

Jest

Hi! I have included @anycable/web (lastest version) and running Jest I am getting:

    Details:

    /Users/dani/code/project/node_modules/@anycable/web/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import {
                                                                                      ^^^^^^

    SyntaxError: Cannot use import statement outside a module

       7 |   useState,
       8 | } from 'react';
    >  9 | import {
         | ^
      10 |   Cable,
      11 |   createCable,
      12 | } from '@anycable/web';

Is there anything to add in jest config or such? What am I missing?

Error: Cable is closed

Hi!

We use Anycable client in React Native to work with our backend service. It works great in all cases of connecting, reconnecting, subscribing etc. but we see the following errors in Bagsnag:

Error: Cable is closed

Stacktrace:

/Users/vagrant/git/node_modules/@anycable/core/cable/index.js:257:17 send
  send(msg) {
    if (this.state === 'closed') {
      throw Error('Cable is closed')
    }
    let data = this.encoder.encode(msg)

/Users/vagrant/git/node_modules/@anycable/core/action_cable_ext/index.js:155:19 ?anon_0_
  // Send pongs asynchrounously—no need to block the main thread
  async sendPong() {
    await new Promise(resolve => setTimeout(resolve, 0))
    this.cable.send({ command: 'pong' })
  }
}

It doesn't break anything but it seems that the library doesn't handle its own throwing errors, does it? What are the approaches to handle errors from the library?

Thanks in advance.

Stale turbo-stream from turbo-cache preview

In testing this latest release @anycable/web-0.4.0 and @anycable/core-0.4.1 with anycable-go-1.2.1 (from changes in #17), I found that a turbo-stream connection on cached turbo drive page no longer processes websocket messages.

Steps to reproduce:

  1. Load page with turbo drive and a turbo-stream element
  2. Navigate to a 2nd page (with same elements)
  3. Navigate back to first (turbo-cached) page
  4. Broadcast a turbo-stream message from the server

Results: browser logs show message received, but the javascript does not process the stream and there's no change in the current DOM.

Server Client Logs

### PAGE 1
D 2022-07-13T23:43:20.550Z context=node sid=Xyr0A-oFV6UX-7AOOu31Z Websocket closed: websocket: close 1001 (going away)
D 2022-07-13T23:43:20.550Z sid=Xyr0A-oFV6UX-7AOOu31Z WebSocket session completed
D 2022-07-13T23:43:20.550Z channel={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} context=hub sid=Xyr0A-oFV6UX-7AOOu31Z Unsubscribed
D 2022-07-13T23:43:20.550Z context=node sid=Xyr0A-oFV6UX-7AOOu31Z Disconnect {"user":"..."} http://localhost/cable?jid=.... &map[REMOTE_ADDR:127.0.0.1 cookie:....] [{"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}]
D 2022-07-13T23:43:20.550Z context=hub sid=Xyr0A-oFV6UX-7AOOu31Z Unregistered
E 2022-07-13T23:43:20.550Z context=node sid=Xyr0A-oFV6UX-7AOOu31Z Disconnect error: grpc connection is not ready
D 2022-07-13T23:43:20.742Z sid=rbAW2Y9stR1SyQHsN7h6P WebSocket session established
D 2022-07-13T23:43:20.742Z context=hub sid=rbAW2Y9stR1SyQHsN7h6P Registered with identifiers: {"user":"..."}
D 2022-07-13T23:43:20.748Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{subscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
D 2022-07-13T23:43:20.748Z context=turbo identifier={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} verified stream: user:abc123
D 2022-07-13T23:43:20.748Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Subscribed to channel: {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:43:20.748Z channel={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} context=hub sid=rbAW2Y9stR1SyQHsN7h6P stream=user:abc123 Subscribed


### PAGE 2
D 2022-07-13T23:43:49.619Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{unsubscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
D 2022-07-13T23:43:49.619Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Unsubscribed from channel: {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:43:49.619Z channel={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} context=hub sid=rbAW2Y9stR1SyQHsN7h6P Unsubscribed
D 2022-07-13T23:43:50.125Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{subscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
D 2022-07-13T23:43:50.125Z context=turbo identifier={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} verified stream: user:abc123
D 2022-07-13T23:43:50.125Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Subscribed to channel: {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:43:50.125Z channel={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} context=hub sid=rbAW2Y9stR1SyQHsN7h6P stream=user:abc123 Subscribed


### BACK TO PAGE 1 (TURBO PREVIEW)
D 2022-07-13T23:44:03.211Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{unsubscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
D 2022-07-13T23:44:03.211Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Unsubscribed from channel: {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:44:03.211Z channel={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} context=hub sid=rbAW2Y9stR1SyQHsN7h6P Unsubscribed     
D 2022-07-13T23:44:03.711Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{subscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
D 2022-07-13T23:44:03.712Z context=turbo identifier={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} verified stream: user:abc123
D 2022-07-13T23:44:03.712Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Subscribed to channel: {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:44:03.712Z channel={"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} context=hub sid=rbAW2Y9stR1SyQHsN7h6P stream=user:abc123 Subscribed
### BACK TO PAGE 1 (TURBO FETCH)               
D 2022-07-13T23:44:04.089Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{subscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
W 2022-07-13T23:44:04.089Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Failed to handle incoming message '{"command":"subscribe","identifier":"{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"...\"}"}' with error: Already subscribed to {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:44:08.716Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{subscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
W 2022-07-13T23:44:08.716Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Failed to handle incoming message '{"command":"subscribe","identifier":"{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"...\"}"}' with error: Already subscribed to {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}
D 2022-07-13T23:44:09.090Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Incoming message: &{subscribe {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."} <nil>}
W 2022-07-13T23:44:09.090Z context=node sid=rbAW2Y9stR1SyQHsN7h6P Failed to handle incoming message '{"command":"subscribe","identifier":"{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"...\"}"}' with error: Already subscribed to {"channel":"Turbo::StreamsChannel","signed_stream_name":"..."}

### BROADCAST MESSAGE
D 2022-07-14T00:12:05.289Z context=pubsub Incoming pubsub message from Redis: {"stream":"user:abc123","data":"\"\\u003cturbo-stream action=\\\"append\\\" target=\\\"turbo-alerts\\\"....\\u003c/turbo-stream\\u003e\""}
D 2022-07-14T00:12:05.289Z context=node Incoming pubsub message: &{user:abc123 "\u003cturbo-stream action=\"append\" target=\"turbo-alerts\"...\u003c/turbo-stream\u003e"}
D 2022-07-14T00:12:05.289Z context=hub stream=user:abc123 Broadcast message: "\u003cturbo-stream action=\"append\" target=\"turbo-alerts\"...\u003c/turbo-stream\u003e"

Client Logs

image

On visiting a turbo cached page, turbo loads the cached version (as a preview) and replaces it with the fetched version when loaded. To met it appears that

  1. The cached/preview DOM connects from the turbo-stream element in the cache.
  2. When turbo swaps the preview page with the fetched page, the new turbo-stream attempts to make the connection, but fails (the server logs the failure and never responds with a success).
  3. When the server broadcasts a turbo-stream message, since logic linked the stream to the cached/preview turbo-stream element (which is no longer in the DOM) it does not update the DOM.

I confirmed that disabling turbo-cache-control temporarily resolves the issue.

<meta name="turbo-cache-control" content="no-cache">

Not a permanent solution, just narrowing down where the problem exists.

FYI, I tested @anycable/web-0.3.2 and @anycable/core-0.3.3 with anycable-go-1.2.1 and don't see this issue, so this was introduced in the latest changes.

On connect callback

I have a scenario where a URL can return a 304 with the prior cached page including an AnyCable JWT token. In the event of a cached page, when the browser renders the cached page with an expired JWT token, it cannot connect.

From the examples I created the following to update the token:

import { createCable } from '@anycable/web';

export default createCable({
  tokenRefresher: async (transport) => {
    let response = await fetch('/cable_url.json');
    let data = await response.json();

    // Update URL for the underlying transport
    transport.setURL(data['url']);
  },
});

This works at updating the token when the prior cached token expired. However, when combined with <turbo-cable-stream-source /> the channels are not initiated since the attempt to establish the channels occurred prior when the elements were first added to the DOM. As a workaround, I added the following to remove and re-add the elements.

export default createCable({
  tokenRefresher: async (transport) => {
    //...

+   // Remove and re-add turbo-cable-stream-source elements to reconnect to channel
+   document.querySelectorAll('turbo-cable-stream-source').forEach((element) => {
+     element.parentNode.replaceChild(element, element);
+   });
  },
});

When testing the above code, the elements try to reconnect before the new connection is established and I get an error:

index.js:257 Uncaught (in promise) NoConnectionError: No connection
    at Cable._subscribe (index.js:257:46)
    at Cable.subscribe (index.js:245:31)
    at Cable.subscribeTo (index.js:466:17)
    at TurboStreamSourceElement.connectedCallback (stream_source_element.js:12:54)
    at cable.js:17:28
    at NodeList.forEach (<anonymous>)
    at tokenRefresher (cable.js:16:62)
    at async index.js:117:7
    at async index.js:136:11

Wrapping it in a setTimeout resolves the issue, but feels a hacky.

export default createCable({
  tokenRefresher: async (transport) => {
    //...

    // Remove and re-add turbo-cable-stream-source elements to reconnect to channel
+   setTimeout(() => {
      document.querySelectorAll('turbo-cable-stream-source').forEach((element) => {
        element.parentNode.replaceChild(element, element);
      });
+   }, 1);
  },
});

Is there a better approach, for example callback available to initiate this when the connection is established?

Testing support

Add ability to unit-test channels.

For example, by creating a test cable implementation.

Example

Having a channel:

import { Channel } from "@anycable/core";

class ChatChannel extends Channel {
  static identifier = "ChatChannel";

  async speak(message) {
    return this.perform("speak", { message });
  }

  async leave() {
    // some custom logic
    return this.disconnect();
  }
}

We would like to test it like this (using Jest):

import { ChatChannel } from './channel.js';
import { TestCable } from '@anycable/core/testing';

describe('ChatChannel', () => {
  let channel: ChatChannel
  let cable: TestCable

  beforeEach(() => {
    cable = new TestCable();
    channel = new ChatChannel({ id: '2021' });
    cable.subscribe(channel);
  })

  it('speak perform an action', async () => {
    await channel.speak("hello");
    
    expect(cable.outgoing).toEqual([{message: "hello"}])
  })

  it('disconnects when leave', () => {
    await channel.leave()

    expect(channel.state).toEqual("disconnected");
  })
})
``

Reconnect state issues

Hola,

Thanks for this awesome library! We are testing this out with a subset of our users(ourselves) as a replacement to the official Action Cable library. The customizable retry strategy is clutch for us to provide more seamless reconnects during transient network events(such as wifi network swtiches and reconnects, etc).

Unfortunately we have been running into scenarios where it appears the connection/subscription states are becoming inconsistent resulting in permanent loss of channel subscription.

This happens often during our Heroku deployments. We are setup in a non-ideal configuration with anycable and anycable-go collocated in the same dynos, however these issues have been observed outside deployments as well.

I'm having trouble reproducing locally or discovering the precise issue through code review.. This is what I have witnessed though during deployments without Heroku "preboot" enabled(meaning the anycable dynos are all stopped before the new ones are brought up):

The first reconnected WS connection lasts for a minute and only one sub ack came through:
image

After that was disconnected another conn was made however no subscription attempts are made:
image

In the console this is the first error to appear:
image

After a bunch of these errors are logged:
image

My theory is that some combination of channel/subscription pending and/or intent are not being properly reset on disconnect/reconnect depending on their states going into those events.

No subscription ack received in 5000ms, retrying. Stale connection

Hi folks!

Let me start off by saying that I'm not sure if this issue is related to the library itself and if not - apologies for wasting your time. That said, I'd really appreciate it if you could point me in the right direction when it comes to debugging the issue I'm facing.

I'm using AnyCable client as a replacement for @rails/actioncable package in conjunction with Rails. I'm NOT using AnyCable on the backend yet (we're just testing the waters at this point).
The application is hosted on Heroku alongside PSQL & Redis Green. There's a group chat written in React that establishes AC connection when you click on one of the groups (React component)

import { createConsumer } from "@anycable/web";

useEffect(() => {
  const cable = createConsumer();
  const subscription = cable.subscriptions.create(channelConfig, { 
    connected: () => {
      // fetchMessages, do other things
    },
  });
  return () => { cable.subscriptions.remove(subscription) }
}, [])

It seems to work great but sometimes I'm seeing the following messages in the console:

no subscription ack received in 5000ms, retrying subscribe {"channel":"Chat::ChannelsChannel","id":19}
Stale connection: 7965ms without pings

It can take from 2 to 20-30 seconds to obtain subscription confirmation.
I've also noticed that it's a bit easier to replicate if I visit a page, open a chat, issue a hard refresh and open the chat again although it's not a reliable replication scenario.

The backend implementation of Chat::ChannelsChannel is quite basic:

module Chat
  class ChannelsChannel < ApplicationCable::Channel
    def subscribed
      channel = Channel.find_by(id: params[:id])
      return reject unless allow_subscription_for?(channel)

      stream_for(channel)
    end

    def unsubscribed
      channel = Channel.find_by(id: params[:id])
      return if channel.nil?

      stop_stream_for(channel)
    end
  end
end

My initial assumption is that a web-socket connection from the previous visit is not closed in time which causes server not to acknowledge a new subscription but I'm not sure how realistic that is.
I was also thinking it could be related to the lack of resources (ActionCable seems to struggle with large number of connections) but we have only 3-4 users testing it at the same time.

Have you encountered anything similar before?

Thanks in advance and thank you for your hard work! ❤️

License?

I don't see any license file. Can one be decided on and added to the repository? Would a PR help?

Here is a quote from the license. It is generally interpreted as a copy of the license in the git repository, and in any downloaded copies of the code like from npm or yarn.

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

History Not Supported When Testing Resumability

Environment
npm packages:
@anycable/turbo-stream @ 0.4.0
@anycable/core @ 0.7.12
@anycable/web @ 0.7.3

AnyCable-Go version:
1.4.8
AnyCable gem version:
anycable-rails (1.4.3)
anycable (1.4.3)

Application.js

import '@hotwired/turbo'
import { start } from '@anycable/turbo-stream'
import { createCable } from '@anycable/web'

start(createCable({ protocol: 'actioncable-v1-ext-json' }))

Partial

<div id=<%= dom_id(message) %>> <%= message.content %> </div>

Index

<%= turbo_stream_from "messages" %>
<div id="messages">
  <%= render @messages %>
</div>

Error - History Not Supported

WARN 2024-01-13T20:45:26.023Z context=node sid=YVQUVol2LVgiLGoYC-lWg Failed to handle incoming message '{"command":"subscribe","identifier":"{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"ImJvb2tzIg==--7d6448fb141034dacfe0e78daf6fed7d6c1e95c39a740379b1fdf83cd29b5274\"}","history":{"since":1705178725,"streams":{}}}' with error: History not supported

Expectation
Resumability is possible.

Actual Behaviour
The subscribe command is called, the reject_history type of message is received. No messages sent during the "offline" state is received. The since param is properly set but the streams are an empty hash.

Screenshots

Screenshot 2024-01-13 at 22 00 52 Screenshot 2024-01-13 at 22 01 31

Question
Are there any other configurations required for History to be supported when the AnyCable JS client is used with turbo streams?

Thank you!

Improve re-using channels

There is common pattern to subscribe to the same channel from multiple independent components. For example:

// component-one.js
import cable from 'cable'
import { ChatChannel } from 'channels/chat'

// Build an instance of a ChatChannel class.
const channel = new ChatChannel({ roomId: '42' })

// Subscribe to the server channel via the client.
await cable.subscribe(channel)

channel.on('message', msg => console.log("component one received message", `${msg.name}: ${msg.text}`))

// component-two.js
import cable from 'cable'
import { ChatChannel } from 'channels/chat'

// Build an instance of a ChatChannel class.
const channel = new ChatChannel({ roomId: '42' })

// Subscribe to the server channel via the client.
await cable.subscribe(channel)

channel.on('message', msg => console.log("component two received message", `${msg.name}: ${msg.text}`))

We cannot build multiple instances with the equal identifiers—that would result in a server side error during the subscription.
If we extract a ChatChannel instance into its own module (easy), we still have a problem with calling potentially calling cable.subscribe(channel) multiple times.

The short term proposal is to make cable.subscribe(channel) idempotent (similarly to cable.connect()).

The long term proposal is to provide an additional abstraction to memoize channel instances, so we can do the following:

const channel = cable.fetchChannel(ChatChannel, { roomId: '42' })

Reliable streams reconnection - SubscriptionTimeoutError: Haven't received subscription ack in 10000ms

Tell us about your environment

npm packages:
@anycable/turbo-stream @ 0.4.0
@anycable/core @ 0.7.9
@anycable/web @ 0.7.3

import '@hotwired/turbo'
import { start } from '@anycable/turbo-stream'
import { createCable } from '@anycable/web'

start(createCable({ protocol: 'actioncable-v1-ext-json' }))

AnyCable-Go version:
anycable-go-pro:1.4.6-alpine

      ANYCABLE_HOST: "0.0.0.0"
      ANYCABLE_PORT: 8080
      ANYCABLE_REDIS_URL: redis://redis:6379/3
      ANYCABLE_RPC_HOST: host.docker.internal:5100
      ANYCABLE_DEBUG: 1
      ANYCABLE_JWT_ID_KEY: my-key-
      ANYCABLE_JWT_ID_ENFORCE: "true"
      ANYCABLE_HEADERS: "cookie,origin"
      ANYCABLE_PRESETS: "broker"

AnyCable gem version:
anycable-rails (1.4.0)
anycable (1.4.3)

default: &default
  access_logs_disabled: false
  log_grpc: false
  broadcast_adapter: redisx
  redis_channel: "__anycable__"
  jwt_id_key: '<%= ENV.fetch("ANYCABLE_JWT_ID_KEY") %>'
  jwt_id_ttl: 600

What did you do?

Trying to setup reliable streams and it's initially working then failing after a short period of time when reconnecting.

  • Connect and confirm broadcasts work
    • Using latest Chrome (118.0.5993.117) as the client I am letting it connect to anycable and confirm it's receiving broadcast messages from my rails application.
  • Set Chrome to 'Offline' in the Network tab of dev tools
    • Stops receiving broadcast messages
  • Set Chrome back to online after ~20s
    • 🟢 Websocket reconnects and I get the history (messages I'd missed) correctly
    • 🟠 New broadcast messages initially come through on the replaced websocket connection
    • 🔴 Shortly after I get a failed to subscribe error
      • ℹ️ Note this doesn't happen every time, seems to depend on how long I wait to reconnect but I can't figure out a pattern exactly.

What did you expect to happen?

Reconnection works, historical messages are received and new broadcasts continue to function indefinately.

What actually happened?

I get a SubscriptionTimeoutError: Haven't received subscription ack in 10000ms for {"channel":"...","signed_stream_name":"...} error ~10s after I go back online.

Up until this error I still receive new live broadcasts. Strangely I still see subsequent live broadcast messages in the websocket messages of the dev tools even after this error - they are just not processed at all by the client, I assume they're just ignored as it thinks the subscription is dead.

This doesn't happen every time, but only when the client websocket re-submits a subscription request to anycable to something it was previously subscribed to before it disconnected.
Sometimes, particularly after a short time offline, it will just reconnect, fetch history, and keep going on perfectly smoothly and never re-subscribe.

Browser console log:

image

Websocket browser messages:

Note how it is trying to subscribe again, even though the backend thinks it already is

image

From the anycable go service (debug enabled):

The backend thinks the client is still subscribed. It does even keep sending messages 'correctly' over the websocket (which are ignored).

anycable  | D 2023-10-30T07:53:39.053Z context=node sid=oKJ-9PStdmrO74aWH9ZX0 Incoming message: &{subscribe {"channel":"ActivityInstanceVisualizationChannel","charts-_lifecycle_target":"activationChannel","signed_stream_name":"Imluc3Rh~~snip~~"} <nil> {1698652416 map[activity_instance_visualization:instance=Z2lk~~snip~~:{YITt 30}]}}
anycable  | W 2023-10-30T07:53:39.054Z context=node sid=oKJ-9PStdmrO74aWH9ZX0 Failed to handle incoming message '{"command":"subscribe","identifier":"{\"channel\":\"ActivityInstanceVisualizationChannel\",\"charts-_lifecycle_target\":\"activationChannel\",\"signed_stream_name\":\"Imluc3Rh~~snip~~\"}","history":{"since":1698652416,"streams":{"activity_instance_visualization:instance=Z2lkO~~snip~~":{"epoch":"YITt","offset":30}}}}' with error: Already subscribed to {"channel":"ActivityInstanceVisualizationChannel","charts-_lifecycle_target":"activationChannel","signed_stream_name":"Imluc3Rh~~snip~~"}

Similar to anycable/anycable-go#141 and anycable/anycable-go#142

The return type for `createCable` is unresolved/any instead of Cable

Screenshot 2024-04-25 at 2 26 57 PM

This is causing type errors in our project when trying to use anycable.

I think you need to import all of the imports from @anycable/core, not export them.

import {
  CreateOptions,
  Cable,
  ActionCableConsumer,
  TokenRefresher
} from '@anycable/core'

// is this necessary?
export { CreateOptions, Cable, ActionCableConsumer, TokenRefresher }

Implement setParam for WebSocket transport

Context

The setParam method in the Transport interface is meant to configure transport object parameters (key-value).

For example, we can specify an authentication token as a transport parameter: transport.setParam('token', 'secret'). The way a transport stores the parameters is implementation specific. For WebSockets, we suggest using query string params (i.e., /cable?token=secret).

Proposal

Implement setParam method for WebSocket transport, which should update the url by adding the provided params to a query string:

let ws = new WebSocketTransport('/cable')
ws.setParam('token', 'secret')
ws.url //=> `/cable?token=secret`

// when url contains params
let ws = new WebSocketTransport('/cable?foo=bar')
ws.setParam('token', 'secret')
ws.url //=> `/cable?foo=bar&token=secret`

ws.setParam('foo', 'baz')
ws.url //=> `/cable?foo=baz&token=secret`

Thoughts on implementation

We can use URL and searchParams under the hood (support in all major browsers and Node).

An example usage:

// transform existing URL string into an object
const uri = new URL(url)
// update search params
uri.searchParams.set(key, val)
// convert back to a string 
const newURL = `${uri.protocol}//${uri.host}${uri.pathname}?${uri.searchParams}`

NOTE: The example above assume an absolute URL; we should think about how to handle a relative one.

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.