diff --git a/packages/core/package.json b/packages/core/package.json index 77b8e963..ae2db243 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@apollo/client": "^3.9.9", + "@rails/actioncable": "^7.1.3-4", "cross-fetch": "^4.0.0", "graphql": "^16.9.0", "graphql-ruby-client": "^1.14.0" @@ -48,6 +49,7 @@ "devDependencies": { "@biomejs/biome": "1.8.3", "@types/node": "20.14.10", + "@types/rails__actioncable": "6.1.11", "@types/react": "18.3.3", "bunchee": "5.2.2", "rimraf": "6.0.0", diff --git a/packages/core/src/api/graphql/links/ActionCableLink.ts b/packages/core/src/api/graphql/links/ActionCableLink.ts index ccbc8ef2..a882abd8 100644 --- a/packages/core/src/api/graphql/links/ActionCableLink.ts +++ b/packages/core/src/api/graphql/links/ActionCableLink.ts @@ -1,10 +1,10 @@ -import { GlobalStorage } from '@/storage' import type { FetchResult, NextLink, Operation } from '@apollo/client/core/index.js' import { ApolloLink, Observable } from '@apollo/client/core/index.js' +import type { Consumer } from '@rails/actioncable' +import { createConsumer } from '@rails/actioncable' import { print } from 'graphql' -import { endpointWebsockets } from '../../../configuration' -import type { Consumer } from './actioncable' -import { createConsumer } from './actioncable' +import { GlobalStorage } from '@/storage' +import { endpointWebsockets } from '@/configuration' type RequestResult = FetchResult< { [key: string]: unknown }, diff --git a/packages/core/src/api/graphql/links/BatchHttpLink.ts b/packages/core/src/api/graphql/links/BatchHttpLink.ts index dc7bfe83..75adeb6b 100644 --- a/packages/core/src/api/graphql/links/BatchHttpLink.ts +++ b/packages/core/src/api/graphql/links/BatchHttpLink.ts @@ -1,11 +1,11 @@ import { BatchHttpLink as ApolloHttpLink } from '@apollo/client/link/batch-http/index.js' import crossfetch from 'cross-fetch' +import { endpointGraphQL } from '@/configuration' + // Use `cross-fetch` only if `fetch` is not available on the `globalThis` object const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch -import { endpointGraphQL } from '../../../configuration' - export const BatchHttpLink = new ApolloHttpLink({ uri: endpointGraphQL, fetch: effectiveFetch, diff --git a/packages/core/src/api/graphql/links/HttpLink.ts b/packages/core/src/api/graphql/links/HttpLink.ts index db5d20dd..d6df2e87 100644 --- a/packages/core/src/api/graphql/links/HttpLink.ts +++ b/packages/core/src/api/graphql/links/HttpLink.ts @@ -4,7 +4,7 @@ import crossfetch from 'cross-fetch' // Use `cross-fetch` only if `fetch` is not available on the `globalThis` object const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch -import { endpointGraphQL } from '../../../configuration' +import { endpointGraphQL } from '@/configuration' export const HttpLink = new ApolloHttpLink({ uri: endpointGraphQL, diff --git a/packages/core/src/api/graphql/links/VersionLink.ts b/packages/core/src/api/graphql/links/VersionLink.ts index 8b328af0..bd8921b5 100644 --- a/packages/core/src/api/graphql/links/VersionLink.ts +++ b/packages/core/src/api/graphql/links/VersionLink.ts @@ -1,6 +1,6 @@ import { ApolloLink } from '@apollo/client/index.js' -import { version } from '../../../configuration' +import { version } from '@/configuration' export const VersionLink = new ApolloLink((operation, forward) => { operation.setContext(({ headers = {} }) => ({ diff --git a/packages/core/src/api/graphql/links/actioncable/README.md b/packages/core/src/api/graphql/links/actioncable/README.md deleted file mode 100644 index edc52ef1..00000000 --- a/packages/core/src/api/graphql/links/actioncable/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a patched version of @rails/actioncable now that https://github.com/rails/rails/pull/47939 has been merged in and released. diff --git a/packages/core/src/api/graphql/links/actioncable/adapters.ts b/packages/core/src/api/graphql/links/actioncable/adapters.ts deleted file mode 100644 index 85c7bd94..00000000 --- a/packages/core/src/api/graphql/links/actioncable/adapters.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default { - logger: typeof globalThis !== 'undefined' ? globalThis.console : undefined, - WebSocket: typeof globalThis !== 'undefined' ? globalThis.WebSocket : undefined, -} diff --git a/packages/core/src/api/graphql/links/actioncable/connection.ts b/packages/core/src/api/graphql/links/actioncable/connection.ts deleted file mode 100644 index 95bb9633..00000000 --- a/packages/core/src/api/graphql/links/actioncable/connection.ts +++ /dev/null @@ -1,188 +0,0 @@ -// @ts-nocheck -import adapters from './adapters' -import ConnectionMonitor from './connection_monitor' -import INTERNAL from './internal' -import logger from './logger' - -// Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. - -const { message_types, protocols } = INTERNAL -const supportedProtocols = protocols.slice(0, protocols.length - 1) - -const indexOf = [].indexOf - -class Connection { - constructor(consumer) { - this.open = this.open.bind(this) - this.consumer = consumer - this.subscriptions = this.consumer.subscriptions - this.monitor = new ConnectionMonitor(this) - this.disconnected = true - } - - send(data) { - if (this.isOpen()) { - this.webSocket.send(JSON.stringify(data)) - return true - } - return false - } - - open() { - if (this.isActive()) { - logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) - return false - } - const socketProtocols = [...protocols, ...(this.consumer.subprotocols || [])] - logger.log( - `Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}` - ) - if (this.webSocket) { - this.uninstallEventHandlers() - } - this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols) - this.installEventHandlers() - this.monitor.start() - return true - } - - close({ allowReconnect } = { allowReconnect: true }) { - if (!allowReconnect) { - this.monitor.stop() - } - // Avoid closing websockets in a "connecting" state due to Safari 15.1+ bug. See: https://github.com/rails/rails/issues/43835#issuecomment-1002288478 - if (this.isOpen()) { - return this.webSocket.close() - } - } - - reopen() { - logger.log(`Reopening WebSocket, current state is ${this.getState()}`) - if (this.isActive()) { - try { - return this.close() - } catch (error) { - logger.log('Failed to reopen WebSocket', error) - } finally { - logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) - setTimeout(this.open, this.constructor.reopenDelay) - } - } else { - return this.open() - } - } - - getProtocol() { - if (this.webSocket) { - return this.webSocket.protocol - } - } - - isOpen() { - return this.isState('open') - } - - isActive() { - return this.isState('open', 'connecting') - } - - triedToReconnect() { - return this.monitor.reconnectAttempts > 0 - } - - // Private - - isProtocolSupported() { - return indexOf.call(supportedProtocols, this.getProtocol()) >= 0 - } - - isState(...states) { - return indexOf.call(states, this.getState()) >= 0 - } - - getState() { - if (this.webSocket) { - for (const state in adapters.WebSocket) { - if (adapters.WebSocket[state] === this.webSocket.readyState) { - return state.toLowerCase() - } - } - } - return null - } - - installEventHandlers() { - for (const eventName in this.events) { - const handler = this.events[eventName].bind(this) - this.webSocket[`on${eventName}`] = handler - } - } - - uninstallEventHandlers() { - for (const eventName in this.events) { - this.webSocket[`on${eventName}`] = () => {} - } - } -} - -Connection.reopenDelay = 500 - -Connection.prototype.events = { - message(event) { - if (!this.isProtocolSupported()) { - return - } - const { identifier, message, reason, reconnect, type } = JSON.parse(event.data) - switch (type) { - case message_types.welcome: - if (this.triedToReconnect()) { - this.reconnectAttempted = true - } - this.monitor.recordConnect() - return this.subscriptions.reload() - case message_types.disconnect: - logger.log(`Disconnecting. Reason: ${reason}`) - return this.close({ allowReconnect: reconnect }) - case message_types.ping: - return this.monitor.recordPing() - case message_types.confirmation: - this.subscriptions.confirmSubscription(identifier) - if (this.reconnectAttempted) { - this.reconnectAttempted = false - return this.subscriptions.notify(identifier, 'connected', { reconnected: true }) - } - return this.subscriptions.notify(identifier, 'connected', { reconnected: false }) - case message_types.rejection: - return this.subscriptions.reject(identifier) - default: - return this.subscriptions.notify(identifier, 'received', message) - } - }, - - open() { - logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) - this.disconnected = false - if (!this.isProtocolSupported()) { - logger.log('Protocol is unsupported. Stopping monitor and disconnecting.') - return this.close({ allowReconnect: false }) - } - }, - - close(_event) { - logger.log('WebSocket onclose event') - if (this.disconnected) { - return - } - this.disconnected = true - this.monitor.recordDisconnect() - return this.subscriptions.notifyAll('disconnected', { - willAttemptReconnect: this.monitor.isRunning(), - }) - }, - - error() { - logger.log('WebSocket onerror event') - }, -} - -export default Connection diff --git a/packages/core/src/api/graphql/links/actioncable/connection_monitor.ts b/packages/core/src/api/graphql/links/actioncable/connection_monitor.ts deleted file mode 100644 index cb5c2f6b..00000000 --- a/packages/core/src/api/graphql/links/actioncable/connection_monitor.ts +++ /dev/null @@ -1,141 +0,0 @@ -// @ts-nocheck -import logger from './logger' - -// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting -// revival reconnections if things go astray. Internal class, not intended for direct user manipulation. - -const now = () => new Date().getTime() - -const secondsSince = (time) => (now() - time) / 1000 - -class ConnectionMonitor { - constructor(connection) { - this.visibilityDidChange = this.visibilityDidChange.bind(this) - this.connection = connection - this.reconnectAttempts = 0 - } - - start() { - if (!this.isRunning()) { - this.startedAt = now() - // biome-ignore lint/performance/noDelete: - delete this.stoppedAt - this.startPolling() - addEventListener('visibilitychange', this.visibilityDidChange) - logger.log( - `ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s` - ) - } - } - - stop() { - if (this.isRunning()) { - this.stoppedAt = now() - this.stopPolling() - removeEventListener('visibilitychange', this.visibilityDidChange) - logger.log('ConnectionMonitor stopped') - } - } - - isRunning() { - return this.startedAt && !this.stoppedAt - } - - recordPing() { - this.pingedAt = now() - } - - recordConnect() { - this.reconnectAttempts = 0 - this.recordPing() - // biome-ignore lint/performance/noDelete: - delete this.disconnectedAt - logger.log('ConnectionMonitor recorded connect') - } - - recordDisconnect() { - this.disconnectedAt = now() - logger.log('ConnectionMonitor recorded disconnect') - } - - // Private - - startPolling() { - this.stopPolling() - this.poll() - } - - stopPolling() { - clearTimeout(this.pollTimeout) - } - - poll() { - this.pollTimeout = setTimeout(() => { - this.reconnectIfStale() - this.poll() - }, this.getPollInterval()) - } - - getPollInterval() { - const { staleThreshold, reconnectionBackoffRate } = this.constructor - const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10) - const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate - const jitter = jitterMax * Math.random() - return staleThreshold * 1000 * backoff * (1 + jitter) - } - - reconnectIfStale() { - if (this.connectionIsStale()) { - logger.log( - `ConnectionMonitor detected stale connection. reconnectAttempts = ${ - this.reconnectAttempts - }, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${ - this.constructor.staleThreshold - } s` - ) - this.reconnectAttempts++ - if (this.disconnectedRecently()) { - logger.log( - `ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince( - this.disconnectedAt - )} s` - ) - } else { - logger.log('ConnectionMonitor reopening') - this.connection.reopen() - } - } - } - - get refreshedAt() { - return this.pingedAt ? this.pingedAt : this.startedAt - } - - connectionIsStale() { - return secondsSince(this.refreshedAt) > this.constructor.staleThreshold - } - - disconnectedRecently() { - return ( - this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold - ) - } - - visibilityDidChange() { - if (document.visibilityState === 'visible') { - setTimeout(() => { - if (this.connectionIsStale() || !this.connection.isOpen()) { - logger.log( - `ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}` - ) - this.connection.reopen() - } - }, 200) - } - } -} - -ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings) -ConnectionMonitor.reconnectionBackoffRate = 0.15 - -export default ConnectionMonitor diff --git a/packages/core/src/api/graphql/links/actioncable/consumer.ts b/packages/core/src/api/graphql/links/actioncable/consumer.ts deleted file mode 100644 index e207b080..00000000 --- a/packages/core/src/api/graphql/links/actioncable/consumer.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Connection from './connection' -import Subscriptions from './subscriptions' - -// The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established, -// the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates. -// The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription -// method. -// -// The following example shows how this can be set up: -// -// App = {} -// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1") -// App.appearance = App.cable.subscriptions.create("AppearanceChannel") -// -// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. -// -// When a consumer is created, it automatically connects with the server. -// -// To disconnect from the server, call -// -// App.cable.disconnect() -// -// and to restart the connection: -// -// App.cable.connect() -// -// Any channel subscriptions which existed prior to disconnecting will -// automatically resubscribe. - -export class Consumer { - _url: string - subscriptions: Subscriptions - connection: Connection - subprotocols: Array - - constructor(url: string) { - this._url = url - this.subscriptions = new Subscriptions(this) - this.connection = new Connection(this) - this.subprotocols = [] - } - - get url() { - return createWebSocketURL(this._url) - } - - send(data: object) { - return this.connection.send(data) - } - - connect() { - return this.connection.open() - } - - disconnect() { - return this.connection.close({ allowReconnect: false }) - } - - ensureActiveConnection() { - if (!this.connection.isActive()) { - return this.connection.open() - } - } - - addSubProtocol(subprotocol: string) { - this.subprotocols = [...this.subprotocols, subprotocol] - } -} - -export function createWebSocketURL(url: string | (() => string)): string { - if (typeof url === 'function') { - url = url() - } - - if (url && !/^wss?:/i.test(url)) { - const a = document.createElement('a') - a.href = url - // biome-ignore lint/correctness/noSelfAssign: - a.href = a.href - a.protocol = a.protocol.replace('http', 'ws') - return a.href - } - return url -} - -export default Consumer diff --git a/packages/core/src/api/graphql/links/actioncable/index.ts b/packages/core/src/api/graphql/links/actioncable/index.ts deleted file mode 100644 index c76dae84..00000000 --- a/packages/core/src/api/graphql/links/actioncable/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -'use client' - -import adapters from './adapters' -import Connection from './connection' -import ConnectionMonitor from './connection_monitor' -import Consumer, { createWebSocketURL } from './consumer' -import INTERNAL from './internal' -import logger from './logger' -import Subscription from './subscription' -import SubscriptionGuarantor from './subscription_guarantor' -import Subscriptions from './subscriptions' - -export { - Connection, - ConnectionMonitor, - Consumer, - INTERNAL, - Subscription, - Subscriptions, - SubscriptionGuarantor, - adapters, - createWebSocketURL, - logger, -} - -export function createConsumer(url = getConfig('url') || INTERNAL.default_mount_path) { - return new Consumer(url) -} - -export function getConfig(name: string) { - const element = document.head.querySelector(`meta[name='action-cable-${name}']`) - if (element) { - return element.getAttribute('content') - } -} diff --git a/packages/core/src/api/graphql/links/actioncable/internal.ts b/packages/core/src/api/graphql/links/actioncable/internal.ts deleted file mode 100644 index aa08c0e4..00000000 --- a/packages/core/src/api/graphql/links/actioncable/internal.ts +++ /dev/null @@ -1,19 +0,0 @@ -const INTERNAL = { - message_types: { - welcome: 'welcome', - disconnect: 'disconnect', - ping: 'ping', - confirmation: 'confirm_subscription', - rejection: 'reject_subscription', - }, - disconnect_reasons: { - unauthorized: 'unauthorized', - invalid_request: 'invalid_request', - server_restart: 'server_restart', - remote: 'remote', - }, - default_mount_path: '/cable', - protocols: ['actioncable-v1-json', 'actioncable-unsupported'], -} - -export default INTERNAL diff --git a/packages/core/src/api/graphql/links/actioncable/logger.ts b/packages/core/src/api/graphql/links/actioncable/logger.ts deleted file mode 100644 index 7264bf3f..00000000 --- a/packages/core/src/api/graphql/links/actioncable/logger.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { debugging } from '../../../../configuration' -import adapters from './adapters' - -class Logger { - get enabled() { - return debugging - } - - log(...messages: Array) { - if (adapters.logger && this.enabled) { - messages.push(Date.now().toString()) - adapters.logger.log('[ActionCable]', ...messages) - } - } -} - -export const logger = new Logger() -export default logger diff --git a/packages/core/src/api/graphql/links/actioncable/subscription.ts b/packages/core/src/api/graphql/links/actioncable/subscription.ts deleted file mode 100644 index a5febe11..00000000 --- a/packages/core/src/api/graphql/links/actioncable/subscription.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Consumer } from './consumer' - -export type Data = { [id: string]: string | object | null | undefined } - -const extend = (object: Data, properties: Data) => { - if (properties !== null) { - for (const key in properties) { - const value = properties[key] - object[key] = value - } - } - return object -} - -export class Subscription { - consumer: Consumer - identifier: string - - // biome-ignore lint/style/useDefaultParameterLast: - constructor(consumer: Consumer, params: Data = {}, mixin: Data) { - this.consumer = consumer - this.identifier = JSON.stringify(params) - extend(this as unknown as Data, mixin) - } - - // Perform a channel action with the optional data passed as an attribute - perform(action: string, data: Data = {}) { - data.action = action - return this.send(data) - } - - send(data: object) { - return this.consumer.send({ - command: 'message', - identifier: this.identifier, - data: JSON.stringify(data), - }) - } - - unsubscribe() { - return this.consumer.subscriptions.remove(this) - } -} - -export default Subscription diff --git a/packages/core/src/api/graphql/links/actioncable/subscription_guarantor.ts b/packages/core/src/api/graphql/links/actioncable/subscription_guarantor.ts deleted file mode 100644 index f6321f16..00000000 --- a/packages/core/src/api/graphql/links/actioncable/subscription_guarantor.ts +++ /dev/null @@ -1,54 +0,0 @@ -import logger from './logger' -import type { Subscription } from './subscription' -import type { Subscriptions } from './subscriptions' - -// Responsible for ensuring channel subscribe command is confirmed, retrying until confirmation is received. -// Internal class, not intended for direct user manipulation. - -class SubscriptionGuarantor { - subscriptions: Subscriptions - pendingSubscriptions: Array - retryTimeout: ReturnType | undefined - - constructor(subscriptions: Subscriptions) { - this.subscriptions = subscriptions - this.pendingSubscriptions = [] - } - - guarantee(subscription: Subscription) { - if (this.pendingSubscriptions.indexOf(subscription) === -1) { - logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`) - this.pendingSubscriptions.push(subscription) - } else { - logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`) - } - this.startGuaranteeing() - } - - forget(subscription: Subscription) { - logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`) - this.pendingSubscriptions = this.pendingSubscriptions.filter((s) => s !== subscription) - } - - startGuaranteeing() { - this.stopGuaranteeing() - this.retrySubscribing() - } - - stopGuaranteeing() { - clearTimeout(this.retryTimeout) - } - - retrySubscribing() { - this.retryTimeout = setTimeout(() => { - if (this.subscriptions && typeof this.subscriptions.subscribe === 'function') { - this.pendingSubscriptions.map((subscription) => { - logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`) - this.subscriptions.subscribe(subscription) - }) - } - }, 500) - } -} - -export default SubscriptionGuarantor diff --git a/packages/core/src/api/graphql/links/actioncable/subscriptions.ts b/packages/core/src/api/graphql/links/actioncable/subscriptions.ts deleted file mode 100644 index 3fc19d31..00000000 --- a/packages/core/src/api/graphql/links/actioncable/subscriptions.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { Consumer } from './consumer' -import logger from './logger' -import type { Data } from './subscription' -import Subscription from './subscription' -import SubscriptionGuarantor from './subscription_guarantor' - -// Collection class for creating (and internally managing) channel subscriptions. -// The only method intended to be triggered by the user is ActionCable.Subscriptions#create, -// and it should be called through the consumer like so: -// -// App = {} -// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1") -// App.appearance = App.cable.subscriptions.create("AppearanceChannel") -// -// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. - -export class Subscriptions { - consumer: Consumer - guarantor: SubscriptionGuarantor - subscriptions: Array - - constructor(consumer: Consumer) { - this.consumer = consumer - this.guarantor = new SubscriptionGuarantor(this) - this.subscriptions = [] - } - - create(channelName: string, mixin: Data) { - const channel = channelName - const params = typeof channel === 'object' ? channel : { channel } - const subscription = new Subscription(this.consumer, params, mixin) - return this.add(subscription) - } - - // Private - - add(subscription: Subscription) { - this.subscriptions.push(subscription) - this.consumer.ensureActiveConnection() - this.notify(subscription, 'initialized') - this.subscribe(subscription) - return subscription - } - - remove(subscription: Subscription) { - this.forget(subscription) - if (!this.findAll(subscription.identifier).length) { - this.sendCommand(subscription, 'unsubscribe') - } - return subscription - } - - reject(identifier: string) { - return this.findAll(identifier).map((subscription) => { - this.forget(subscription) - this.notify(subscription, 'rejected') - return subscription - }) - } - - forget(subscription: Subscription) { - this.guarantor.forget(subscription) - this.subscriptions = this.subscriptions.filter((s) => s !== subscription) - return subscription - } - - findAll(identifier: string) { - return this.subscriptions.filter((s) => s.identifier === identifier) - } - - reload() { - return this.subscriptions.map((subscription) => this.subscribe(subscription)) - } - - notifyAll(callbackName: string, ...args: any[]) { - return this.subscriptions.map((subscription) => - this.notify(subscription, callbackName, ...args) - ) - } - - notify(subscription: Subscription, callbackName: string, ...args: any[]) { - let subscriptions: Array - if (typeof subscription === 'string') { - subscriptions = this.findAll(subscription) - } else { - subscriptions = [subscription] - } - - return subscriptions.map((subscription: any) => - typeof subscription[callbackName] === 'function' - ? subscription[callbackName](...args) - : undefined - ) - } - - subscribe(subscription: Subscription) { - if (this.sendCommand(subscription, 'subscribe')) { - this.guarantor.guarantee(subscription) - } - } - - confirmSubscription(identifier: string) { - logger.log(`Subscription confirmed ${identifier}`) - this.findAll(identifier).map((subscription) => this.guarantor.forget(subscription)) - } - - sendCommand(subscription: Subscription, command: string) { - const { identifier } = subscription - return this.consumer.send({ command, identifier }) - } -} - -export default Subscriptions diff --git a/packages/core/tests/api/graphql/links/ActionCableLink.test.ts b/packages/core/tests/api/graphql/links/ActionCableLink.test.ts index c0299302..8e28624f 100644 --- a/packages/core/tests/api/graphql/links/ActionCableLink.test.ts +++ b/packages/core/tests/api/graphql/links/ActionCableLink.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import { Observable, Operation, gql, FetchResult } from '@apollo/client/core' +import type { Operation, FetchResult } from '@apollo/client/core' +import { Observable, gql } from '@apollo/client/core' import ActionCableLink from '@/api/graphql/links/ActionCableLink' import { GlobalStorage } from '@/storage' -import { createConsumer } from '@/api/graphql/links/actioncable' +import { createConsumer } from '@rails/actioncable' vi.mock('@/storage', () => ({ GlobalStorage: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1452b0c9..0abaeff4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: '@apollo/client': specifier: ^3.9.9 version: 3.9.9(@types/react@18.3.3)(graphql@16.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rails/actioncable': + specifier: ^7.1.3-4 + version: 7.1.3-4 cross-fetch: specifier: ^4.0.0 version: 4.0.0(encoding@0.1.13) @@ -124,6 +127,9 @@ importers: '@types/node': specifier: 20.14.10 version: 20.14.10 + '@types/rails__actioncable': + specifier: 6.1.11 + version: 6.1.11 '@types/react': specifier: 18.3.3 version: 18.3.3 @@ -1609,6 +1615,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@rails/actioncable@7.1.3-4': + resolution: {integrity: sha512-54K+dO4wvL1FVnw8XFRkY8VZXTU9HdjB5QCd2dNDAkE9cJRD8bBrQZAi8zm5npL2sKGKwOSGmfZrXFE12LMYGA==} + '@react-native-community/cli-clean@11.4.1': resolution: {integrity: sha512-cwUbY3c70oBGv3FvQJWe2Qkq6m1+/dcEBonMDTYyH6i+6OrkzI4RkIGpWmbG1IS5JfE9ISUZkNL3946sxyWNkw==} @@ -2052,6 +2061,9 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/rails__actioncable@6.1.11': + resolution: {integrity: sha512-L6A3Rg6sGsv2cqalOgdOmyFvL1Pw69Mg0WuG6NtY9chzabhtkiSFY5fczo72mqRGezrMvl0Jy80v+N719CW+Tg==} + '@types/react-dom@18.2.6': resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==} @@ -6736,6 +6748,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@rails/actioncable@7.1.3-4': {} + '@react-native-community/cli-clean@11.4.1(encoding@0.1.13)': dependencies: '@react-native-community/cli-tools': 11.4.1(encoding@0.1.13) @@ -7225,6 +7239,8 @@ snapshots: '@types/prop-types@15.7.12': {} + '@types/rails__actioncable@6.1.11': {} + '@types/react-dom@18.2.6': dependencies: '@types/react': 18.3.3