Cable Shared Worker Save

ActionCable and AnyCable Shared Worker support

Project README

Cable-shared-worker (CableSW) - ActionCable and AnyCable Shared Worker support Test/Build/Deploy

schema

Cable-shared-worker is running ActionCable or AnyCable client in a Shared Worker allows you to share a single websocket connection for multiple browser windows and tabs.

Motivation

  • It's more efficient to have a single websocket connection
  • Page refreshes and new tabs already have a websocket connection, so connection setup time is zero
  • The websocket connection runs in a separate thread/process so your UI is 'faster'
  • Cordination of event notifications is simpler as updates have a single source
  • Close connection for non active (on background) tabs (by Page Visibility API)
  • It's the cool stuff...

Install

npm install @cable-shared-worker/web @cable-shared-worker/worker
# or
yarn add @cable-shared-worker/web @cable-shared-worker/worker

Both packages should be the same version.

Web

You need to initialize worker inside your JS file:

import {initWorker} from '@cable-shared-worker/web'

await initWorker('/worker.js')

Second argument accept different options:

await initWorker(
  '/worker.js',
  {
    workerOptions: { // worker options - more info https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker/SharedWorker
      name: 'CableSW'
    },
    onError: (error) => console.error(error), // subscribe to worker errors
    fallbackToWebWorker: true, // switch to web worker on safari
    visibilityTimeout: 0, // timeout for visibility API, before close channels; 0 is disabled
    onVisibilityChange: () => ({}) // subscribe for visibility changes
  }
)

After this you can start subscription channel:

import {createChannel} from '@cable-shared-worker/web'

// Subscribe to the server channel via the client
const channel = await createChannel('ChatChannel', {roomId: 42}, (data) => {
  console.log(data)
})

// call `ChatChannel#speak(data)` on the server
channel.perform('speak', {msg: 'Hello'})

// Unsubscribe from the channel
channel.unsubscribe()

You can manually close worker (for shared worker this will only close current tab connection, but not worker itself):

import {closeWorker} from '@cable-shared-worker/web'

// close tab connection to worker
closeWorker()

This helpers may help to get info what kind of workers available in browser:

import {
  isWorkersAvailable,
  isSharedWorkerAvailable,
  isWebWorkerAvailable
} from '@cable-shared-worker/web'

isWorkersAvailable // return true, if Shared or Web worker available
isSharedWorkerAvailable // return true, if Shared worker available
isWebWorkerAvailable // return true, if Web worker available

Visibility API

You can use Page Visibility API to detect, that user move tab on background and close websocket channels. Shared Worker websocket connection can be closed, if no active channels (behaviour controlled by option closeWebsocketWithoutChannels in worker component).

import {initWorker} from '@cable-shared-worker/web'

initWorker(
  '/worker.js',
  {
    visibilityTimeout: 60, // 60 seconds wait before start close channels, default 0 is disable this functionality
    onVisibilityChange: (isVisible, isChannelsWasPaused) => { // callback for visibility changes
      if (isVisible && isChannelsWasPaused) {
        // this condition can be used to fetch data changes, because channels was closed due to tab on background
      }
    }
  }
)

Worker

In worker script (in example /worker.js) you need initialize websocket connection.

For actioncable you need installed @rails/actioncable package:

import * as actioncableLibrary from '@rails/actioncable'
import {initCableLibrary} from '@cable-shared-worker/worker'

// init actioncable library
const api = initCableLibrary({
  cableType: 'actioncable',
  cableLibrary: actioncableLibrary
})

// connect by websocket url
api.createCable(WebSocketURL)

For anycable you need install @anycable/web package:

import * as anycableLibrary from '@anycable/web'
import {initCableLibrary} from '@cable-shared-worker/worker'

// init anycable library
const api = initCableLibrary({
  cableType: 'anycable',
  cableLibrary: anycableLibrary
})

// connect by websocket url
api.createCable(WebSocketURL)

You can also use Msgpack and Protobuf protocols supported by AnyCable Pro (you must install the corresponding encoder package yourself):

import * as anycableLibrary from '@anycable/web'
import {MsgpackEncoder} from '@anycable/msgpack-encoder'
import {initCableLibrary} from '@cable-shared-worker/worker'

const api = initCableLibrary({
  cableType: 'anycable',
  cableLibrary: anycableLibrary
})

api.createCable(
  webSocketURL,
  {
    protocol: 'actioncable-v1-msgpack',
    encoder: new MsgpackEncoder()
  }
)

// or for protobuf
import * as anycableLibrary from '@anycable/web'
import {ProtobufEncoder} from '@anycable/protobuf-encoder'
import {initCableLibrary} from '@cable-shared-worker/worker'

const api = initCableLibrary({
  cableType: 'anycable',
  cableLibrary: anycableLibrary
})

api.createCable(
  webSocketURL,
  {
    protocol: 'actioncable-v1-protobuf',
    encoder: new ProtobufEncoder()
  }
)

If you need manually close websocket connection, you can use destroyCable method:

import * as actioncableLibrary from '@rails/actioncable'
import {initCableLibrary} from '@cable-shared-worker/worker'

const api = initCableLibrary({
  cableType: 'actioncable',
  cableLibrary: actioncableLibrary
})

api.createCable(WebSocketURL)

// later in code

api.destroyCable()

Method initCableLibrary accept additional option closeWebsocketWithoutChannels:

const api = initCableLibrary({
  cableType: 'actioncable',
  cableLibrary: actioncableLibrary,
  // if true (default), worker will close websocket connection, if have zero active channels
  // example: all tabs on the background send a signal to close all channels by visibility API timeout
  closeWebsocketWithoutChannels: false
})

Custom communication between window and worker

You can use cable-shared-worker for custom communication between window and worker. In window you can use method sendCommand to send custom command to worker:

import {initWorker} from '@cable-shared-worker/web'

const worker = await initWorker('/worker.js')

worker.sendCommand('WINDOW_CUSTOM_COMMAND', {data: 'example'})

On worker side you need define handleCustomWebCommand function. First argument will be custom command (in example WINDOW_CUSTOM_COMMAND), second one - command data (in example {data: 'example'}), third one - response function, which can send response command to window:

import * as actioncableLibrary from '@rails/actioncable'
import {initCableLibrary} from '@cable-shared-worker/worker'

const api = initCableLibrary({
  cableType: 'actioncable',
  cableLibrary: actioncableLibrary,
  handleCustomWebCommand: (command, data, responseFunction) => {
    responseFunction('WORKER_CUSTOM_COMMAND', {another: 'data'})
  }
})

To handle custom commands from worker in window, you need provide handleCustomWorkerCommand method in initWorker:

import {initWorker} from '@cable-shared-worker/web'

const worker = await initWorker(
  '/worker.js',
  {
    handleCustomWorkerCommand: (command, data) => {
      console.log('worker response', command, data)
    }
  }
)

worker.sendCommand('WINDOW_CUSTOM_COMMAND', {data: 'example'})

Note: You cannot send commands, that the package uses itself for communication.

Browser Support

Supported modern browsers, that support Shared Worker (IE, Opera Mini not supported).

Safari supports Shared Worker only from version 16.0 (Sep, 2022). For older version, package will switch to Web Worker, which cannot share connection between tabs. You can disable fallback to Web Worker by fallbackToWebWorker: false (or use isSharedWorkerAvailable for own logic).

Development

$ yarn # install all dependencies
$ yarn dev # run development build with watch functionality
$ yarn build # run production build
$ yarn lint # run eslint checks
$ yarn test # run tests
Open Source Agenda is not affiliated with "Cable Shared Worker" Project. README Source: le0pard/cable-shared-worker
Stars
57
Open Issues
0
Last Commit
1 month ago
License
MIT

Open Source Agenda Badge

Open Source Agenda Rating